schleuder 2.2.1 → 2.2.2

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 CHANGED
Binary file
@@ -8,7 +8,14 @@ require 'rubygems'
8
8
  require 'rubygems/dependency_installer.rb'
9
9
 
10
10
  inst = Gem::DependencyInstaller.new
11
- spec = Gem::Specification.find_by_name 'schleuder'
11
+
12
+ if Gem::Specification.respond_to? :find_by_name
13
+ spec = Gem::Specification.find_by_name 'schleuder'
14
+ spec_file = spec.spec_file
15
+ else
16
+ spec = Gem.source_index.find_name('schleuder').first
17
+ spec_file = spec.loaded_from
18
+ end
12
19
 
13
20
  begin
14
21
  if RUBY_VERSION < "1.9"
@@ -21,7 +28,7 @@ begin
21
28
  inst.install name, ver
22
29
  spec.add_dependency name, ver
23
30
  # Write spec back to file, from rubygems/installer.rb
24
- File.open(spec.spec_file.untaint, "w") do |f|
31
+ File.open(spec_file.untaint, "w") do |f|
25
32
  f << spec.to_ruby_for_cache
26
33
  end
27
34
  rescue Gem::FilePermissionError => e
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
+ trap ("INT") { exit 1 }
4
+
3
5
  $:.unshift File.dirname(__FILE__) + '/../lib'
4
6
  require 'schleuder'
5
7
  require 'etc'
@@ -90,7 +92,9 @@ class ListCreator
90
92
  listdir = File.join([Schleuder.config.lists_dir, listname.split('@').reverse].flatten)
91
93
  raise NewListError, "List or parts of a list named: #{listname} already exists!" if File.directory?(listdir)
92
94
  list_email = ListCreator::verify_emailvar(listname,interactive,"The lists's email address")
93
- list_realname = ListCreator::verify_strvar(args[:list_realname],interactive,"'Realname' (for GPG-key and email-headers)")
95
+ list_realname = ListCreator::verify_strvar(args[:list_realname],interactive,"'Realname' (for GPG-key and email-headers)") do |var|
96
+ raise NewListError, "Realname must be at least 5 characters long (requirement of GnuPG)" if var.size < 5
97
+ end
94
98
  list_adminaddress = ListCreator::verify_emailvar(args[:list_adminaddress],interactive,"Admin email address")
95
99
 
96
100
  raise NewListError,"Lists' email address and the admin address can't be the same" if list_email == list_adminaddress
@@ -183,7 +187,7 @@ class ListCreator
183
187
  Schleuder.log.debug "Store list config..."
184
188
  list.config = list.config
185
189
  Schleuder.log.debug "Changing ownership..."
186
- ListCreator::filepermissions(listdir,mailuser)
190
+ ListCreator::filepermissions(list, mailuser)
187
191
  Schleuder.log.debug "List successfully created..."
188
192
  ListCreator::print_list_infos(list) if interactive
189
193
  end
@@ -206,7 +210,7 @@ class ListCreator
206
210
  end
207
211
  end
208
212
 
209
- def self.verify_strvar(var,interactive,question, echo=true)
213
+ def self.verify_strvar(var,interactive,question, echo=true, &block)
210
214
  if (var.nil? or var.empty?) and interactive then
211
215
  str = question+": "
212
216
  if echo
@@ -215,6 +219,7 @@ class ListCreator
215
219
  var = ask(str) { |question| question.echo = '*' }
216
220
  end
217
221
  end
222
+ yield(var) if block_given?
218
223
  raise NewListError,"Missing mandatory variable: "+question if (var.nil? or var.empty?)
219
224
  var
220
225
  end
@@ -290,9 +295,9 @@ class ListCreator
290
295
  else
291
296
  reply = nil
292
297
  end
293
- #$stderr.puts line
298
+ $stderr.puts line if $DEBUG
294
299
  if reply
295
- #$stderr.puts reply
300
+ $stderr.puts reply if $DEBUG
296
301
  stdin.puts reply
297
302
  end
298
303
  end
@@ -332,7 +337,7 @@ class ListCreator
332
337
  end
333
338
 
334
339
  def self.import_keypair(list,list_privatekeyfile,list_publickeyfile)
