schleuder 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data.tar.gz.sig +0 -0
- data/LICENSE +339 -0
- data/README +32 -0
- data/bin/schleuder +96 -0
- data/bin/schleuder-fix-gem-dependencies +30 -0
- data/bin/schleuder-init-setup +37 -0
- data/bin/schleuder-migrate-v2.1-to-v2.2 +205 -0
- data/bin/schleuder-newlist +384 -0
- data/contrib/check-expired-keys.rb +59 -0
- data/contrib/mutt-schleuder-colors.rc +10 -0
- data/contrib/mutt-schleuder-resend.vim +24 -0
- data/contrib/smtpserver.rb +76 -0
- data/ext/default-list.conf +146 -0
- data/ext/default-members.conf +7 -0
- data/ext/list.conf.example +14 -0
- data/ext/schleuder.conf +62 -0
- data/lib/schleuder.rb +49 -0
- data/lib/schleuder/archiver.rb +46 -0
- data/lib/schleuder/crypt.rb +188 -0
- data/lib/schleuder/errors.rb +5 -0
- data/lib/schleuder/list.rb +177 -0
- data/lib/schleuder/list_config.rb +146 -0
- data/lib/schleuder/log/listlogger.rb +56 -0
- data/lib/schleuder/log/outputter/emailoutputter.rb +118 -0
- data/lib/schleuder/log/outputter/metaemailoutputter.rb +50 -0
- data/lib/schleuder/log/schleuderlogger.rb +23 -0
- data/lib/schleuder/mail.rb +861 -0
- data/lib/schleuder/mailer.rb +26 -0
- data/lib/schleuder/member.rb +69 -0
- data/lib/schleuder/plugin.rb +54 -0
- data/lib/schleuder/processor.rb +363 -0
- data/lib/schleuder/schleuder_config.rb +72 -0
- data/lib/schleuder/storage.rb +84 -0
- data/lib/schleuder/utils.rb +80 -0
- data/lib/schleuder/version.rb +3 -0
- data/man/schleuder-newlist.8 +191 -0
- data/man/schleuder.8 +400 -0
- data/plugins/README +20 -0
- data/plugins/manage_keys_plugin.rb +113 -0
- data/plugins/manage_members_plugin.rb +152 -0
- data/plugins/manage_self_plugin.rb +26 -0
- data/plugins/resend_plugin.rb +35 -0
- data/plugins/version_plugin.rb +12 -0
- metadata +178 -0
- metadata.gz.sig +2 -0
@@ -0,0 +1,56 @@
|
|
1
|
+
module Schleuder
|
2
|
+
class ListLogger < Log4r::Logger
|
3
|
+
# Instantiates the list-logger and sets outputters and level.
|
4
|
+
# Warning: Do *not* rely on the list-object here! It's not yet available
|
5
|
+
# (this actually is part of setting it up) and you'd produce loops and
|
6
|
+
# break the whole thing.
|
7
|
+
def initialize(listname, listdir, config)
|
8
|
+
# Initialize self
|
9
|
+
super('list')
|
10
|
+
|
11
|
+
# define the initial log_level for all outputters (they inherit it)
|
12
|
+
@level = eval("Log4r::#{config.log_level.upcase}")
|
13
|
+
|
14
|
+
# Setting up outputters.
|
15
|
+
fmtr = Log4r::PatternFormatter.new(:pattern => "%d #{listname} %l\t%M")
|
16
|
+
|
17
|
+
if config.log_file
|
18
|
+
require 'log4r/outputter/fileoutputter'
|
19
|
+
filename = config.log_file
|
20
|
+
filename = File.join(listdir, filename) unless filename[0..0].eql?('/')
|
21
|
+
add Log4r::FileOutputter.new("file",
|
22
|
+
{ :level => @level,
|
23
|
+
:filename => filename,
|
24
|
+
:formatter => fmtr }
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
28
|
+
if config.log_syslog
|
29
|
+
require 'log4r/outputter/syslogoutputter'
|
30
|
+
syslogfmtr = Log4r::PatternFormatter.new(:pattern => "#{listname}\t%M")
|
31
|
+
add Log4r::SyslogOutputter.new("syslog",
|
32
|
+
{ :level => @level,
|
33
|
+
:ident => 'Schleuder',
|
34
|
+
:facility => "LOG_MAIL",
|
35
|
+
:formatter => syslogfmtr }
|
36
|
+
)
|
37
|
+
end
|
38
|
+
|
39
|
+
if config.log_io
|
40
|
+
require 'log4r/outputter/iooutputter'
|
41
|
+
require 'stringio'
|
42
|
+
io = IO.popen(config.log_io, 'w')
|
43
|
+
add Log4r::IOOutputter.new("io", io, :level => @level)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Add this as last outputter, else you'll see logging-statements from
|
47
|
+
# sending the email before the original error is logged — that's a little
|
48
|
+
# confusing.
|
49
|
+
add EmailOutputter.new("email", :formatter => fmtr)
|
50
|
+
end
|
51
|
+
|
52
|
+
def notify_admin(*args)
|
53
|
+
Log4r::Outputter['email'].notify_admin *args
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
module Schleuder
|
2
|
+
class EmailOutputter < Log4r::EmailOutputter
|
3
|
+
def initialize(name, hash={})
|
4
|
+
# The class needs to/from/subject, we don't use it
|
5
|
+
hash = {:to => Schleuder.config.superadminaddr,
|
6
|
+
:from => Schleuder.config.myaddr,
|
7
|
+
:'error-to' => Schleuder.config.superadminaddr,
|
8
|
+
:subject => 'Error',
|
9
|
+
:immediate_at => 'ERROR, FATAL',
|
10
|
+
:formatter => formatter,
|
11
|
+
:domain => 'schleuder', # necessary for log4r from debian "stable"
|
12
|
+
:buffsize => 1024**1024 # set the buff size very high otherwise we would trigger random log mails on random log statements if the buffer is full.
|
13
|
+
}.merge(hash)
|
14
|
+
@altmsg = "Hello,\n\nsending an encrypted error message to you failed. Therefore you receive only\nthis message and are kindly requested to take care of the encryption problem\n(e.g. fix your keys) and have a look at the logs to find the error.\n\nYours, Schleuder\n"
|
15
|
+
super name, hash
|
16
|
+
end
|
17
|
+
|
18
|
+
def format(events)
|
19
|
+
events.map { |event| @formatter.format(event) }
|
20
|
+
end
|
21
|
+
|
22
|
+
def notify_admin(subject, msg)
|
23
|
+
send_mail(makeemail(subject, msg))
|
24
|
+
end
|
25
|
+
|
26
|
+
def makeemail(subject=@subject, msg=nil)
|
27
|
+
m = Mail.new
|
28
|
+
m.subject = subject
|
29
|
+
m.date = Time.now
|
30
|
+
m.from = @from
|
31
|
+
|
32
|
+
if msg
|
33
|
+
case msg
|
34
|
+
when String
|
35
|
+
m.body = msg
|
36
|
+
when Array
|
37
|
+
m.body = ''
|
38
|
+
m.mime_version = 1.0
|
39
|
+
msg.each do |msgpart|
|
40
|
+
part = Mail.new
|
41
|
+
case msgpart
|
42
|
+
when Mail
|
43
|
+
# This contruction is needed to have valid boundaries. Did I mention that Tmail sucks?
|
44
|
+
part.body = Mail.parse(msgpart.to_s).to_s
|
45
|
+
part.content_type = 'message/rfc822'
|
46
|
+
when String
|
47
|
+
part.body = msgpart.to_s
|
48
|
+
part.content_type = 'text/plain'
|
49
|
+
else
|
50
|
+
# This shouldn't happen (we should only have forwarded emails or
|
51
|
+
# strings to notify admins), but who knows.
|
52
|
+
part.body = msgpart
|
53
|
+
part.content_type = 'application/octet-stream'
|
54
|
+
end
|
55
|
+
m.parts.push part
|
56
|
+
end
|
57
|
+
end
|
58
|
+
else
|
59
|
+
# Get the triggering error-msg(s).
|
60
|
+
msg = format(@buff.select { |e| e.level > Log4r::WARN }).join
|
61
|
+
# Get the whole log.
|
62
|
+
backlog = format(@buff).join
|
63
|
+
|
64
|
+
infopart = Mail.new
|
65
|
+
infopart.body = "An error occurred working for list #{Schleuder.list.listname}:\n\n#{msg}\n\nSee also attachments.\n\n"
|
66
|
+
m.parts.push infopart
|
67
|
+
|
68
|
+
if Schleuder.origmsg
|
69
|
+
origm = Mail.new
|
70
|
+
origm.body = Schleuder.origmsg << "\n"
|
71
|
+
origm.content_type = "message/rfc822"
|
72
|
+
origm.set_content_disposition 'inline', { :filename => 'schleuder-orig-message.txt' }
|
73
|
+
origm['content-description'] = "'The originally incoming message'"
|
74
|
+
m.parts.push origm
|
75
|
+
end
|
76
|
+
|
77
|
+
backlogpart = Mail.new
|
78
|
+
backlogpart.body = backlog << "\n\n"
|
79
|
+
backlogpart['content-description'] = "'Schleuder logging output'"
|
80
|
+
backlogpart.set_content_disposition 'inline', { :filename => 'schleuder-log.txt' }
|
81
|
+
m.parts.push backlogpart
|
82
|
+
end
|
83
|
+
|
84
|
+
# Trigger re-parsing of the body, else TMail doesn't know it's multipart... :/
|
85
|
+
m.to_s
|
86
|
+
m
|
87
|
+
end
|
88
|
+
|
89
|
+
def send_mail(mail=makeemail)
|
90
|
+
if @send_mail_lock
|
91
|
+
self.level = Log4r::OFF # it's possible that the loglevel haven't yet been changed
|
92
|
+
Schleuder.log.warn "This is a loop in sending a mail in the EmailOutputer, breaking!"
|
93
|
+
return false
|
94
|
+
end
|
95
|
+
@send_mail_lock = true
|
96
|
+
Schleuder.log.info 'Sending notification to admin'
|
97
|
+
Schleuder.list.config.admins.each do |admin|
|
98
|
+
Schleuder.log.debug { "Looping for admin #{admin}" }
|
99
|
+
m = mail.individualize(admin)
|
100
|
+
m.to = admin.email
|
101
|
+
sender = Schleuder.config.superadminaddr
|
102
|
+
if !Processor.send(m, admin, true, sender)
|
103
|
+
m.body = @altmsg
|
104
|
+
m.content_type = 'text/plain'
|
105
|
+
Processor.send(m, admin, false, sender)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
Schleuder.log.info { 'Sending notification done' }
|
109
|
+
rescue => e
|
110
|
+
# switch off logging per email, else we create loops here!
|
111
|
+
self.level = Log4r::OFF
|
112
|
+
raise
|
113
|
+
ensure
|
114
|
+
@send_mail_lock = false
|
115
|
+
@buff.clear
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Schleuder
|
2
|
+
class MetaEmailOutputter < EmailOutputter
|
3
|
+
# TODO: refactor (merge?) with EmailOutputter
|
4
|
+
def send_mail
|
5
|
+
if @send_mail_lock
|
6
|
+
self.level = Log4r::OFF # it's possible that the loglevel haven't yet been changed
|
7
|
+
Schleuder.log.warn 'This is a loop in sending a mail in the MetaEmailOutputter, breaking!'
|
8
|
+
return false
|
9
|
+
end
|
10
|
+
@send_mail_lock = true
|
11
|
+
Schleuder.log.info { 'notifying superadmin' }
|
12
|
+
begin
|
13
|
+
mail = makeemail
|
14
|
+
mail.to = Schleuder.config.superadminaddr
|
15
|
+
r = Member.new('email' => Schleuder.config.superadminaddr)
|
16
|
+
m = mail.individualize(r)
|
17
|
+
if ! Processor.send(m, r, true, Schleuder.config.superadminaddr).first
|
18
|
+
m.body = @altmsg
|
19
|
+
m.content_type = 'text/plain'
|
20
|
+
Processor.send(m, r, false, Schleuder.config.superadminaddr)
|
21
|
+
end
|
22
|
+
rescue => e
|
23
|
+
Schleuder.log.warn "An error occurred while reporting an error: #{e.message}\n#{e.backtrace[0..3].join("\n")}"
|
24
|
+
Schleuder.log.warn "Sending emergency notice to superadmin"
|
25
|
+
mail = "From: root@localhost
|
26
|
+
To: #{Schleuder.config.superadminaddr}
|
27
|
+
Date: #{Time.now.strftime('%a, %d %b %Y %H:%M:%S %z')}
|
28
|
+
Subject: Error
|
29
|
+
|
30
|
+
Serious errors happened working for list #{Schleuder.listname}.
|
31
|
+
Please check the logs!
|
32
|
+
"
|
33
|
+
Mailer.send mail, Schleuder.config.superadminaddr, 'root@localhost', true
|
34
|
+
end
|
35
|
+
Schleuder.log.info { 'Notifying done' }
|
36
|
+
rescue => e
|
37
|
+
# switch off logging per email, else we create loops here!
|
38
|
+
self.level = Log4r::OFF
|
39
|
+
if e.message.frozen?
|
40
|
+
Schleuder.log.warn "A problematic error happened. I'll better dump the original message to preserve it:\n\n#{Schleuder.origmsg}"
|
41
|
+
else
|
42
|
+
e.message << "\nThis is very problematic, I'll better dump the original message to preserve it:\n\n#{Schleuder.origmsg}"
|
43
|
+
end
|
44
|
+
Schleuder.log.fatal { e }
|
45
|
+
ensure
|
46
|
+
@send_mail_lock = false
|
47
|
+
@buff.clear
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Schleuder
|
2
|
+
class SchleuderLogger < Log4r::Logger
|
3
|
+
# Instantiates a logger to be used outside of list-contexts and sets
|
4
|
+
# outputters and level.
|
5
|
+
def initialize
|
6
|
+
# The name 'log4r' is special: it is considered as meta-logger by Log4r
|
7
|
+
# and receives log_internal()-messages.
|
8
|
+
super 'log4r'
|
9
|
+
# The initial log_level is inherited by all outputters.
|
10
|
+
@level = eval("Log4r::#{Schleuder.config.log_level.upcase}")
|
11
|
+
pattern = "%d Schleuder %l\t%M"
|
12
|
+
formatter = Log4r::PatternFormatter.new(:pattern => pattern)
|
13
|
+
add Log4r::FileOutputter.new('file',
|
14
|
+
{ :level => @level,
|
15
|
+
:filename => Schleuder.config.log_file,
|
16
|
+
:formatter => formatter }
|
17
|
+
)
|
18
|
+
add MetaEmailOutputter.new('email',
|
19
|
+
{:to => Schleuder.config.superadminaddr,
|
20
|
+
:immediate_at => 'ERROR, FATAL'})
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,861 @@
|
|
1
|
+
module Schleuder
|
2
|
+
class Mail < TMail::Mail
|
3
|
+
|
4
|
+
# Schleuder::Member's the mail shall be sent to
|
5
|
+
attr_accessor :recipients
|
6
|
+
|
7
|
+
# Additional data that is to be stored into internally sent
|
8
|
+
# mails. Must be a Hash.
|
9
|
+
attr_accessor :metadata
|
10
|
+
|
11
|
+
# Indicator if the incoming mail was encrypted
|
12
|
+
attr_accessor :in_encrypted
|
13
|
+
|
14
|
+
# Indicator if (and by whom) the incoming mail was signed. Is +false+ or a
|
15
|
+
# GPGME::Signature.
|
16
|
+
attr_accessor :in_signed
|
17
|
+
|
18
|
+
attr_accessor :resend_to
|
19
|
+
|
20
|
+
# The incoming mail in raw form (string). Needed to verify detached
|
21
|
+
# signatures (see +decrypt!+)
|
22
|
+
attr_accessor :original_message
|
23
|
+
|
24
|
+
# Shall we consider this email to be sent by the list-admin?
|
25
|
+
attr_reader :from_admin
|
26
|
+
|
27
|
+
# Shall we consider this email to be sent by a list-member?
|
28
|
+
attr_reader :from_member
|
29
|
+
|
30
|
+
# internal message_id we'll use for all outgoing mails
|
31
|
+
@@schleuder_message_id = nil
|
32
|
+
|
33
|
+
def initialize(*data)
|
34
|
+
super(*data)
|
35
|
+
@resend_to = []
|
36
|
+
@metadata = {:resent_to => [], :notice => [], :warning => [], :error => []}
|
37
|
+
end
|
38
|
+
|
39
|
+
# Overwrites TMail.parse to store the original incoming data. We possibly
|
40
|
+
# need that to verify pgp-signatures (TMail changes headers which
|
41
|
+
# invalidates the signature)
|
42
|
+
def self.parse(string)
|
43
|
+
foo = super(string)
|
44
|
+
foo.original_message = string
|
45
|
+
foo
|
46
|
+
end
|
47
|
+
|
48
|
+
# Desctructivly decrypt/verify +self+.
|
49
|
+
def decrypt!
|
50
|
+
# Note: We don't recurse into nested Mime-parts. Only the first level
|
51
|
+
# will be touched
|
52
|
+
if self.multipart? && !self.type_param('protocol').nil? && self.type_param('protocol').match(/^application\/pgp.*/)
|
53
|
+
Schleuder.log.debug 'mail is pgp/mime-formatted'
|
54
|
+
if self.sub_type == 'signed'
|
55
|
+
Schleuder.log.debug 'mail is signed but not encrypted'
|
56
|
+
Schleuder.log.debug 'Parsing original_message to split content from signature'
|
57
|
+
|
58
|
+
# first, identify the boundary
|
59
|
+
bm = self.original_message.match(/boundary="?'?([^"';]*)/)
|
60
|
+
boundary = "--" + Regexp.escape(bm[1])
|
61
|
+
Schleuder.log.debug "Identified boundary: #{boundary}"
|
62
|
+
# next, find the signed string between the first and the second-last boundary
|
63
|
+
signed_string = self.original_message.match(/.*#{boundary}\r?\n(.*?)\r?\n#{boundary}.*?#{boundary}.*?/m)[1]
|
64
|
+
# Add CRs, which probably have been stripped by the MTA
|
65
|
+
signed_string.gsub!(/\n/, "\r\n") unless signed_string.include?("\r")
|
66
|
+
Schleuder.log.debug "Identified signed string"
|
67
|
+
# third, find the pgp-signature
|
68
|
+
signature = self.original_message.match(/.*(-----BEGIN PGP SIGNATURE-----.*?-----END PGP SIGNATURE-----).*/m)[1] rescue ''
|
69
|
+
Schleuder.log.debug "Identified signature"
|
70
|
+
# finally, verify
|
71
|
+
Schleuder.log.info "Verifying"
|
72
|
+
foo, self.in_signed = self.crypt.verify(signature, signed_string)
|
73
|
+
plaintext = self.parts[0].to_s
|
74
|
+
elsif self.sub_type == 'encrypted'
|
75
|
+
Schleuder.log.debug 'mail is encrypted (and possibly signed)'
|
76
|
+
plaintext, self.in_encrypted, self.in_signed = self.crypt.decrypt(self.parts[1].body)
|
77
|
+
else
|
78
|
+
Schleuder.log.warn "Strange: message claims to be pgp/mime but neither to be signed nor encrypted"
|
79
|
+
end
|
80
|
+
# replace encrypted content with cleartext content
|
81
|
+
plainmsg = Mail.parse(plaintext)
|
82
|
+
#Schleuder.log.debug "plainmsg: #{plainmsg.to_s.inspect}"
|
83
|
+
# test for signed content within the previously encrypted content
|
84
|
+
if plainmsg._content_type_stripped == 'multipart/signed'
|
85
|
+
Schleuder.log.debug "Found signed message as cleartext, recurseing once"
|
86
|
+
plainmsg.original_message = plaintext
|
87
|
+
plainmsg.decrypt!
|
88
|
+
Schleuder.log.debug "End of recursion"
|
89
|
+
self.in_signed = plainmsg.in_signed
|
90
|
+
end
|
91
|
+
# get headers and body(parts) into self
|
92
|
+
if plainmsg.multipart?
|
93
|
+
Schleuder.log.debug "plainmsg.multipart? => true"
|
94
|
+
self.parts.clear
|
95
|
+
plainmsg.parts.each do |p|
|
96
|
+
self.parts.push(Mail.parse(p.to_s))
|
97
|
+
end
|
98
|
+
else
|
99
|
+
Schleuder.log.debug "plainmsg.multipart? => false"
|
100
|
+
self.body = plainmsg.body
|
101
|
+
end
|
102
|
+
self.content_type = plainmsg._content_type
|
103
|
+
self.disposition = plainmsg._disposition
|
104
|
+
else
|
105
|
+
Schleuder.log.debug 'no pgp-mime found, looking for pgp-inline'
|
106
|
+
# Do we simply push everything to crypt as that should return plain
|
107
|
+
# text if it is feeded plain text? Or does crypt return only an error
|
108
|
+
# if there is nothing to do for it?
|
109
|
+
if self.multipart?
|
110
|
+
Schleuder.log.debug 'multipart found, looking for pgp content in each part'
|
111
|
+
self.parts.each do |part|
|
112
|
+
_decrypt_pgp_inline(part)
|
113
|
+
end
|
114
|
+
else
|
115
|
+
Schleuder.log.debug 'single inline content found, looking for pgp content'
|
116
|
+
_decrypt_pgp_inline(self)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
_parse_signature
|
120
|
+
end
|
121
|
+
|
122
|
+
# Destructivly encrypt and sign. +receiver+ must be a string or a Schleuder::Member
|
123
|
+
def encrypt!(receiver)
|
124
|
+
if receiver.kind_of?(String)
|
125
|
+
receiver = Member.new({'email' => receiver})
|
126
|
+
elsif ! receiver.kind_of?(Member)
|
127
|
+
raise "need a Member-object as argument, got '#{receiver.inspect}'"
|
128
|
+
end
|
129
|
+
|
130
|
+
Schleuder.log.info "Encrypting for #{receiver.inspect}"
|
131
|
+
|
132
|
+
key, msg = receiver.key
|
133
|
+
if key == false
|
134
|
+
Schleuder.log.warn msg.capitalize
|
135
|
+
return [false, msg]
|
136
|
+
elsif key.nil?
|
137
|
+
Schleuder.log.warn "No public key found for '#{keystring}'"
|
138
|
+
return [false, 'no public key found']
|
139
|
+
end
|
140
|
+
|
141
|
+
# choose pgp-variant from addresses value oder config.default_mime
|
142
|
+
pgpvariant = receiver.mime || Schleuder.list.config.default_mime
|
143
|
+
|
144
|
+
case pgpvariant.upcase
|
145
|
+
when 'PLAIN'
|
146
|
+
Schleuder.log.debug "encrypting as text/plain"
|
147
|
+
if self.multipart?
|
148
|
+
self.parts.each_with_index do |p,i|
|
149
|
+
self.parts[i] = _encrypt_pgp_inline(p, receiver)
|
150
|
+
end
|
151
|
+
self.content_type = 'multipart/mixed'
|
152
|
+
self.encoding = nil
|
153
|
+
else
|
154
|
+
foo = _encrypt_pgp_inline(self, receiver)
|
155
|
+
self.body = foo.body
|
156
|
+
foo.header.each do |k,v|
|
157
|
+
self[k] = v.to_s rescue nil
|
158
|
+
end
|
159
|
+
end
|
160
|
+
when 'MIME'
|
161
|
+
Schleuder.log.debug "encrypting pgp-mime"
|
162
|
+
gpgcontent = Mail.new
|
163
|
+
if self.multipart?
|
164
|
+
self.parts.each do |p|
|
165
|
+
gpgcontent.parts.push(p)
|
166
|
+
end
|
167
|
+
else
|
168
|
+
gpgcontent.body = self.body
|
169
|
+
gpgcontent.content_type = self._content_type
|
170
|
+
gpgcontent.charset = self.charset || 'UTF-8'
|
171
|
+
end
|
172
|
+
|
173
|
+
gpgcontainer = _new_part(crypt.encrypt_str(gpgcontent.to_s, receiver), 'application/octet-stream', self.charset)
|
174
|
+
gpgcontainer.set_disposition('inline', {:filename => 'message.asc'})
|
175
|
+
|
176
|
+
mimecontainer = _new_part("Version: 1\n", 'application/pgp-encrypted')
|
177
|
+
mimecontainer.disposition = 'attachment'
|
178
|
+
|
179
|
+
self.encoding = nil
|
180
|
+
self.body = ""
|
181
|
+
self.parts.clear
|
182
|
+
self.parts.push(mimecontainer)
|
183
|
+
self.parts.push(gpgcontainer)
|
184
|
+
self.set_content_type('multipart', 'encrypted', {:protocol => 'application/pgp-encrypted'})
|
185
|
+
self.mime_version = 1.0
|
186
|
+
|
187
|
+
when 'APPL'
|
188
|
+
Schleuder.log.error "mime-setting 'APPL' is deprecated, use 'MIME' instead for user '#{receiver.email}'"
|
189
|
+
else
|
190
|
+
Schleuder.log.error "Unknown mime-setting for user '#{receiver.email}': #{pgpvariant}"
|
191
|
+
end
|
192
|
+
return true
|
193
|
+
end
|
194
|
+
|
195
|
+
# Clearsign the message. Returns a string (raw msg, which must not be
|
196
|
+
# changed anymore) or a Schleuder::Mail.
|
197
|
+
def sign
|
198
|
+
Schleuder.log.debug "signing message"
|
199
|
+
if (self.to || []).length != 1
|
200
|
+
Schleuder.log.error "I need exactly one recipient in To-header. Found this: #{self.to.inspect}"
|
201
|
+
return false
|
202
|
+
end
|
203
|
+
member = Schleuder.list.find_member_by_email(self.to.to_s)
|
204
|
+
# ugly but necessary: member might be false and member.mime might be nil
|
205
|
+
pgpvariant = member.mime || Schleuder.list.config.default_mime rescue Schleuder.list.config.default_mime
|
206
|
+
case pgpvariant.upcase
|
207
|
+
when 'MIME'
|
208
|
+
Schleuder.log.debug "signing MIME"
|
209
|
+
gpgcontainer = Mail.new
|
210
|
+
if self.multipart?
|
211
|
+
self.parts.each do |p|
|
212
|
+
if p.main_type.eql?('text')
|
213
|
+
# Encode QP (there might be trailing white space
|
214
|
+
p.body = p.body.to_s.split("\n").map do |line|
|
215
|
+
[line].pack("M")
|
216
|
+
end.join("\n")
|
217
|
+
p.encoding = 'Quoted-Printable'
|
218
|
+
end
|
219
|
+
gpgcontainer.parts.push(p)
|
220
|
+
end
|
221
|
+
self.parts.clear
|
222
|
+
else
|
223
|
+
# Encode QP (there might be trailing white space which is illegal
|
224
|
+
# according to RFC 3156).
|
225
|
+
# Don't call body() as that returns the decoded string
|
226
|
+
gpgcontainer.body = body.to_s.split("\n").map do |line|
|
227
|
+
[line].pack("M")
|
228
|
+
end.join("\n")
|
229
|
+
gpgcontainer.encoding = 'Quoted-Printable'
|
230
|
+
gpgcontainer.content_type = self._content_type
|
231
|
+
gpgcontainer.disposition = self._disposition
|
232
|
+
self.body = ''
|
233
|
+
end
|
234
|
+
self.encoding = ''
|
235
|
+
self.parts.push gpgcontainer
|
236
|
+
|
237
|
+
# This following part is quite complicated. We have to do it this way
|
238
|
+
# because TMail changes the mime-boundary on every encoded() (implicit
|
239
|
+
# in to_s()), which invalidates the signature
|
240
|
+
|
241
|
+
# TODO: refactor with encrypt!()
|
242
|
+
|
243
|
+
# First we create the signature-attachment and fill it with a dummy
|
244
|
+
dummy = "dummytobereplaced-#{Time.now.to_f}"
|
245
|
+
sigpart = _new_part(dummy, 'application/pgp-signature')
|
246
|
+
self.parts.push(sigpart)
|
247
|
+
|
248
|
+
# TODO: take care of micalg: the digest used for hashing the plaintext.
|
249
|
+
# RFC 3156 requires it to be set in the content-type. ruby-gpgme
|
250
|
+
# doesn't provide it to us, though.
|
251
|
+
# (Don't use symbols with TMail, it expects strings.)
|
252
|
+
self.set_content_type('multipart', 'signed', {'protocol' => 'application/pgp-signature', 'micalg' => 'pgp-sha1'})
|
253
|
+
|
254
|
+
# Then we dump the crafted msg
|
255
|
+
rawmsg = self.encoded
|
256
|
+
|
257
|
+
# get mime boundary from raw mail
|
258
|
+
bm = rawmsg.match(/boundary="?'?([^"';\r\n]*)/)
|
259
|
+
boundary = "--" + Regexp.escape(bm[1])
|
260
|
+
|
261
|
+
# Now get the to be signed string from the raw message
|
262
|
+
parts = rawmsg.match(/(.*#{boundary}\r?\n)(.*\r?\n)(\r?\n#{boundary}.*?#{boundary}.*)/m)
|
263
|
+
|
264
|
+
if parts
|
265
|
+
# For some reason I don't manage to do this in one gsub-call... *snif*
|
266
|
+
tobesigned_crlf = parts[2].gsub(/\n/, "\r\n").gsub(/\r\r/, "\r")
|
267
|
+
rawmsg = "#{parts[1]}#{tobesigned_crlf}#{parts[3]}"
|
268
|
+
|
269
|
+
# sign the extracted part
|
270
|
+
sig = self.crypt.sign(tobesigned_crlf)
|
271
|
+
|
272
|
+
# replace dummy with signature
|
273
|
+
rawmsg.gsub!(/(.*)#{dummy}(.*?#{boundary}.*?)/m, "\\1#{sig}\\2")
|
274
|
+
rawmsg
|
275
|
+
else
|
276
|
+
Schleuder.log.error "Detection of mime parts in mail for #{self.to} with boundary #{bm} failed. This should not happen. -> Cannot sign outgoing mail."
|
277
|
+
|
278
|
+
false
|
279
|
+
end
|
280
|
+
when 'PLAIN'
|
281
|
+
Schleuder.log.debug "signing PLAIN"
|
282
|
+
if self.multipart?
|
283
|
+
self.parts.each_with_index do |p,i|
|
284
|
+
if p.disposition == 'attachment' || !p.disposition_param('filename').nil?
|
285
|
+
Schleuder.log.debug "found attachment, creating detached signature"
|
286
|
+
# attachment, need detached sig
|
287
|
+
container = Mail.new
|
288
|
+
container.parts.push Mail.parse(p.to_s)
|
289
|
+
|
290
|
+
sig = crypt.sign(p.body)
|
291
|
+
Schleuder.log.debug "sig: #{sig.inspect}"
|
292
|
+
sigpart = _new_part(sig, 'text/plain')
|
293
|
+
sigpart.set_disposition('attachment', {:filename => p.disposition_param('filename') + '.asc'})
|
294
|
+
container.parts.push Mail.parse(sigpart.to_s)
|
295
|
+
|
296
|
+
container.content_type = 'multipart/mixed'
|
297
|
+
|
298
|
+
self.parts[i] = Mail.parse(container.to_s)
|
299
|
+
else
|
300
|
+
p.body = crypt.clearsign(p.body)
|
301
|
+
end
|
302
|
+
end
|
303
|
+
else
|
304
|
+
self.body = crypt.clearsign(self.body)
|
305
|
+
end
|
306
|
+
self
|
307
|
+
|
308
|
+
else
|
309
|
+
Schleuder.log.error "Strange mime-setting: #{pgpvariant}. Don't know what to do with that, ignoring it."
|
310
|
+
self
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
# Create a new Schleuder::Mail-instance and copy/set selected headers.
|
315
|
+
def individualize(recv)
|
316
|
+
Schleuder.log.debug { "Individualizing mail for #{recv.inspect}" }
|
317
|
+
new = Mail.new
|
318
|
+
new.crypt = crypt
|
319
|
+
# add some headers we want to keep
|
320
|
+
new.content_type = self['content-type'].to_s if self['content-type']
|
321
|
+
new.disposition = self._disposition
|
322
|
+
new.encoding = self.encoding.to_s
|
323
|
+
new.subject = _quote_if_necessary(self.subject.to_s,'UTF-8')
|
324
|
+
|
325
|
+
new.message_id = Mail._schleuder_message_id
|
326
|
+
|
327
|
+
new.to = recv.email.to_s
|
328
|
+
new.date = Time.now
|
329
|
+
new.from = Schleuder.list.config.myaddr
|
330
|
+
new = _add_openpgp_header(new) if Schleuder.list.config.include_openpgp_header
|
331
|
+
if self.multipart?
|
332
|
+
self.parts.each do |p|
|
333
|
+
new.parts.push(Mail.parse(p.to_s))
|
334
|
+
end
|
335
|
+
else
|
336
|
+
new.body = self.body
|
337
|
+
end
|
338
|
+
new
|
339
|
+
end
|
340
|
+
|
341
|
+
def individualize_member(recv)
|
342
|
+
new = self.individualize(recv)
|
343
|
+
_message_ids(new) if Schleuder.list.config.keep_msgid
|
344
|
+
_list_headers(new) if Schleuder.list.config.include_list_headers
|
345
|
+
end
|
346
|
+
|
347
|
+
# Strips keywords from self and stores them in @+keywords+
|
348
|
+
def keywords
|
349
|
+
unless @keywords
|
350
|
+
@keywords = []
|
351
|
+
# we're looking for keywords only in the first mime-part
|
352
|
+
b = self
|
353
|
+
while b.multipart?
|
354
|
+
b = b.parts.first
|
355
|
+
end
|
356
|
+
# split to array to ease parsing
|
357
|
+
a = b.body.split("\n")
|
358
|
+
|
359
|
+
a.map! do |line|
|
360
|
+
if line.match(/^X-.*/)
|
361
|
+
Schleuder.log.debug "found keyword-line: #{line.chomp}"
|
362
|
+
key, val = $&.split(/:|: | /, 2)
|
363
|
+
keyword = key.slice(2..-1).strip
|
364
|
+
if !self.from_admin && Schleuder.list.config.keywords_admin_only.include?(keyword)
|
365
|
+
Schleuder.log.info "Keyword '#{keyword}' is listed as admin only and mail is not from admin, skipping"
|
366
|
+
self.metadata[:error] << "Keyword '#{keyword}' is configured as admin-only."
|
367
|
+
# return the line to have it stay in the body
|
368
|
+
line
|
369
|
+
else
|
370
|
+
# Split values to catch multiple values separated by comma or the like (deprecated).
|
371
|
+
values = val.to_s.strip.split(/[ ,;]+/)
|
372
|
+
values << ' ' if values.empty?
|
373
|
+
values.each do |v|
|
374
|
+
Schleuder.log.info "Storing keyword #{keyword} with value #{v.inspect}"
|
375
|
+
@keywords << {keyword => v.to_s.strip}
|
376
|
+
end
|
377
|
+
nil
|
378
|
+
end
|
379
|
+
elsif line.chomp.empty?
|
380
|
+
line
|
381
|
+
else
|
382
|
+
# break on the first non-empty and non-command line so we don't parse
|
383
|
+
# the whole message (could be much data)
|
384
|
+
break
|
385
|
+
end
|
386
|
+
end
|
387
|
+
# delete nil's (lines formerly containing X-Commands) from array
|
388
|
+
a.compact!
|
389
|
+
# rejoin to string
|
390
|
+
b.body = a.join("\n")
|
391
|
+
# The whole procedure makes TMail parse and reformat the message, so now
|
392
|
+
# it's decoded utf-8
|
393
|
+
b.charset = 'UTF-8'
|
394
|
+
b.encoding = nil
|
395
|
+
Schleuder.log.debug "Inspecting found keywords: #{@keywords.inspect}"
|
396
|
+
end
|
397
|
+
@keywords
|
398
|
+
end
|
399
|
+
|
400
|
+
def request?
|
401
|
+
@request ||= self.to.to_a.include?(Schleuder.list.request_addr)
|
402
|
+
end
|
403
|
+
|
404
|
+
def bounce?
|
405
|
+
self.to.to_a.include?(Schleuder.list.bounce_addr) || \
|
406
|
+
( # empty Return-Path
|
407
|
+
['<>'].include?(self['Return-path'].to_s) || \
|
408
|
+
( # Auto-Submitted exists and does not equal 'no'
|
409
|
+
! ["no", ""].include?(self['Auto-Submitted'].to_s) && \
|
410
|
+
self['X-Cron-Env'].nil? # cron mails are also autosubmitted
|
411
|
+
)
|
412
|
+
)
|
413
|
+
end
|
414
|
+
|
415
|
+
# +require+s all found plugins, tests SomePlugin.match and if that returns
|
416
|
+
# true runs SomePlugin.process
|
417
|
+
def process_plugins!
|
418
|
+
if self.keywords.empty?
|
419
|
+
Schleuder.log.info 'No keywords present, skipping plugins'
|
420
|
+
elsif File.directory? Schleuder.config.plugins_dir
|
421
|
+
replymsg = ""
|
422
|
+
# We might want to replace the object later, which we can't do with self.
|
423
|
+
mail = self
|
424
|
+
plugins = {:request => [], :list => []}
|
425
|
+
ptype = (self.request? ? :request : :list)
|
426
|
+
used_keywords = []
|
427
|
+
Plugin.signing_key(self)
|
428
|
+
Dir[Schleuder.config.plugins_dir + '/*_plugin.rb'].each do |plugfile|
|
429
|
+
Schleuder.log.debug "Instanciating #{plugfile} as plugin"
|
430
|
+
require plugfile
|
431
|
+
# interpreting class name from file name
|
432
|
+
classname = File.basename(plugfile, '.rb').split('_').collect { |p| p.capitalize }.join
|
433
|
+
plugin = instance_eval(classname).new
|
434
|
+
Schleuder.log.debug "Storing plugin in plugin-list '#{plugin.plugin_type}'"
|
435
|
+
plugins[plugin.plugin_type] << plugin
|
436
|
+
end
|
437
|
+
# This is neither elegant nor fast, but it's got to live only until the rewrite.
|
438
|
+
Schleuder.log.debug "Processing #{ptype}-plugins"
|
439
|
+
self.keywords.each do |kwhash|
|
440
|
+
keyword = kwhash.keys.first
|
441
|
+
value = kwhash.values.first
|
442
|
+
Schleuder.log.debug "Looping for keyword #{keyword}"
|
443
|
+
plugins[ptype].each do |plugin|
|
444
|
+
command = keyword.downcase.gsub(/-/, '_')
|
445
|
+
Schleuder.log.debug "Does #{plugin.class}.respond_to? '#{command}'"
|
446
|
+
if plugin.respond_to?(command)
|
447
|
+
Schleuder.log.debug "Yes it does, executing #{plugin.class}.#{command}"
|
448
|
+
used_keywords << keyword
|
449
|
+
foo = plugin.send(command, mail, value)
|
450
|
+
case foo
|
451
|
+
when String
|
452
|
+
Schleuder.log.debug "Method returned a string, saving it for reply to sender."
|
453
|
+
replymsg << foo << "\n\n\n"
|
454
|
+
when Mail
|
455
|
+
Schleuder.log.debug "Method returned a Mail-object, replacing myself by it."
|
456
|
+
mail = foo
|
457
|
+
else
|
458
|
+
Schleuder.log.debug "Method returned '#{foo.inspect}', don't know how to handle, skipping."
|
459
|
+
end
|
460
|
+
next
|
461
|
+
else
|
462
|
+
Schleuder.log.debug "No it doesn't."
|
463
|
+
end
|
464
|
+
end
|
465
|
+
# Generate error-message if no plugin was executed
|
466
|
+
unless used_keywords.include?(keyword)
|
467
|
+
msg = "No #{ptype}-plugin responded to keyword #{keyword}"
|
468
|
+
Schleuder.log.debug msg
|
469
|
+
if mail.request?
|
470
|
+
replymsg << "Error: #{msg}.\n\n"
|
471
|
+
else
|
472
|
+
mail.metadata[:error] << msg
|
473
|
+
end
|
474
|
+
end
|
475
|
+
end
|
476
|
+
# Decide how to go on: reply or list?
|
477
|
+
if mail.request?
|
478
|
+
Schleuder.log.debug "Message is a request, sending plugins-output back to sender."
|
479
|
+
if replymsg.empty?
|
480
|
+
replymsg = 'The keywords you sent did not produce any output. If you feel this is an error please contact the adminisrators of this list.'.fmt
|
481
|
+
end
|
482
|
+
# This exits.
|
483
|
+
Plugin.reply(mail, replymsg, used_keywords)
|
484
|
+
end
|
485
|
+
else
|
486
|
+
Schleuder.log.error "#{Schleuder.config.plugins_dir} does not exist or is not readable!"
|
487
|
+
end
|
488
|
+
end
|
489
|
+
|
490
|
+
def add_prefix!
|
491
|
+
# only add if it's not already present
|
492
|
+
unless self.subject.index(Schleuder.list.config.prefix)
|
493
|
+
Schleuder.log.debug "adding prefix"
|
494
|
+
self.subject = Schleuder.list.config.prefix + " " + self.subject.to_s
|
495
|
+
end
|
496
|
+
end
|
497
|
+
|
498
|
+
def add_prefix_in!
|
499
|
+
# only add if it's not already present
|
500
|
+
unless self.subject.index(Schleuder.list.config.prefix_in)
|
501
|
+
Schleuder.log.debug "adding prefix_in"
|
502
|
+
self.subject = Schleuder.list.config.prefix_in + " " + self.subject.to_s
|
503
|
+
end
|
504
|
+
end
|
505
|
+
|
506
|
+
def add_prefix_out!
|
507
|
+
# only add if it's not already present
|
508
|
+
unless self.subject.index(Schleuder.list.config.prefix_out)
|
509
|
+
Schleuder.log.debug "adding prefix_out"
|
510
|
+
self.subject = Schleuder.list.config.prefix_out + " " + self.subject.to_s
|
511
|
+
end
|
512
|
+
end
|
513
|
+
|
514
|
+
def add_metadata!
|
515
|
+
Schleuder.log.info "Adding meta-information on old mail to new mail"
|
516
|
+
Schleuder.log.debug 'Generating meta-information'
|
517
|
+
meta = ''
|
518
|
+
Schleuder.list.config.headers_to_meta.each do |h|
|
519
|
+
h = h.to_s.capitalize
|
520
|
+
val = self.header_string(h) rescue '(not parsable)'
|
521
|
+
next if val.nil?
|
522
|
+
val = TMail::Unquoter.unquote_and_convert_to(val,'utf-8')
|
523
|
+
meta << "#{h}: #{val}\n"
|
524
|
+
end
|
525
|
+
meta << "Enc: #{_enc_str @in_encrypted}\n"
|
526
|
+
meta << "Sig: #{_sig_str @in_signed}\n"
|
527
|
+
if self.multipart?
|
528
|
+
self.parts.each_with_index do |p,i|
|
529
|
+
unless p.in_encrypted.nil? && p.in_signed.nil?
|
530
|
+
meta << "part #{i+1}:\n"
|
531
|
+
meta << " enc: #{_enc_str p.in_encrypted}\n"
|
532
|
+
meta << " sig: #{_sig_str p.in_signed}\n"
|
533
|
+
end
|
534
|
+
end
|
535
|
+
end
|
536
|
+
|
537
|
+
Schleuder.log.debug 'Adding extra meta data'
|
538
|
+
@metadata.each do |name, content|
|
539
|
+
meta << "#{name.to_s.gsub(/_/, '-').capitalize}: #{(content.join('; ') || content.to_s)}\n" unless content.empty?
|
540
|
+
end
|
541
|
+
meta << "\n"
|
542
|
+
|
543
|
+
# insert oder prepend to the message
|
544
|
+
if self.first_part._content_type_stripped == 'text/plain'
|
545
|
+
Schleuder.log.debug "Glueing meta data into first part of message"
|
546
|
+
self.first_part.body = meta + self.first_part.body
|
547
|
+
# body is now utf-8 and decoded!
|
548
|
+
self.first_part.charset = 'UTF-8'
|
549
|
+
self.first_part.encoding = ''
|
550
|
+
else
|
551
|
+
# make the message multipart and prepend the meta-part
|
552
|
+
self.to_multipart! unless self.multipart?
|
553
|
+
Schleuder.log.debug "Prepending meta data as own mime part to message"
|
554
|
+
self.parts.unshift _new_part(meta, 'text/plain', 'UTF-8')
|
555
|
+
end
|
556
|
+
|
557
|
+
end
|
558
|
+
|
559
|
+
# Adds Schleuder::ListConfig.public_footer to the end of the body of self
|
560
|
+
# or the body of the first mimepart (if one of those is text/plain) or
|
561
|
+
# appends it as a new mimepart.
|
562
|
+
def add_public_footer!
|
563
|
+
if Schleuder.list.config.public_footer.strip.empty?
|
564
|
+
return false
|
565
|
+
end
|
566
|
+
Schleuder.log.debug "appending public footer"
|
567
|
+
footer = "\n\n-- \n#{Schleuder.list.config.public_footer}\n"
|
568
|
+
|
569
|
+
if self.first_part._content_type_stripped == 'text/plain'
|
570
|
+
self.first_part.body = self.first_part.body.to_s + footer
|
571
|
+
else
|
572
|
+
self.to_multipart! unless self.multipart?
|
573
|
+
self.parts.push _new_part(footer, 'text/plain', 'UTF-8')
|
574
|
+
end
|
575
|
+
end
|
576
|
+
|
577
|
+
|
578
|
+
def to_multipart!
|
579
|
+
# skip if already multipart
|
580
|
+
return false if self.multipart?
|
581
|
+
Schleuder.log.debug "Making message multipart"
|
582
|
+
# else move the body into a mime-part
|
583
|
+
p = _new_part(self.body, self['content-type'].to_s, self.charset, self.encoding)
|
584
|
+
p.disposition = self._disposition
|
585
|
+
self.parts.push p
|
586
|
+
self.body = ''
|
587
|
+
self.set_content_type 'multipart', 'mixed', {:boundary => TMail.new_boundary}
|
588
|
+
self.disposition = ''
|
589
|
+
end
|
590
|
+
|
591
|
+
def first_part
|
592
|
+
if self.multipart?
|
593
|
+
self.parts.first
|
594
|
+
else
|
595
|
+
self
|
596
|
+
end
|
597
|
+
end
|
598
|
+
|
599
|
+
def _content_type(default = 'text/plain')
|
600
|
+
(self['content-type'] || default).to_s rescue default
|
601
|
+
end
|
602
|
+
|
603
|
+
def _content_type_stripped(default = nil)
|
604
|
+
( default && _content_type(default) || _content_type() ).to_s.split(';').first
|
605
|
+
end
|
606
|
+
|
607
|
+
def _disposition
|
608
|
+
(self['content-disposition'] || '').to_s rescue ''
|
609
|
+
end
|
610
|
+
|
611
|
+
def from_member_address?
|
612
|
+
from.all? { |a| Schleuder.list.find_member_by_address(a) }
|
613
|
+
end
|
614
|
+
|
615
|
+
def from_admin_address?
|
616
|
+
from.all? { |a| Schleuder.list.find_admin_by_address(a) }
|
617
|
+
end
|
618
|
+
|
619
|
+
# An instance of Schleuder::Crypt
|
620
|
+
attr_writer :crypt
|
621
|
+
def crypt
|
622
|
+
@crypt ||= Crypt.new(Schleuder.list.config.gpg_password)
|
623
|
+
end
|
624
|
+
|
625
|
+
private
|
626
|
+
|
627
|
+
def _decrypt_pgp_inline(msg)
|
628
|
+
if msg._content_type_stripped == 'text/html'
|
629
|
+
Schleuder.log.debug "Content-type is text/html, can't handle that, skipping"
|
630
|
+
elsif msg.body =~ /^-----BEGIN PGP.*/ || msg.body.content_type.split('/').last.eql?('pgp')
|
631
|
+
Schleuder.log.debug 'found pgp-inline in input'
|
632
|
+
# Look for charset-armor-header. In most cases useless but nobody shall say we didn't try.
|
633
|
+
msg.body.each_line do |l|
|
634
|
+
break if l.strip.empty?
|
635
|
+
next unless m = l.match(/^Charset:\s(.*)$/)
|
636
|
+
@charset = m[1]
|
637
|
+
end
|
638
|
+
# TMail decodes QP if body() is called, Umlauts if to_s() is called.
|
639
|
+
# Life with TMail is hard...
|
640
|
+
if msg.encoding.to_s.downcase.eql?('quoted-printable') || !msg.main_type.eql?('text')
|
641
|
+
str = msg.body
|
642
|
+
else
|
643
|
+
str = msg.to_s
|
644
|
+
end
|
645
|
+
# We need to do this in three steps:
|
646
|
+
# 1. get the decoded body from TMail
|
647
|
+
# 2. delete the encoding-header
|
648
|
+
# 3. put the plaintext back into TMail
|
649
|
+
# Else TMail either can't decode the body correctly or 'over-decodes' it
|
650
|
+
# on next output
|
651
|
+
ptxt, enc, sig = crypt.decrypt(str)
|
652
|
+
if msg.main_type.eql?('text')
|
653
|
+
msg.body = ptxt
|
654
|
+
msg.charset = @charset || 'UTF-8'
|
655
|
+
msg.encoding = nil
|
656
|
+
else
|
657
|
+
# base64-encode if not text as we don't know what's in there. Might
|
658
|
+
# be binary data, which could break otherweise.
|
659
|
+
msg.body = [ptxt].pack("m")
|
660
|
+
msg.encoding = 'base64'
|
661
|
+
end
|
662
|
+
msg.in_encrypted = enc
|
663
|
+
msg.in_signed = sig
|
664
|
+
# RFC 2440 defines that the armored text by default is utf-8
|
665
|
+
if msg.content_type && msg['content-type'].params['x-action'] =~ /^pgp/
|
666
|
+
msg['content-type'].params['x-action'] = nil
|
667
|
+
end
|
668
|
+
unless msg.disposition_param('filename').nil?
|
669
|
+
msg['content-disposition'].params['filename'] = msg.disposition_param('filename').gsub(/\.(gpg|pgp|asc)$/, '')
|
670
|
+
end
|
671
|
+
else
|
672
|
+
Schleuder.log.debug 'no pgp-inline-data found, doing nothing'
|
673
|
+
end
|
674
|
+
end
|
675
|
+
|
676
|
+
def _encrypt_pgp_inline(msg, receiver)
|
677
|
+
msg.body = crypt.encrypt_str(msg.body.to_s, receiver)
|
678
|
+
# reset encoding, TMail has converted the content
|
679
|
+
msg.encoding = nil
|
680
|
+
# if attachment add '.gpg'-suffix to attachments file names
|
681
|
+
# (The query for 'filename' is a work around against buggy client
|
682
|
+
# formatting that doesn't disposition attachments as attachments. This
|
683
|
+
# variant works ok.)
|
684
|
+
unless msg.disposition_param('filename').nil?
|
685
|
+
msg.set_content_type('application', 'octet-stream', {'x-action' => 'pgp-encrypted'})
|
686
|
+
msg.set_disposition('attachment', {:filename => msg.disposition_param('filename') + '.gpg'})
|
687
|
+
else
|
688
|
+
msg.set_content_type('text', 'plain', {'x-action' => 'pgp-encrypted'})
|
689
|
+
msg.charset = 'UTF-8'
|
690
|
+
end
|
691
|
+
msg
|
692
|
+
end
|
693
|
+
|
694
|
+
def _enc_str(arg)
|
695
|
+
arg ? 'encrypted' : 'unencrypted'
|
696
|
+
end
|
697
|
+
|
698
|
+
def _sig_str(arg)
|
699
|
+
if arg
|
700
|
+
if crypt.get_key(arg.fpr).first
|
701
|
+
arg.to_s
|
702
|
+
else
|
703
|
+
#Schleuder.log.debug arg.inspect
|
704
|
+
"Unknown signature from #{arg.fpr} (public key not present)"
|
705
|
+
end
|
706
|
+
else
|
707
|
+
'No signature'
|
708
|
+
end
|
709
|
+
end
|
710
|
+
|
711
|
+
# Tests if the signature of the incoming mail (if any) belongs to a
|
712
|
+
# list-member or an admin by testing all gpg-key-ids of the signing
|
713
|
+
# key for matches
|
714
|
+
def _parse_signature
|
715
|
+
@from_admin = @from_member = false
|
716
|
+
|
717
|
+
Schleuder.log.debug 'Testing for valid signature'
|
718
|
+
if !in_signed
|
719
|
+
Schleuder.log.info 'No signature found'
|
720
|
+
elsif in_signed.status != 0
|
721
|
+
Schleuder.log.info 'Invalid or unknown signature found'
|
722
|
+
else
|
723
|
+
Schleuder.log.info 'Valid signature found'
|
724
|
+
key, msg = crypt.get_key(in_signed.fpr)
|
725
|
+
if key
|
726
|
+
Schleuder.log.debug "Testing key for matching some member's or admin's keys"
|
727
|
+
if Schleuder.list.find_admin_by_key(key)
|
728
|
+
@from_admin = true
|
729
|
+
end
|
730
|
+
if Schleuder.list.find_member_by_key(key)
|
731
|
+
@from_member = true
|
732
|
+
end
|
733
|
+
else
|
734
|
+
Schleuder.log.debug "Keylookup for signature failed! Reason: #{msg}"
|
735
|
+
end
|
736
|
+
end
|
737
|
+
end
|
738
|
+
|
739
|
+
# Convert the given text into quoted printable format, with an instruction
|
740
|
+
# that the text be eventually interpreted in the given charset.
|
741
|
+
def _quoted_printable(text, charset)
|
742
|
+
text = text.gsub( /[^a-z ]/i ) { _quoted_printable_encode($&) }.
|
743
|
+
gsub( / /, "_" )
|
744
|
+
"=?#{charset}?Q?#{text}?="
|
745
|
+
end
|
746
|
+
|
747
|
+
# Convert the given character to quoted printable format, taking into
|
748
|
+
# account multi-byte characters (if executing with $KCODE="u", for instance)
|
749
|
+
def _quoted_printable_encode(character)
|
750
|
+
result = ""
|
751
|
+
character.each_byte { |b| result << "=%02X" % b }
|
752
|
+
result
|
753
|
+
end
|
754
|
+
|
755
|
+
# A quick-and-dirty regexp for determining whether a string contains any
|
756
|
+
# characters that need escaping.
|
757
|
+
if !defined?(CHARS_NEEDING_QUOTING)
|
758
|
+
CHARS_NEEDING_QUOTING = /[\000-\011\013\014\016-\037\177-\377]/
|
759
|
+
end
|
760
|
+
|
761
|
+
# Quote the given text if it contains any "illegal" characters
|
762
|
+
def _quote_if_necessary(text, charset)
|
763
|
+
text = text.dup.force_encoding(Encoding::ASCII_8BIT) if text.respond_to?(:force_encoding)
|
764
|
+
|
765
|
+
(text =~ CHARS_NEEDING_QUOTING) ?
|
766
|
+
_quoted_printable(text, charset) :
|
767
|
+
text
|
768
|
+
end
|
769
|
+
|
770
|
+
# Helper for creating new message-parts
|
771
|
+
def _new_part(body, content_type, charset='', encoding='')
|
772
|
+
p = Mail.new
|
773
|
+
p.body = body.to_s
|
774
|
+
p.content_type = content_type
|
775
|
+
p.charset = charset.to_s unless charset.to_s.empty?
|
776
|
+
p.encoding = encoding.to_s unless encoding.to_s.empty?
|
777
|
+
p
|
778
|
+
end
|
779
|
+
|
780
|
+
def _collect_message_ids(ids)
|
781
|
+
return nil if ids.nil? || !ids.is_a?(Array) || ids.empty?
|
782
|
+
ids.select{ |id| Utils.schleuder_id?(id, Schleuder.list.listid) }
|
783
|
+
end
|
784
|
+
|
785
|
+
def self._schleuder_message_id
|
786
|
+
@@schleuder_message_id = Utils.generate_message_id(Schleuder.list.listid) unless @@schleuder_message_id
|
787
|
+
@@schleuder_message_id
|
788
|
+
end
|
789
|
+
|
790
|
+
def _message_ids(mail)
|
791
|
+
Schleuder.log.debug "Copying msgid to in-reply-to/references"
|
792
|
+
mail.in_reply_to = _collect_message_ids(self.in_reply_to)
|
793
|
+
mail.references = _collect_message_ids(self.references)
|
794
|
+
mail
|
795
|
+
end
|
796
|
+
|
797
|
+
def _list_headers(mail)
|
798
|
+
Schleuder.log.debug "Generating list-ids"
|
799
|
+
mail['List-Id'] = "<#{Schleuder.list.listid}>"
|
800
|
+
mail['List-Owner'] = _list_owner
|
801
|
+
mail['List-Post'] = _list_post
|
802
|
+
# TODO: adapt URL to version.
|
803
|
+
mail['List-Help'] = '<https://schleuder2.nadir.org/documentation.html>'
|
804
|
+
mail
|
805
|
+
end
|
806
|
+
|
807
|
+
def _list_owner
|
808
|
+
"<mailto:#{Schleuder.list.owner_addr}> (Use list's public key)"
|
809
|
+
end
|
810
|
+
|
811
|
+
def _list_post
|
812
|
+
if Schleuder.list.config.receive_admin_only
|
813
|
+
"NO (Admins only)"
|
814
|
+
elsif Schleuder.list.config.receive_authenticated_only
|
815
|
+
"<mailto:#{Schleuder.list.config.myaddr}> (Subscribers only)"
|
816
|
+
else
|
817
|
+
"<mailto:#{Schleuder.list.config.myaddr}>"
|
818
|
+
end
|
819
|
+
end
|
820
|
+
|
821
|
+
def _add_openpgp_header(mail)
|
822
|
+
Schleuder.log.debug "Add OpenPGP-Headers"
|
823
|
+
mail['OpenPGP'] = "id=#{Schleuder.list.key_fingerprint} "+
|
824
|
+
"(Send an email to #{Schleuder.list.sendkey_addr} to receive the public-key)"+
|
825
|
+
_gen_openpgp_pref_header
|
826
|
+
mail
|
827
|
+
end
|
828
|
+
|
829
|
+
def _gen_openpgp_pref_header
|
830
|
+
unless Schleuder.list.config.openpgp_header_preference == 'none'
|
831
|
+
pref_str = "; preference=#{Schleuder.list.config.openpgp_header_preference} ("
|
832
|
+
if Schleuder.list.config.receive_admin_only
|
833
|
+
pref_str << 'Only encrypted and signed emails by list-admins are accepted'
|
834
|
+
elsif !Schleuder.list.config.receive_authenticated_only
|
835
|
+
if Schleuder.list.config.receive_encrypted_only \
|
836
|
+
&& Schleuder.list.config.receive_signed_only
|
837
|
+
pref_str << 'Only encrypted and signed emails are accepted'
|
838
|
+
elsif Schleuder.list.config.receive_encrypted_only \
|
839
|
+
&& !Schleuder.list.config.receive_signed_only
|
840
|
+
pref_str << 'Only encrypted emails are accepted'
|
841
|
+
elsif !Schleuder.list.config.receive_encrypted_only \
|
842
|
+
&& Schleuder.list.config.receive_signed_only
|
843
|
+
pref_str << 'Only signed emails are accepted'
|
844
|
+
else
|
845
|
+
pref_str << 'All kind of emails are accepted'
|
846
|
+
end
|
847
|
+
elsif Schleuder.list.config.receive_authenticated_only
|
848
|
+
if Schleuder.list.config.receive_encrypted_only
|
849
|
+
pref_str << 'Only encrypted and signed emails by list-members are accepted'
|
850
|
+
else
|
851
|
+
pref_str << 'Only signed emails by list-members are accepted'
|
852
|
+
end
|
853
|
+
else
|
854
|
+
pref_str << 'All kind of emails are accepted'
|
855
|
+
end
|
856
|
+
pref_str << ')'
|
857
|
+
end
|
858
|
+
pref_str || ''
|
859
|
+
end
|
860
|
+
end
|
861
|
+
end
|