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.
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