335
- crypt = Schleuder::Crypt.new(list.config.gpg_password)
340
+ crypt = Schleuder::Crypt.new(list.config.gpg_password, :new_keyring => true)
336
341
  Schleuder.log.debug "Importing private key from #{list_privatekeyfile}"
337
342
  crypt.add_key_from_file(list_privatekeyfile)
338
343
  Schleuder.log.debug "Importing public key from #{list_publickeyfile}"
@@ -353,15 +358,23 @@ Passphrase: #{pass}
353
358
  </GnupgKeyParms>"
354
359
  end
355
360
 
356
- def self.filepermissions(listdir, mailuser)
357
- File.chown(mailuser,nil,listdir)
358
- File.chmod(0700,listdir)
361
+ def self.filepermissions(list, mailuser)
362
+ listdir = list.listdir
363
+ dirs = [File.dirname(listdir), listdir]
364
+ FileUtils.chown mailuser, nil, dirs
365
+ FileUtils.chmod 0700, dirs
359
366
  Dir.new(listdir).each{ |f|
360
367
  unless f =~ /^\./
361
368
  File.chown(mailuser,nil,listdir+"/"+f)
362
- File.chmod(0600)
369
+ File.chmod(0600,listdir+"/"+f)
363
370
  end
364
371
  }
372
+ if list.config.log_file
373
+ log_file = list.config.log_file
374
+ log_file = File.join(listdir, log_file) unless log_file.start_with?('/')
375
+ FileUtils.chown mailuser, nil, log_file
376
+ FileUtils.chmod 0600, log_file
377
+ end
365
378
  end
366
379
 
367
380
  def self.print_list_infos(list)
@@ -8,6 +8,7 @@
8
8
 
9
9
  $VERBOSE = nil
10
10
 
11
+ $:.unshift File.dirname(__FILE__) + '/../lib'
11
12
  require 'schleuder'
12
13
  include Schleuder
13
14
 
@@ -26,10 +26,6 @@
26
26
  # Name of the per list config file.
27
27
  #lists_configfile: list.conf
28
28
  #
29
- # Per list logfile name. Will be written into the directory
30
- # of the list.
31
- #lists_logfile: list.log
32
- #
33
29
  # Name of the per list file containing all members and their
34
30
  # options.
35
31
  #lists_memberfile: members.conf
@@ -3,15 +3,17 @@ module Schleuder
3
3
  # change but aliases will be set up then.
4
4
  class Crypt
5
5
  # Instantiates and stores password
6
- def initialize(password)
7
- # Check file permissions. ruby-gpgme unfortunately returns unspecific
8
- # errors if it can't read files.
9
- %w(pubring.gpg secring.gpg trustdb.gpg).each do |fn|
10
- f = File.join(ENV['GNUPGHOME'], fn)
11
- if ! File.readable?(f)
12
- raise Errno::EACCES.new('%s is not readable' % f)
13
- elsif ! File.writable?(f)
14
- raise Errno::EACCES.new('%s is not writable' % f)
6
+ def initialize(password, options={})
7
+ unless options[:new_keyring]
8
+ # Check file permissions. ruby-gpgme unfortunately returns unspecific
9
+ # errors if it can't read files.
10
+ %w(pubring.gpg secring.gpg trustdb.gpg).each do |fn|
11
+ f = File.join(ENV['GNUPGHOME'], fn)
12
+ if ! File.readable?(f)
13
+ raise Errno::EACCES.new('%s is not readable' % f)
14
+ elsif ! File.writable?(f)
15
+ raise Errno::EACCES.new('%s is not writable' % f)
16
+ end
15
17
  end
16
18
  end
17
19
 
@@ -21,7 +23,7 @@ module Schleuder
21
23
  end
22
24
  @ctx = GPGME::Ctx.new
23
25
  # feed the passphrase into the Context
24
- @ctx.set_passphrase_cb(method(:passfunc))
26
+ @ctx.set_passphrase_cb(method(:passfunc))
25
27
  end
26
28
 
27
29
  # Verify a gpg-signature. Use +signed_string+ if the signature is
@@ -56,35 +58,41 @@ module Schleuder
56
58
  in_encrypted = nil
57
59
  in_signed = nil
58
60
 
