schleuder 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. data.tar.gz.sig +0 -0
  2. data/LICENSE +339 -0
  3. data/README +32 -0
  4. data/bin/schleuder +96 -0
  5. data/bin/schleuder-fix-gem-dependencies +30 -0
  6. data/bin/schleuder-init-setup +37 -0
  7. data/bin/schleuder-migrate-v2.1-to-v2.2 +205 -0
  8. data/bin/schleuder-newlist +384 -0
  9. data/contrib/check-expired-keys.rb +59 -0
  10. data/contrib/mutt-schleuder-colors.rc +10 -0
  11. data/contrib/mutt-schleuder-resend.vim +24 -0
  12. data/contrib/smtpserver.rb +76 -0
  13. data/ext/default-list.conf +146 -0
  14. data/ext/default-members.conf +7 -0
  15. data/ext/list.conf.example +14 -0
  16. data/ext/schleuder.conf +62 -0
  17. data/lib/schleuder.rb +49 -0
  18. data/lib/schleuder/archiver.rb +46 -0
  19. data/lib/schleuder/crypt.rb +188 -0
  20. data/lib/schleuder/errors.rb +5 -0
  21. data/lib/schleuder/list.rb +177 -0
  22. data/lib/schleuder/list_config.rb +146 -0
  23. data/lib/schleuder/log/listlogger.rb +56 -0
  24. data/lib/schleuder/log/outputter/emailoutputter.rb +118 -0
  25. data/lib/schleuder/log/outputter/metaemailoutputter.rb +50 -0
  26. data/lib/schleuder/log/schleuderlogger.rb +23 -0
  27. data/lib/schleuder/mail.rb +861 -0
  28. data/lib/schleuder/mailer.rb +26 -0
  29. data/lib/schleuder/member.rb +69 -0
  30. data/lib/schleuder/plugin.rb +54 -0
  31. data/lib/schleuder/processor.rb +363 -0
  32. data/lib/schleuder/schleuder_config.rb +72 -0
  33. data/lib/schleuder/storage.rb +84 -0
  34. data/lib/schleuder/utils.rb +80 -0
  35. data/lib/schleuder/version.rb +3 -0
  36. data/man/schleuder-newlist.8 +191 -0
  37. data/man/schleuder.8 +400 -0
  38. data/plugins/README +20 -0
  39. data/plugins/manage_keys_plugin.rb +113 -0
  40. data/plugins/manage_members_plugin.rb +152 -0
  41. data/plugins/manage_self_plugin.rb +26 -0
  42. data/plugins/resend_plugin.rb +35 -0
  43. data/plugins/version_plugin.rb +12 -0
  44. metadata +178 -0
  45. metadata.gz.sig +2 -0
