schleuder 2.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|