59
- # TODO: return ciphertext if missing key. Sensible e.g. if it is part
60
- # of a nested MIME-message and encrypted to someone else on purpose.
61
- # Breaking if even the whole message is not decryptable is a job for
62
- # the processor.
61
+ # TODO: return ciphertext if missing key when decrypting inline
62
+ # attachments or recursing into mime-messages. Breaking if even the
63
+ # whole message is not decryptable is a job for the processor.
63
64
 
64
- # return input instead of empty String if not encrypted.
65
- # String#content_type is provided by filemagic/ext.
66
- unless str =~ /^-----BEGIN PGP MESSAGE-----/ || str.content_type.split('/').last.eql?('pgp')
67
- output, in_signed = verify(str)
68
- # match pgp-mime- and inline-pgp-signatures
69
- if str =~ /^-----BEGIN PGP SIG/
70
- Schleuder.log.debug 'found signed, not encrypted message, verifying'
71
- output, in_signed = verify(str)
72
- else
73
- Schleuder.log.debug 'found not signed, not encrypted message, returning input'
74
- output = str
75
- end
65
+ # match pgp-mime- and inline-pgp-signatures
66
+ if str =~ /^-----BEGIN PGP SIG/
67
+ Schleuder.log.debug 'found ascii-armored, signed, not encrypted message, verifying'
68
+ output, in_signed = verify(str)
76
69
  else
77
- Schleuder.log.debug 'found pgp content, decrypting and verifying with gpgme'
78
- in_encrypted = true
79
- output = GPGME.decrypt(str, :passphrase_callback => method(:passfunc)) do |sig|
80
- in_signed = sig
81
- end
82
- if output.empty?
83
- Exception.new("Output from GPGME.decrypt was empty!")
70
+ begin
71
+ Schleuder.log.debug 'Trying to decrypt'
72
+ output = GPGME.decrypt(str, :passphrase_callback => method(:passfunc)) do |sig|
73
+ in_signed = sig
74
+ end
75
+ in_encrypted = true
76
+ rescue GPGME::Error::NoData => exc
77
+ Schleuder.log.debug "Caught NoData-exception from gpgme. This probably means we're dealing with a binary signature, trying to verify."
78
+ output, in_signed = verify(str)
79
+ if output.empty?
80
+ Schleuder.log.debug "Empty output from verification. No more options, returning."
81
+ end
84
82
  end
85
- # TODO: return mailadresses or keys instead of signature-objects?
86
83
  end
84
+ Schleuder.log.debug "mime_type of decrypted content: #{output.mime.inspect}"
85
+ Schleuder.log.debug "in_signed: #{in_signed.inspect}"
86
+ Schleuder.log.debug "in_encrypted: #{in_encrypted.inspect}"
87
+ if output.empty?
88
+ Schleuder.log.debug "Empty output, returning input"
89
+ output = str
90
+ end
91
+ # TODO: return mailadresses or keys instead of signature-objects?
87
92
  [output, in_encrypted, in_signed]
93
+ rescue GPGME::Error::General => exc
94
+ Schleuder.log.warn = "gpgme returned a general error. Most likely this is due to unexpected input, but as you never know here's the input and the exception:\ninput:\n#{str.inspect}\n#{exc.to_s}\n#{exc.backtrace[0..9].join("\n")}"
95
+ [str, nil, nil]
88
96
  end
89
97
 
90
98
  # Encrypt a string to a single receiver and sign it. +receiver+ must be a
@@ -114,7 +122,7 @@ module Schleuder
114
122
  pattern = "<#{pattern}>" if pattern =~ /.*@.*/ && !(pattern =~ /^<.*>$/)
115
123
  keys = list_keys(pattern)
116
124
 
117
- keys.reject! { |key| [:revoked,:expired].include?(key.trust) } if only_valid_keys
125
+ keys.reject! { |key| unusable_key?(key) } if only_valid_keys
118
126
 
119
127
  if keys.empty?
120
128
  [false, "no key found for #{pattern}."]
@@ -175,7 +183,21 @@ module Schleuder
175
183
  private
176
184
 
177
185
  def key_infos(keys, only_valid_keys=false)