@@ -0,0 +1,26 @@
1
+ module Schleuder
2
+ class Mailer
3
+ def self.send(msg, to=nil, sender=Schleuder.list.bounce_addr, nolog=false)
4
+ to = msg.to if to.nil?
5
+
6
+ # TODO: TLS
7
+ host = Schleuder.config.smtp_host
8
+ port = Schleuder.config.smtp_port
9
+ unless nolog
10
+ Schleuder.log.info { "Delivering mail to #{host}:#{port}" }
11
+ Schleuder.log.debug { "Mail has Sender: #{sender} - To: #{to.inspect}" }
12
+ end
13
+ Net::SMTP.start(host, port) do |smtpcon|
14
+ smtpcon.send_message msg.to_s, sender, to
15
+ end
16
+ true
17
+ rescue => e
18
+ if e.message.frozen?
19
+ Schleuder.log.warn "Something went wrong, while sending a message. I'll better preserve the message that should have been sent:\n#{msg.to_s}"
20
+ else
21
+ e.message << "\nOriginal message:\n#{msg.to_s}"
22
+ end
23
+ raise
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,69 @@
1
+ module Schleuder
2
+ class Member < Storage
3
+ schleuder_attr :email, nil
4
+ schleuder_attr :mime, 'MIME'
5
+ # only send encrypted mail to this member
6
+ schleuder_attr :encrypted_only, false
7
+ schleuder_attr :key_fingerprint, nil
8
+
9
+ def initialize(config_file, fromfile=true)
10
+ super(config_file, fromfile)
11
+
12
+ # compress fingerprint
13
+ self.key_fingerprint = self.key_fingerprint
14
+ end
15
+
16
+ def to_hash
17
+ Hash[*self.class.default_schleuder_attributes.keys.collect { |key|
18
+ unless (val = send(key)).to_s.empty?
19
+ [ key, val ]
20
+ else
21
+ nil
22
+ end
23
+ }.flatten.compact]
24
+ end
25
+
26
+ def key_fingerprint=(fpr)
27
+ schleuder_attributes['key_fingerprint'] = Schleuder::Utils.compress_fingerprint(fpr)
28
+ end
29
+
30
+ def to_s
31
+ email
32
+ end
33
+
34
+ def key(only_valid_keys=true)
35
+ if @key.nil? || !only_valid_keys # by default we want only valid keys. If
36
+ # we ask also for invalid keys we want
37
+ # to redo the keylookup
38
+ lookup_str = self.key_fingerprint.nil? ? self.email : self.key_fingerprint
39
+ key_result = crypt.get_key(lookup_str,only_valid_keys)
40
+ if k = key_result.first
41
+ @key = k
42
+ self.key_fingerprint = @key.subkeys.first.fingerprint
43
+ else
44
+ @key = nil
45
+ self.key_fingerprint = nil
46
+ return key_result
47
+ end
48
+ end
49
+ @key
50
+ end
51
+
52
+ def uses_key?(other_key)
53
+ key # initialize @key
54
+ !@key.nil? && @key.subkeys.first.fingerprint == other_key.subkeys.first.fingerprint
55
+ end
56
+
57
+ def key_descr
58
+ k, msg = key
59
+ k ? crypt.key_descr(k) : "*Warning:* #{msg}"
60
+ end
61
+
62
+ private
63
+ def crypt
64
+ # No simple way to get hands on mail.crypt, so we create another here.
65
+ # But on class side as it does not need to be per member
66
+ @@crypt ||= Crypt.new(nil)
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,54 @@
1
+ module Schleuder
2
+ # Parent-class for plugins. Sets up stub and helper methods.
3
+ class Plugin
4
+ # Define whether this plugin should be used for emails sent to the
5
+ # list-address or the request-address.
6
+ attr_reader :plugin_type
7
+
8
+ def Plugin.signing_key(mail)
9
+ @@key ||= mail.crypt.get_key(mail.in_signed.fpr).first
10
+ end
11
+
12
+ # Helper: creates a reply to the sender of the incoming message. The
13
+ # destination address is detemined by finding the list-member connected to
14
+ # the key that signed the incoming message. +msg+ is used as body for the
15
+ # outgoing mail.
16
+ def Plugin.reply(mail, msg, keywords)
17
+ Schleuder.log.info "Building reply mail"
18
+ out = Mail.new
19
+
20
+ out.subject = "Re: #{mail.subject}"
21
+ key = Plugin.signing_key(mail)
22
+ Schleuder.log.debug "Looking up member by key: #{key.inspect}"
23
+ member = Schleuder.list.find_member_by_key(key) || Schleuder.list.find_admin_by_key(key)
24
+ Schleuder.log.debug { "Result: #{member.inspect}" }
25
+ if member
26
+ Schleuder.log.info { "Found member for key: %s" % member.inspect }
27
+ else
28
+ Schleuder.log.error "No member or admin found for signing key, aborting reply."
29
+ return false
30
+ end
31
+
32
+ imail = out.individualize(member)
33
+ imail.in_reply_to = mail.message_id
34
+ imail.body = msg
35
+
36
+ unless imail.encrypt!(member)
37
+ Schleuder.log.debug 'encrypting failed, replacing body with please-fix-message'
38
+ imail.body = "Encrypting to #{imail.to} failed. Please fix."
39
+ end
40
+
41
+ Schleuder.log.info "Sending to #{imail.to}"
42
+ Mailer.send(imail)
43
+
44
+ unless (Schleuder.list.config.keywords_admin_notify & keywords).empty?
45
+ Schleuder.log.info "Sending notification to admins"
46
+ msg = "Hello list-admin,\n\nmember #{member.email} sent the keyword(s) '#{keywords.join("','")}' and received the following reply-message:\n\n\n".fmt << msg
47
+ Schleuder.log.notify_admin 'Notice', msg
48
+ end
49
+
50
+ Schleuder.log.info "Exiting"
51
+ exit 0
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,363 @@
1
+ module Schleuder
2
+ def log
3
+ SchleuderLogger.new unless Log4r::Logger['log4r']
4
+ # If there's a ListLogger we use that, else the global SchleuderLogger.
5
+ # The ListLogger is being set up in List::initialize().
6
+ Log4r::Logger['list'] || Log4r::Logger['log4r']
7
+ end
8
+ module_function :log
9
+
10
+ def config(config_file=nil)
11
+ # read the base config (class overloads itself with conf/schleuder.conf)
12
+ @config ||= SchleuderConfig.new(config_file)
13
+ end
14
+ module_function :config
15
+
16
+ def list
17
+ @list || nil
18
+ end
19
+ module_function :list
20
+
21
+ def list=(list)
22
+ @list = list
23
+ end
24
+ module_function :list=
25
+
26
+ def origmsg=(msg)
27
+ @origmsg = msg
28
+ end
29
+ module_function :origmsg=
30
+
31
+ def origmsg
32
+ @origmsg || nil
33
+ end
34
+ module_function :origmsg
35
+
36
+ def self.listname(listname=nil)
37
+ @listname ||= listname
38
+ end
39
+
40
+ class Processor
41
+ def self.run(listname, message)
42
+ Schleuder.listname listname
43
+ Schleuder.origmsg = message
44
+ Schleuder.log.debug "Testing if list-dir exists"
45
+ unless File.directory?(List.listdir(listname))
46
+ msg = "No such list: #{listname}"
47
+ $stderr.puts msg
48
+ Schleuder.log.warn msg
49
+ exit 1
50
+ end
51
+
52
+ Schleuder.log.debug "Creating list"
53
+ Schleuder.list = List.new(listname)
54
+
55
+ Schleuder.log.debug "Parsing incoming message"
56
+ mail = Mail.parse(message)
57
+ Schleuder.log.info "New mail incoming from #{mail.from}"
58
+
59
+ msize = message.size / 1024
60
+ lsize = Schleuder.list.config.max_message_size
61
+ if msize > lsize
62
+ self.bounce_or_drop "too big (#{msize}kB > #{lsize}kB)", "This address accepts only messages up to #{Schleuder.list.config.max_message_size} kilobyte.", mail
63
+ end
64
+
65
+ # if mail is a bounce we forward it directly to the admins, as
66
+ # dealing with those probably fails (encrypted attachments etc.)
67
+ if mail.bounce?
68
+ Schleuder.log.info "This is a bounce, forwarding to admins and exiting"
69
+ Schleuder.log.notify_admin "Problem", [ "Hello,\n\nI caught a bounce. Please take care of it.", message ]
70
+ Schleuder.log.info "Exiting cleanly"
71
+ exit(0)
72
+ end
73
+
74
+ if Schleuder.list.config.dump_incoming_mail
75
+ dump_dir = File.join(Schleuder.list.listdir,'dumps')
76
+ Dir.mkdir dump_dir unless File.directory? dump_dir
77
+ require 'digest/sha1'
78
+ msg_file = File.join(dump_dir,"#{Digest::SHA1.hexdigest(message[0..10000])}.msg")
79
+ Schleuder.log.info "Dumping message to #{msg_file}"
80
+ File.open(msg_file,"w") { |f| f << message }
81
+ Schleuder.log.notify_admin "Dump notification", "Hello,\n\nI dumped the current incoming message to #{msg_file} \n\nYou should erase that file after inspection!"
82
+ end
83
+
84
+ # if mail is a send-key-request we answer directly
85
+ if mail.to.to_a.include?(Schleuder.list.sendkey_addr) || (mail.subject.strip.downcase == 'send key!' && mail.body.strip.empty?)
86
+ Schleuder.log.info "Found send-key-request"
87
+ # TODO: refactor with Plugin::reply
88
+ out = Mail.new
89
+ out.subject = "Re: #{mail.subject}"
90
+ Schleuder.log.info "Building reply"
91
+ iout = out.individualize(Member.new({'email' => mail.from.first}))
92
+ iout.in_reply_to = mail.message_id
93
+ Schleuder.log.debug "Filling body with key-id and key"
94
+ iout.body = "#{Schleuder.list.key.to_s}\n#{mail.crypt.export(Schleuder.list.key_fingerprint).to_s}"
95
+ Schleuder.log.debug "Signing outgoing email"
96
+ rawmail = iout.sign
97
+ Schleuder.log.info "Handing over to Mailer"
98
+ Mailer.send(rawmail, iout.to)
99
+ Schleuder.log.info "Exiting"
100
+ exit 0
101
+ end
102
+
103
+ # Analyse data (hopefully an incoming mail).
104
+ # Is it multi-part? encrypted? pgp/mime? signed?
105
+ Schleuder.log.debug "Analysing incoming mail"
106
+ begin
107
+ mail.decrypt!
108
+ rescue GPGME::Error::DecryptFailed => e
109
+ Schleuder.log.error "#{e}\n\nOriginal message:\n#{message}"
110
+ $stdout.puts "Schleuder speaking. Cannot decrypt. Please check your setup.\nMessages to this list need to be encrypted with this key:\n#{Schleuder.list.key}"
111
+ exit 100
112
+ end
113
+
114
+ if !mail.to.nil? && mail.to.include?(Schleuder.list.owner_addr)
115
+ Schleuder.log.info "This is a message to the owner(s), forwarding to admins and exiting"
116
+ Schleuder.log.notify_admin "Message", [ "Hello,\n\nthe attached message is directed to you as list-owner(s). Please take care of it!\n\n", mail ]
117
+ Schleuder.log.info "Exiting cleanly"
118
+ exit(0)
119
+ end
120
+
121
+ if Schleuder.list.config.receive_signed_only && !mail.in_signed
122
+ self.bounce_or_drop "not validly signed", "This address accepts only messages validly signed in an OpenPGP-compatible way", mail
123
+ end
124
+
125
+ if Schleuder.list.config.receive_encrypted_only && !mail.in_encrypted
126
+ self.bounce_or_drop 'not encrypted', "This address accepts only messages encrypted with this key:\n#{Schleuder.list.key}", mail, message
127
+ end
128
+
129
+ if Schleuder.list.config.receive_authenticated_only && !(mail.from_member || mail.from_admin)
130
+ self.bounce_or_drop "not authenticated", "This address accepts only messages validly signed by a list-member's key.", mail
131
+ end
132
+
133
+ if Schleuder.list.config.receive_from_member_emailaddresses_only && !(mail.from_member_address?||mail.from_admin_address?)
134
+ self.bounce_or_drop "not from a member e-mail address", "This address accepts only messages from member addresses.", mail
135
+ end
136
+
137
+ if Schleuder.list.config.receive_admin_only && !mail.from_admin
138
+ self.bounce_or_drop "not authenticated as admin", "This address accepts only messages validly signed by a list-admin's key.", mail
139
+ end
140
+
141
+ if mail.to.to_a.include?(Schleuder.list.request_addr)
142
+ if (mail.from_member || mail.from_admin) && mail.in_encrypted
143
+ process_plugins(mail)
144
+ # If we reach this point no plugin took over
145
+ self.bounce_or_drop "not a valid request", "No valid command found in message. This address (-request) accepts only messages containing valid schleuder-keyword-commands.".fmt, mail
146
+ end
147
+ self.bounce_or_drop "not authenticated", "This address (-request) accepts only encrypted messages by list-members.".fmt, mail
148
+ end
149
+
150
+ unless mail.from_member || mail.from_admin
151
+ Schleuder.log.debug "Mail is not from a list-member, adding prefix_in"
152
+ mail.add_prefix_in!
153
+ end
154
+
155
+ # Checking for keywords in mail-body (e.g. X-RESEND)
156
+ if mail.from_member || mail.from_admin
157
+ if mail.in_encrypted
158
+ Schleuder.log.info "Incoming mail is encrypted and authorized, processing plugins"
159
+ process_plugins(mail)
160
+ else
161
+ Schleuder.log.info "Incoming mail is not encrypted or authorized, skipping plugins"
162
+ end
163
+ end
164
+
165
+ # first encrypt and send message to external recipients and collect
166
+ # information about the process
167
+ mail.resend_to.each do |receiver|
168
+ if receiver.email.to_s.empty?
169
+ errmsg = "Invalid value for receiver.email: %s. Skipping."
170
+ Schleuder.log.warn { errmsg % receiver.email.inspect }
171
+ next
172
+ end
173
+
174
+ imail = mail.individualize(receiver)
175
+ imail.add_public_footer!
176
+
177
+ if Schleuder.list.config.send_encrypted_only
178
+ enc_only = true
179
+ else
180
+ enc_only = receiver.encrypted_only
181
+ end
182
+
183
+ begin
184
+ ret = self.send(imail, receiver, enc_only)
185
+ if ret.first
186
+ # store meta-data about the sent message
187
+ crypt = " (#{ret[1]})"
188
+ mail.metadata[:resent_to] << receiver.email + crypt
189
+ else
190
+ mail.metadata[:error] << "Not resent to #{receiver.email}: #{ret[1]}"
191
+ end
192
+ rescue => e
193
+ Schleuder.log.error e
194
+ end
195
+ end
196
+
197
+ mail.add_prefix_out! unless mail.metadata[:resent_to].empty?
198
+ mail.add_prefix!
199
+ mail.add_metadata!
200
+
201
+ # archive message if necessary
202
+ Schleuder.list.archive(mail) if Schleuder.list.config.archive
203
+
204
+ # encrypt message and send mail for each list-member
205
+ Schleuder.log.info { 'Looping over all list-members to send out' }
206
+ Schleuder.list.members.each do |receiver|
207
+ Schleuder.log.debug { "Looping for #{receiver.inspect}" }
208
+
209
+ if receiver.email.to_s.empty?
210
+ errmsg = "Invalid value for receiver.email: %s. Skipping."
211
+ Schleuder.log.warn { errmsg % receiver.email.inspect }
212
+ next
213
+ end
214
+
215
+ imail = mail.individualize_member(receiver)
216
+
217
+ if Schleuder.list.config.send_encrypted_only
218
+ enc_only = true
219
+ else
220
+ enc_only = receiver.encrypted_only
221
+ end
222
+
223
+ # encrypt for each receiver
224
+ begin
225
+ sent = self.send(imail, receiver, enc_only)
226
+ unless sent.first
227
+ Schleuder.log.error sent[1]
228
+ end
229
+ rescue => e
230
+ Schleuder.log.error e
231
+ end
232
+ end
233
+ Schleuder.log.info { 'Processing done, this is the end.' }
234
+ end
235
+
236
+ def self.send(mail, receiver, encrypted_only=true, sender=Schleuder.list.bounce_addr)
237
+ begin
238
+ encrypted, errmsg = mail.encrypt!(receiver)
239
+ rescue GPGME::Error::UnusablePublicKey => e
240
+ # This exception is thrown, if the public key of a certain list
241
+ # member is not usable (because it is revoked, expired, disabled or
242
+ # invalid).
243
+ k = e.keys.first
244
+ key = mail.crypt.get_key(k.fpr).first
245
+ errmsg = "#{e.message}: (#{k.class})\n#{key.to_s}"
246
+ Schleuder.log.error "Encryption failed (#{errmsg})"
247
+ encrypted = false
248
+ rescue GPGME::Error::General => e
249
+ errmsg = e.message
250
+ Schleuder.log.error "Encryption failed (#{errmsg})"
251
+ encrypted = false
252
+ end
253
+ if encrypted
254
+ Mailer.send(mail, nil, sender)
255
+ return [true, 'encrypted']
256
+ else
257
+ if encrypted_only
258
+ Schleuder.log.debug "Sending plaintext not allowed, message not sent to #{receiver.inspect}!"
259
+ return [false, "Encrypting to #{receiver.email.inspect} failed: #{errmsg} (sending plaintext disallowed)"]
260
+ else
261
+ Schleuder.log.info "Sending plaintext allowed, doing so"
262
+ # sign msg
263
+ if rawmail = mail.sign
264
+ Mailer.send(rawmail, mail.to, sender)
265
+ return [true, "unencrypted (#{errmsg})"]
266
+ else
267
+ return [false, "signing failed"]
268
+ end
269
+ end
270
+ end
271
+ end
272
+
273
+ def self.test(listname=nil)
274
+ # Doing things that trigger exceptions if they fail, which we rescue and
275
+ # put to stderr
276
+ # TODO: test #{smtp_host}:25
277
+ begin
278
+ # test listdir
279
+ Dir.entries Schleuder.config.lists_dir
280
+
281
+ # test superadminaddr
282
+ Utils::verify_addr('superadminaddr', Schleuder.config.superadminaddr)
283
+
284
+ if listname
285
+ # testwise create a list-object (reads list-config etc.)
286
+ list = List.new listname
287
+ # test for listdir
288
+ Dir.entries list.listdir
289
+ # test admins
290
+ list.config.admins.each do |a|
291
+ Utils::verify_addr('adminaddr', a.email)
292
+ key, msg = a.key
293
+ if !key
294
+ raise "Admin: #{a}'s key lookup fails! Problem: #{msg}"
295
+ end
296
+ end
297
+
298
+ crypt = Crypt.new(list.config.gpg_password)
299
+
300
+ list.members.each do |m|
301
+ Utils::verify_addr('member address', m.email)
302
+ key, msg = m.key
303
+ if !key
304
+ raise "#{m}'s key lookup fails! Problem: #{msg}"
305
+ end
306
+ end
307
+ end
308
+ rescue => e
309
+ $stderr.puts e.message
310
+ $stderr.puts e.backtrace
311
+ exit 1
312
+ end
313
+ end
314
+
315
+ def self.newlist(listname, interactive='true', args=nil)
316
+ created_list = Schleuder::ListCreator.create(listname,interactive,args)
317
+ end
318
+
319
+ def self.bounce_or_drop(status, bounce_msg, mail)
320
+ Schleuder.log.warn "Mail is #{status}, not passing it along"
321
+
322
+ if Schleuder.list.config.bounces_drop_all
323
+ Schleuder.log.warn "Found bounces_drop_all being true: dropping!"
324
+ self.bounce_notify_admin status, "bounces_drop_all is true"
325
+ exit 0
326
+ end
327
+
328
+ Schleuder.log.debug "Testing bounces_drop_on_headers"
329
+ Schleuder.list.config.bounces_drop_on_headers.each do |header,value|
330
+ Schleuder.log.debug "Testing #{header} => #{value}"
331
+ if mail.header[header].to_s.downcase == value.to_s.downcase
332
+ Schleuder.log.warn "Found matching drop-header: #{header} => #{value} -- dropping!"
333
+ self.bounce_notify_admin status, "Forbidden header found: '#{header.capitalize}: #{value.capitalize}'"
334
+ exit 0
335
+ end
336
+ end
337
+
338
+ # if we're still alive: bounce message
339
+ Schleuder.log.info "bouncing mail to sender"
340
+ self.bounce_notify_admin status
341
+ $stdout.puts bounce_msg
342
+ exit 100
343
+ end
344
+
345
+ def self.bounce_notify_admin(reason, drop_reason='')
346
+ msg = "The attached incoming email has not been passed to the list.\nReason: #{reason}.\n"
347
+ if drop_reason.empty?
348
+ msg += "\nIt has been bounced to the sender.\n"
349
+ else
350
+ msg += "\nIt has *not* been bounced but dropped.\nReason: #{drop_reason}.\n"
351
+ end
352
+
353
+ Schleuder.log.notify_admin("Notice", [ msg, Mail.parse(Schleuder.origmsg) ]) if Schleuder.list.config.bounces_notify_admin
354
+ end
355
+
356
+ def self.process_plugins(mail)
357
+ Schleuder.log.info 'Email is encrypted and authorized, processing plugins'
358
+ # process plugins
359
+ Schleuder.log.debug 'Processing plugins'
360
+ mail.process_plugins!
361
+ end
362
+ end
363
+ end