sup 0.11 → 0.12
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of sup might be problematic. Click here for more details.
- data/CONTRIBUTORS +16 -5
- data/History.txt +9 -0
- data/ReleaseNotes +9 -0
- data/bin/sup +9 -24
- data/bin/sup-add +3 -3
- data/bin/sup-config +1 -1
- data/bin/sup-dump +22 -9
- data/bin/sup-recover-sources +1 -11
- data/bin/sup-sync +65 -130
- data/bin/sup-sync-back +6 -5
- data/bin/sup-tweak-labels +5 -6
- data/lib/sup.rb +89 -71
- data/lib/sup/account.rb +3 -2
- data/lib/sup/buffer.rb +28 -16
- data/lib/sup/client.rb +92 -0
- data/lib/sup/crypto.rb +91 -49
- data/lib/sup/draft.rb +14 -17
- data/lib/sup/hook.rb +10 -5
- data/lib/sup/index.rb +72 -28
- data/lib/sup/logger.rb +1 -1
- data/lib/sup/maildir.rb +55 -112
- data/lib/sup/mbox.rb +151 -6
- data/lib/sup/message-chunks.rb +20 -4
- data/lib/sup/message.rb +183 -76
- data/lib/sup/modes/compose-mode.rb +2 -1
- data/lib/sup/modes/console-mode.rb +4 -1
- data/lib/sup/modes/edit-message-mode.rb +50 -5
- data/lib/sup/modes/line-cursor-mode.rb +1 -0
- data/lib/sup/modes/reply-mode.rb +17 -11
- data/lib/sup/modes/thread-index-mode.rb +10 -9
- data/lib/sup/modes/thread-view-mode.rb +48 -2
- data/lib/sup/poll.rb +56 -60
- data/lib/sup/protocol.rb +161 -0
- data/lib/sup/sent.rb +8 -11
- data/lib/sup/server.rb +116 -0
- data/lib/sup/source.rb +15 -33
- data/lib/sup/thread.rb +6 -0
- data/lib/sup/util.rb +44 -39
- metadata +126 -88
- data/lib/sup/connection.rb +0 -63
- data/lib/sup/imap.rb +0 -349
- data/lib/sup/mbox/loader.rb +0 -180
- data/lib/sup/mbox/ssh-file.rb +0 -254
- data/lib/sup/mbox/ssh-loader.rb +0 -74
data/lib/sup/message.rb
CHANGED
@@ -9,9 +9,6 @@ module Redwood
|
|
9
9
|
## i would like, for example, to be able to add in a ruby-talk
|
10
10
|
## specific module that would detect and link to /ruby-talk:\d+/
|
11
11
|
## sequences in the text of an email. (how sweet would that be?)
|
12
|
-
##
|
13
|
-
## this class catches all source exceptions. if the underlying source
|
14
|
-
## throws an error, it is caught and handled.
|
15
12
|
|
16
13
|
class Message
|
17
14
|
SNIPPET_LEN = 80
|
@@ -26,24 +23,32 @@ class Message
|
|
26
23
|
|
27
24
|
QUOTE_PATTERN = /^\s{0,4}[>|\}]/
|
28
25
|
BLOCK_QUOTE_PATTERN = /^-----\s*Original Message\s*----+$/
|
29
|
-
SIG_PATTERN = /(
|
26
|
+
SIG_PATTERN = /(^(- )*-- ?$)|(^\s*----------+\s*$)|(^\s*_________+\s*$)|(^\s*--~--~-)|(^\s*--\+\+\*\*==)/
|
27
|
+
|
28
|
+
GPG_SIGNED_START = "-----BEGIN PGP SIGNED MESSAGE-----"
|
29
|
+
GPG_SIGNED_END = "-----END PGP SIGNED MESSAGE-----"
|
30
|
+
GPG_START = "-----BEGIN PGP MESSAGE-----"
|
31
|
+
GPG_END = "-----END PGP MESSAGE-----"
|
32
|
+
GPG_SIG_START = "-----BEGIN PGP SIGNATURE-----"
|
33
|
+
GPG_SIG_END = "-----END PGP SIGNATURE-----"
|
30
34
|
|
31
35
|
MAX_SIG_DISTANCE = 15 # lines from the end
|
32
36
|
DEFAULT_SUBJECT = ""
|
33
37
|
DEFAULT_SENDER = "(missing sender)"
|
34
38
|
MAX_HEADER_VALUE_SIZE = 4096
|
35
39
|
|
36
|
-
attr_reader :id, :date, :from, :subj, :refs, :replytos, :to,
|
40
|
+
attr_reader :id, :date, :from, :subj, :refs, :replytos, :to,
|
37
41
|
:cc, :bcc, :labels, :attachments, :list_address, :recipient_email, :replyto,
|
38
|
-
:
|
42
|
+
:list_subscribe, :list_unsubscribe
|
39
43
|
|
40
44
|
bool_reader :dirty, :source_marked_read, :snippet_contains_encrypted_content
|
41
45
|
|
46
|
+
attr_accessor :locations
|
47
|
+
|
42
48
|
## if you specify a :header, will use values from that. otherwise,
|
43
49
|
## will try and load the header from the source.
|
44
50
|
def initialize opts
|
45
|
-
@
|
46
|
-
@source_info = opts[:source_info] or raise ArgumentError, "source_info can't be nil"
|
51
|
+
@locations = opts[:locations] or raise ArgumentError, "locations can't be nil"
|
47
52
|
@snippet = opts[:snippet]
|
48
53
|
@snippet_contains_encrypted_content = false
|
49
54
|
@have_snippet = !(opts[:snippet].nil? || opts[:snippet].empty?)
|
@@ -70,14 +75,15 @@ class Message
|
|
70
75
|
def parse_header encoded_header
|
71
76
|
header = SavingHash.new { |k| decode_header_field encoded_header[k] }
|
72
77
|
|
73
|
-
@id =
|
78
|
+
@id = ''
|
79
|
+
if header["message-id"]
|
74
80
|
mid = header["message-id"] =~ /<(.+?)>/ ? $1 : header["message-id"]
|
75
|
-
sanitize_message_id mid
|
76
|
-
|
77
|
-
|
78
|
-
|
81
|
+
@id = sanitize_message_id mid
|
82
|
+
end
|
83
|
+
if (not @id.include? '@') || @id.length < 6
|
84
|
+
@id = "sup-faked-" + Digest::MD5.hexdigest(raw_header)
|
85
|
+
#from = header["from"]
|
79
86
|
#debug "faking non-existent message-id for message from #{from}: #{id}"
|
80
|
-
id
|
81
87
|
end
|
82
88
|
|
83
89
|
@from = Person.from_address(if header["from"]
|
@@ -170,10 +176,10 @@ class Message
|
|
170
176
|
|
171
177
|
attr_reader :snippet
|
172
178
|
def is_list_message?; !@list_address.nil?; end
|
173
|
-
def is_draft?; @
|
179
|
+
def is_draft?; @labels.member? :draft; end
|
174
180
|
def draft_filename
|
175
181
|
raise "not a draft" unless is_draft?
|
176
|
-
|
182
|
+
source.fn_for_offset source_info
|
177
183
|
end
|
178
184
|
|
179
185
|
## sanitize message ids by removing spaces and non-ascii characters.
|
@@ -224,77 +230,59 @@ class Message
|
|
224
230
|
@chunks
|
225
231
|
end
|
226
232
|
|
233
|
+
def location
|
234
|
+
@locations.find { |x| x.valid? } || raise(OutOfSyncSourceError.new)
|
235
|
+
end
|
236
|
+
|
237
|
+
def source
|
238
|
+
location.source
|
239
|
+
end
|
240
|
+
|
241
|
+
def source_info
|
242
|
+
location.info
|
243
|
+
end
|
244
|
+
|
227
245
|
## this is called when the message body needs to actually be loaded.
|
228
246
|
def load_from_source!
|
229
247
|
@chunks ||=
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
rescue SourceError, SocketError => e
|
246
|
-
warn "problem getting messages from #{@source}: #{e.message}"
|
247
|
-
## we need force_to_top here otherwise this window will cover
|
248
|
-
## up the error message one
|
249
|
-
@source.error ||= e
|
250
|
-
Redwood::report_broken_sources :force_to_top => true
|
251
|
-
[Chunk::Text.new(error_message(e.message).split("\n"))]
|
252
|
-
end
|
248
|
+
begin
|
249
|
+
## we need to re-read the header because it contains information
|
250
|
+
## that we don't store in the index. actually i think it's just
|
251
|
+
## the mailing list address (if any), so this is kinda overkill.
|
252
|
+
## i could just store that in the index, but i think there might
|
253
|
+
## be other things like that in the future, and i'd rather not
|
254
|
+
## bloat the index.
|
255
|
+
## actually, it's also the differentiation between to/cc/bcc,
|
256
|
+
## so i will keep this.
|
257
|
+
rmsg = location.parsed_message
|
258
|
+
parse_header rmsg.header
|
259
|
+
message_to_chunks rmsg
|
260
|
+
rescue SourceError, SocketError, RMail::EncodingUnsupportedError => e
|
261
|
+
warn "problem reading message #{id}"
|
262
|
+
[Chunk::Text.new(error_message.split("\n"))]
|
253
263
|
end
|
254
264
|
end
|
255
265
|
|
256
|
-
def error_message
|
266
|
+
def error_message
|
257
267
|
<<EOS
|
258
268
|
#@snippet...
|
259
269
|
|
260
270
|
***********************************************************************
|
261
|
-
An error occurred while loading this message.
|
262
|
-
the source has changed, or (in the case of remote sources) is down.
|
263
|
-
You can check the log for errors, though hopefully an error window
|
264
|
-
should have popped up at some point.
|
265
|
-
|
266
|
-
The message location was:
|
267
|
-
#@source##@source_info
|
271
|
+
An error occurred while loading this message.
|
268
272
|
***********************************************************************
|
269
|
-
|
270
|
-
The error message was:
|
271
|
-
#{msg}
|
272
273
|
EOS
|
273
274
|
end
|
274
275
|
|
275
|
-
## wrap any source methods that might throw sourceerrors
|
276
|
-
def with_source_errors_handled
|
277
|
-
begin
|
278
|
-
yield
|
279
|
-
rescue SourceError => e
|
280
|
-
warn "problem getting messages from #{@source}: #{e.message}"
|
281
|
-
@source.error ||= e
|
282
|
-
Redwood::report_broken_sources :force_to_top => true
|
283
|
-
error_message e.message
|
284
|
-
end
|
285
|
-
end
|
286
|
-
|
287
276
|
def raw_header
|
288
|
-
|
277
|
+
location.raw_header
|
289
278
|
end
|
290
279
|
|
291
280
|
def raw_message
|
292
|
-
|
281
|
+
location.raw_message
|
293
282
|
end
|
294
283
|
|
295
|
-
## much faster than raw_message
|
296
284
|
def each_raw_message_line &b
|
297
|
-
|
285
|
+
location.each_raw_message_line &b
|
298
286
|
end
|
299
287
|
|
300
288
|
## returns all the content from a message that will be indexed
|
@@ -336,7 +324,7 @@ EOS
|
|
336
324
|
end
|
337
325
|
|
338
326
|
def self.build_from_source source, source_info
|
339
|
-
m = Message.new :
|
327
|
+
m = Message.new :locations => [Location.new(source, source_info)]
|
340
328
|
m.load_from_source!
|
341
329
|
m
|
342
330
|
end
|
@@ -442,8 +430,21 @@ private
|
|
442
430
|
|
443
431
|
chunks
|
444
432
|
elsif m.header.content_type && m.header.content_type.downcase == "message/rfc822"
|
433
|
+
encoding = m.header["Content-Transfer-Encoding"]
|
445
434
|
if m.body
|
446
|
-
|
435
|
+
body =
|
436
|
+
case encoding
|
437
|
+
when "base64"
|
438
|
+
m.body.unpack("m")[0]
|
439
|
+
when "quoted-printable"
|
440
|
+
m.body.unpack("M")[0]
|
441
|
+
when "7bit", "8bit", nil
|
442
|
+
m.body
|
443
|
+
else
|
444
|
+
raise RMail::EncodingUnsupportedError, encoding.inspect
|
445
|
+
end
|
446
|
+
body = body.normalize_whitespace
|
447
|
+
payload = RMail::Parser.read(body)
|
447
448
|
from = payload.header.from.first ? payload.header.from.first.format : ""
|
448
449
|
to = payload.header.to.map { |p| p.format }.join(", ")
|
449
450
|
cc = payload.header.cc.map { |p| p.format }.join(", ")
|
@@ -474,10 +475,12 @@ private
|
|
474
475
|
end
|
475
476
|
else
|
476
477
|
filename =
|
477
|
-
## first, paw through the headers looking for a filename
|
478
|
-
|
478
|
+
## first, paw through the headers looking for a filename.
|
479
|
+
## RFC 2183 (Content-Disposition) specifies that disposition-parms are
|
480
|
+
## separated by ";". So, we match everything up to " and ; (if present).
|
481
|
+
if m.header["Content-Disposition"] && m.header["Content-Disposition"] =~ /filename="?(.*?[^\\])("|;|\z)/m
|
479
482
|
$1
|
480
|
-
elsif m.header["Content-Type"] && m.header["Content-Type"] =~ /name="?(.*?[^\\])("
|
483
|
+
elsif m.header["Content-Type"] && m.header["Content-Type"] =~ /name="?(.*?[^\\])("|;|\z)/im
|
481
484
|
$1
|
482
485
|
|
483
486
|
## haven't found one, but it's a non-text message. fake
|
@@ -508,12 +511,75 @@ private
|
|
508
511
|
|
509
512
|
## otherwise, it's body text
|
510
513
|
else
|
511
|
-
##
|
512
|
-
##
|
513
|
-
##
|
514
|
-
body =
|
515
|
-
|
514
|
+
## Decode the body, charset conversion will follow either in
|
515
|
+
## inline_gpg_to_chunks (for inline GPG signed messages) or
|
516
|
+
## a few lines below (messages without inline GPG)
|
517
|
+
body = m.body ? m.decode : ""
|
518
|
+
|
519
|
+
## Check for inline-PGP
|
520
|
+
chunks = inline_gpg_to_chunks body, $encoding, (m.charset || $encoding)
|
521
|
+
return chunks if chunks
|
522
|
+
|
523
|
+
if m.body
|
524
|
+
## if there's no charset, use the current encoding as the charset.
|
525
|
+
## this ensures that the body is normalized to avoid non-displayable
|
526
|
+
## characters
|
527
|
+
body = Iconv.easy_decode($encoding, m.charset || $encoding, m.decode)
|
528
|
+
else
|
529
|
+
body = ""
|
530
|
+
end
|
531
|
+
|
532
|
+
text_to_chunks(body.normalize_whitespace.split("\n"), encrypted)
|
533
|
+
end
|
534
|
+
end
|
535
|
+
end
|
536
|
+
|
537
|
+
## looks for gpg signed (but not encrypted) inline messages inside the
|
538
|
+
## message body (there is no extra header for inline GPG) or for encrypted
|
539
|
+
## (and possible signed) inline GPG messages
|
540
|
+
def inline_gpg_to_chunks body, encoding_to, encoding_from
|
541
|
+
lines = body.split("\n")
|
542
|
+
gpg = lines.between(GPG_SIGNED_START, GPG_SIGNED_END)
|
543
|
+
if !gpg.empty?
|
544
|
+
msg = RMail::Message.new
|
545
|
+
msg.body = gpg.join("\n")
|
546
|
+
|
547
|
+
body = Iconv.easy_decode(encoding_to, encoding_from, body)
|
548
|
+
lines = body.split("\n")
|
549
|
+
sig = lines.between(GPG_SIGNED_START, GPG_SIG_START)
|
550
|
+
startidx = lines.index(GPG_SIGNED_START)
|
551
|
+
endidx = lines.index(GPG_SIG_END)
|
552
|
+
before = startidx != 0 ? lines[0 .. startidx-1] : []
|
553
|
+
after = endidx ? lines[endidx+1 .. lines.size] : []
|
554
|
+
|
555
|
+
payload = RMail::Message.new
|
556
|
+
payload.body = sig[1, sig.size-2].join("\n")
|
557
|
+
return [text_to_chunks(before, false),
|
558
|
+
CryptoManager.verify(nil, msg, false),
|
559
|
+
message_to_chunks(payload),
|
560
|
+
text_to_chunks(after, false)].flatten.compact
|
561
|
+
end
|
562
|
+
|
563
|
+
gpg = lines.between(GPG_START, GPG_END)
|
564
|
+
# between does not check if GPG_END actually exists
|
565
|
+
if !gpg.empty? && !lines.index(GPG_END).nil?
|
566
|
+
msg = RMail::Message.new
|
567
|
+
msg.body = gpg.join("\n")
|
568
|
+
|
569
|
+
startidx = lines.index(GPG_START)
|
570
|
+
before = startidx != 0 ? lines[0 .. startidx-1] : []
|
571
|
+
after = lines[lines.index(GPG_END)+1 .. lines.size]
|
572
|
+
|
573
|
+
notice, sig, decryptedm = CryptoManager.decrypt msg, true
|
574
|
+
chunks = if decryptedm # managed to decrypt
|
575
|
+
children = message_to_chunks(decryptedm, true)
|
576
|
+
[notice, sig].compact + children
|
577
|
+
else
|
578
|
+
[notice]
|
516
579
|
end
|
580
|
+
return [text_to_chunks(before, false),
|
581
|
+
chunks,
|
582
|
+
text_to_chunks(after, false)].flatten.compact
|
517
583
|
end
|
518
584
|
end
|
519
585
|
|
@@ -581,7 +647,9 @@ private
|
|
581
647
|
@snippet ||= ""
|
582
648
|
@snippet += " " unless @snippet.empty?
|
583
649
|
@snippet += line.gsub(/^\s+/, "").gsub(/[\r\n]/, "").gsub(/\s+/, " ")
|
650
|
+
oldlen = @snippet.length
|
584
651
|
@snippet = @snippet[0 ... SNIPPET_LEN].chomp
|
652
|
+
@snippet += "..." if @snippet.length < oldlen
|
585
653
|
@dirty = true unless encrypted && $config[:discard_snippets_from_encrypted_messages]
|
586
654
|
@snippet_contains_encrypted_content = true if encrypted
|
587
655
|
end
|
@@ -600,4 +668,43 @@ private
|
|
600
668
|
end
|
601
669
|
end
|
602
670
|
|
671
|
+
class Location
|
672
|
+
attr_reader :source
|
673
|
+
attr_reader :info
|
674
|
+
|
675
|
+
def initialize source, info
|
676
|
+
@source = source
|
677
|
+
@info = info
|
678
|
+
end
|
679
|
+
|
680
|
+
def raw_header
|
681
|
+
source.raw_header info
|
682
|
+
end
|
683
|
+
|
684
|
+
def raw_message
|
685
|
+
source.raw_message info
|
686
|
+
end
|
687
|
+
|
688
|
+
## much faster than raw_message
|
689
|
+
def each_raw_message_line &b
|
690
|
+
source.each_raw_message_line info, &b
|
691
|
+
end
|
692
|
+
|
693
|
+
def parsed_message
|
694
|
+
source.load_message info
|
695
|
+
end
|
696
|
+
|
697
|
+
def valid?
|
698
|
+
source.valid? info
|
699
|
+
end
|
700
|
+
|
701
|
+
def == o
|
702
|
+
o.source.id == source.id and o.info == info
|
703
|
+
end
|
704
|
+
|
705
|
+
def hash
|
706
|
+
[source.id, info].hash
|
707
|
+
end
|
708
|
+
end
|
709
|
+
|
603
710
|
end
|
@@ -21,12 +21,13 @@ class ComposeMode < EditMessageMode
|
|
21
21
|
end
|
22
22
|
|
23
23
|
def self.spawn_nicely opts={}
|
24
|
+
from = opts[:from] || (BufferManager.ask_for_account(:account, "From (default #{AccountManager.default_account.email}): ") or return if $config[:ask_for_from])
|
24
25
|
to = opts[:to] || (BufferManager.ask_for_contacts(:people, "To: ", [opts[:to_default]]) or return if ($config[:ask_for_to] != false))
|
25
26
|
cc = opts[:cc] || (BufferManager.ask_for_contacts(:people, "Cc: ") or return if $config[:ask_for_cc])
|
26
27
|
bcc = opts[:bcc] || (BufferManager.ask_for_contacts(:people, "Bcc: ") or return if $config[:ask_for_bcc])
|
27
28
|
subj = opts[:subj] || (BufferManager.ask(:subject, "Subject: ") or return if $config[:ask_for_subject])
|
28
29
|
|
29
|
-
mode = ComposeMode.new :from =>
|
30
|
+
mode = ComposeMode.new :from => from, :to => to, :cc => cc, :bcc => bcc, :subj => subj
|
30
31
|
BufferManager.spawn "New Message", mode
|
31
32
|
mode.edit_message
|
32
33
|
end
|
@@ -8,7 +8,7 @@ class Console
|
|
8
8
|
end
|
9
9
|
|
10
10
|
def query(query)
|
11
|
-
|
11
|
+
Enumerator.new(Index.instance, :each_message, Index.parse_query(query))
|
12
12
|
end
|
13
13
|
|
14
14
|
def add_labels(query, *labels)
|
@@ -26,6 +26,9 @@ class Console
|
|
26
26
|
|
27
27
|
def special_methods; methods - Object.methods end
|
28
28
|
|
29
|
+
def puts x; @mode << "#{x.to_s.rstrip}\n" end
|
30
|
+
def p x; puts x.inspect end
|
31
|
+
|
29
32
|
## files that won't cause problems when reloaded
|
30
33
|
## TODO expand this list / convert to blacklist
|
31
34
|
RELOAD_WHITELIST = %w(sup/index.rb sup/modes/console-mode.rb)
|
@@ -1,7 +1,6 @@
|
|
1
1
|
require 'tempfile'
|
2
2
|
require 'socket' # just for gethostname!
|
3
3
|
require 'pathname'
|
4
|
-
require 'rmail'
|
5
4
|
|
6
5
|
module Redwood
|
7
6
|
|
@@ -58,6 +57,18 @@ Return value:
|
|
58
57
|
none
|
59
58
|
EOS
|
60
59
|
|
60
|
+
HookManager.register "sendmail", <<EOS
|
61
|
+
Sends the given mail. If this hook doesn't exist, the sendmail command
|
62
|
+
configured for the account is used.
|
63
|
+
The message will be saved after this hook is run, so any modification to it
|
64
|
+
will be recorded.
|
65
|
+
Variables:
|
66
|
+
message: RMail::Message instance of the mail to send
|
67
|
+
account: Account instance matching the From address
|
68
|
+
Return value:
|
69
|
+
True if mail has been sent successfully, false otherwise.
|
70
|
+
EOS
|
71
|
+
|
61
72
|
attr_reader :status
|
62
73
|
attr_accessor :body, :header
|
63
74
|
bool_reader :edited
|
@@ -207,7 +218,7 @@ protected
|
|
207
218
|
def mime_encode string
|
208
219
|
string = [string].pack('M') # basic quoted-printable
|
209
220
|
string.gsub!(/=\n/,'') # .. remove trailing newline
|
210
|
-
string.gsub!(/_/,'=
|
221
|
+
string.gsub!(/_/,'=5F') # .. encode underscores
|
211
222
|
string.gsub!(/\?/,'=3F') # .. encode question marks
|
212
223
|
string.gsub!(/ /,'_') # .. translate space to underscores
|
213
224
|
"=?utf-8?q?#{string}?="
|
@@ -341,8 +352,17 @@ protected
|
|
341
352
|
begin
|
342
353
|
date = Time.now
|
343
354
|
m = build_message date
|
344
|
-
|
345
|
-
|
355
|
+
|
356
|
+
if HookManager.enabled? "sendmail"
|
357
|
+
if not HookManager.run "sendmail", :message => m, :account => acct
|
358
|
+
warn "Sendmail hook was not successful"
|
359
|
+
return false
|
360
|
+
end
|
361
|
+
else
|
362
|
+
IO.popen(acct.sendmail, "w") { |p| p.puts m }
|
363
|
+
raise SendmailCommandFailed, "Couldn't execute #{acct.sendmail}" unless $? == 0
|
364
|
+
end
|
365
|
+
|
346
366
|
SentManager.write_sent_message(date, from_email) { |f| f.puts sanitize_body(m.to_s) }
|
347
367
|
BufferManager.kill_buffer buffer
|
348
368
|
BufferManager.flash "Message sent!"
|
@@ -382,6 +402,11 @@ protected
|
|
382
402
|
if @crypto_selector && @crypto_selector.val != :none
|
383
403
|
from_email = Person.from_address(@header["From"]).email
|
384
404
|
to_email = [@header["To"], @header["Cc"], @header["Bcc"]].flatten.compact.map { |p| Person.from_address(p).email }
|
405
|
+
if m.multipart?
|
406
|
+
m.each_part {|p| p = transfer_encode p}
|
407
|
+
else
|
408
|
+
m = transfer_encode m
|
409
|
+
end
|
385
410
|
|
386
411
|
m = CryptoManager.send @crypto_selector.val, from_email, to_email, m
|
387
412
|
end
|
@@ -401,7 +426,8 @@ protected
|
|
401
426
|
m.header["Date"] = date.rfc2822
|
402
427
|
m.header["Message-Id"] = @message_id
|
403
428
|
m.header["User-Agent"] = "Sup/#{Redwood::VERSION}"
|
404
|
-
m.header["Content-Transfer-Encoding"]
|
429
|
+
m.header["Content-Transfer-Encoding"] ||= '8bit'
|
430
|
+
m.header["MIME-Version"] = "1.0" if m.multipart?
|
405
431
|
m
|
406
432
|
end
|
407
433
|
|
@@ -497,6 +523,25 @@ private
|
|
497
523
|
[]
|
498
524
|
end
|
499
525
|
end
|
526
|
+
|
527
|
+
def transfer_encode msg_part
|
528
|
+
## return the message unchanged if it's already encoded
|
529
|
+
if (msg_part.header["Content-Transfer-Encoding"] == "base64" ||
|
530
|
+
msg_part.header["Content-Transfer-Encoding"] == "quoted-printable")
|
531
|
+
return msg_part
|
532
|
+
end
|
533
|
+
|
534
|
+
## encode to quoted-printable for all text/* MIME types,
|
535
|
+
## use base64 otherwise
|
536
|
+
if msg_part.header["Content-Type"] =~ /text\/.*/
|
537
|
+
msg_part.header["Content-Transfer-Encoding"] = 'quoted-printable'
|
538
|
+
msg_part.body = [msg_part.body].pack('M')
|
539
|
+
else
|
540
|
+
msg_part.header["Content-Transfer-Encoding"] = 'base64'
|
541
|
+
msg_part.body = [msg_part.body].pack('m')
|
542
|
+
end
|
543
|
+
msg_part
|
544
|
+
end
|
500
545
|
end
|
501
546
|
|
502
547
|
end
|