178
- keys.collect { |key| key_descr(key) + (!only_valid_keys && [:revoked, :expired].include?(key.trust)) ? " *#{key.trust}*" : '' }
186
+ keys.collect do |key|
187
+ info = key_descr(key)
188
+ unless only_valid_keys
189
+ info << trust_info(key)
190
+ end
191
+ info
192
+ end
193
+ end
194
+
195
+ def trust_info(key)
196
+ unusable_key?(key) ? " *#{key.trust}*" : ''
197
+ end
198
+
199
+ def unusable_key?(key)
200
+ [:revoked, :expired].include?(key.trust)
179
201
  end
180
202
 
181
203
  def passfunc(hook, uid_hint, passphrase_info, prev_was_bad, fd)
@@ -8,7 +8,7 @@ module Schleuder
8
8
  # Prepare some variables, set up the SchleuderLogger and set GNUPGHOME
9
9
  def initialize(listname,newlist=false)
10
10
  @listname = listname
11
- @config = _load_config(false) if newlist
11
+ @config = ListConfig.new if newlist
12
12
  @log = ListLogger.new listname, listdir, config
13
13
 
14
14
  # setting GNUPGHOME to list's home, to make use of the keys there
@@ -22,7 +22,7 @@ module Schleuder
22
22
  unless @members
23
23
  Schleuder.log.debug("reading #{members_file}")
24
24
  @members = YAML::load_file(members_file).collect do |h|
25
- h.kind_of?(Schleuder::Member) ? h : Schleuder::Member.new(h,false)
25
+ h.kind_of?(Schleuder::Member) ? h : Schleuder::Member.new(h)
26
26
  end
27
27
  end
28
28
  @members
@@ -36,7 +36,7 @@ module Schleuder
36
36
  def members=(arr)
37
37
  Schleuder.log.debug 'writing members'
38
38
  Schleuder.log.info("writing #{members_file}")
39
- @members = arr.collect { |m| m.kind_of?(Hash) ? Member.new(m,false) : m }
39
+ @members = arr.collect { |m| m.kind_of?(Hash) ? Member.new(m) : m }
40
40
  _write(YAML.dump(@members.collect { |m| m.to_hash }), members_file)
41
41
  @members
42
42
  end
@@ -144,10 +144,9 @@ module Schleuder
144
144
  private
145
145
 
146
146
  # Loads the configuration
147
- # fromfile = Whether to load the config from file.
148
- def _load_config(fromfile=true)
147
+ def _load_config
149
148
  Schleuder.log.debug("reading list-config for: #{@listname}") unless Schleuder.log.nil?
150
- @config = ListConfig.new(File.join(listdir, Schleuder.config.lists_configfile),fromfile)
149
+ @config = ListConfig.new(File.join(listdir, Schleuder.config.lists_configfile))
151
150
  end
152
151
 
153
152
  def find_by_key(ary, key)
@@ -75,10 +75,10 @@ module Schleuder
75
75
  schleuder_attr :headers_to_meta, [:from, :to, :cc, :date]
76
76
 
77
77
  # Restrict specific plugins to admin
78
- schleuder_attr :keywords_admin_only, ['SAVE-MEMBERS', 'DEL-KEY']
78
+ schleuder_attr :keywords_admin_only, ['ADD-MEMBER', 'DELETE-MEMBER', 'DELETE-KEY', 'SAVE-MEMBERS', 'DEL-KEY' ]
79
79
 
80
80
  # Notify admin if these keywords triggered commands.
81
- schleuder_attr :keywords_admin_notify, [ 'X-ADD-KEY' ]
81
+ schleuder_attr :keywords_admin_notify, [ 'ADD-KEY' ]
82
82
 
83
83
  # Drop any bounces (incoming email not passing the receive_*_only-rules)
84
84
  schleuder_attr :bounces_drop_all, false
@@ -112,11 +112,11 @@ module Schleuder
112
112
 
113
113
  ### END OF CONFIG OPTIONS
114
114
 
115
- def initialize(config_file, fromfile=true)
115
+ def initialize(config=nil)
116
116
  # First Overload with default-list.conf then load our config
117
117
  overload_from_file!(Schleuder.config.lists_default_conf)
118
118
  # overload with config_file
119
- super(config_file, fromfile)
119
+ super(config)
120
120
 
121
121
  # load admins as members
122
122
  self.admins = self.admins
@@ -111,6 +111,11 @@ module Schleuder
111
111
  self.parts.each do |part|
112
112
  _decrypt_pgp_inline(part)
113
113
  end
114
+ # If all parts are equally encrypted and signed mark the whole message as such
115
+ self.in_encrypted = self.parts.map{ |p| p.in_encrypted }.uniq == [true]
116
+ if self.parts.map { |p| p.in_signed.fpr }.uniq.size == 1
117
+ self.in_signed = self.parts.first.in_signed
118
+ end
114
119
  else
115
120
  Schleuder.log.debug 'single inline content found, looking for pgp content'
116
121
  _decrypt_pgp_inline(self)
@@ -627,14 +632,14 @@ module Schleuder
627
632
  def _decrypt_pgp_inline(msg)
628
633
  if msg._content_type_stripped == 'text/html'
629
634
  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')
635
+ elsif msg.body =~ /^-----BEGIN PGP.*/ || ['pgp', 'gpg'].include?(msg.disposition_param('filename').to_s.split('.').last.to_s.downcase)
631
636
  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
637
+ # We need to do this in three steps:
638
+ # 1. get the decoded body from TMail
639
+ # 2. delete the encoding-header
640
+ # 3. put the plaintext back into TMail
641
+ # Else TMail either can't decode the body correctly or 'over-decodes' it
642
+ # on next output
638
643
  # TMail decodes QP if body() is called, Umlauts if to_s() is called.
639
644
  # Life with TMail is hard...
640
645
  if msg.encoding.to_s.downcase.eql?('quoted-printable') || !msg.main_type.eql?('text')
@@ -642,32 +647,29 @@ module Schleuder
642
647
  else
643
648
  str = msg.to_s
644
649
  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
650
  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
651
+ if ptxt == str
652
+ Schleuder.log.debug "Output equals input, content was obviously not suitable for gpg. Passing it untouched."
656
653
  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)$/, '')
654
+ #Schleuder.log.debug "ptxt.mime_encoding: #{FileMagic.fm(:mime_encoding).buffer(ptxt).inspect}"
655
+ Schleuder.log.debug "Processing plaincontent: #{ptxt.mime}"
656
+ msg.in_encrypted = enc
657
+ msg.in_signed = sig
658
+ if ! msg.disposition_param('filename').nil?
659
+ msg['content-disposition'].params['filename'] = msg.disposition_param('filename').gsub(/\.(gpg|pgp|asc)$/, '')
660
+ end
661
+ if ptxt.content_type.split('/').first == 'text'
662
+ msg['content-type'] = ptxt.mime
663
+ msg.encoding = nil
664
+ msg.body = ptxt
665
+ else
666
+ # TMail handles binary data in an "interesting" way, so we manuall encode before handing Tmail the data.
667
+ msg['content-type'] = ptxt.content_type
668
+ msg.encoding = 'Base64'
669
+ msg.body = [ptxt].pack('m')
670
+ end
670
671
  end
672
+ msg
671
673
  else
672
674
  Schleuder.log.debug 'no pgp-inline-data found, doing nothing'
673
675
  end
@@ -696,15 +698,16 @@ module Schleuder
696
698
  end
697
699
 
698
700
  def _sig_str(arg)
699
- if arg
701
+ # gpgme breaks if we use arg.to_s.empty? here.
702
+ if arg.nil? || (arg.is_a?(String) && arg.empty?)
703
+ 'No signature'
704
+ else
700
705
  if crypt.get_key(arg.fpr).first
701
706
  arg.to_s
702
707
  else
703
708
  #Schleuder.log.debug arg.inspect
704
709
  "Unknown signature from #{arg.fpr} (public key not present)"
705
710
  end
706
- else
707
- 'No signature'
708
711
  end
709
712
  end
710
713
 
@@ -6,8 +6,8 @@ module Schleuder
6
6
  schleuder_attr :encrypted_only, false
7
7
  schleuder_attr :key_fingerprint, nil
8
8
 
9
- def initialize(config_file, fromfile=true)
10
- super(config_file, fromfile)
9
+ def initialize(config=nil)
10
+ super(config)
11
11
 
12
12
  # compress fingerprint
13
13
  self.key_fingerprint = self.key_fingerprint