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,46 @@
1
+ module Schleuder
2
+ class Archiver
3
+ def archive(mail)
4
+ Schleuder.log.info "Archiving email"
5
+ mail2archive = mail.individualize_member(_receiver)
6
+
7
+ # TODO: wrap that duplicated code out into it's dedicated method
8
+ begin
9
+ encrypted, errmsg = mail2archive.encrypt!(_receiver)
10
+ rescue GPGME::Error::UnusablePublicKey => e
11
+ # This exception is thrown, if the public key of a certain list
12
+ # member is not usable (because it is revoked, expired, disabled or
13
+ # invalid).
14
+ k = e.keys.first
15
+ key = mail2archive.crypt.get_key(k.fpr).first
16
+ errmsg = "#{e.message}: (#{k.class})\n#{key.to_s}"
17
+ encrypted = false
18
+ rescue GPGME::Error::General => e
19
+ errmsg = e.message
20
+ encrypted = false
21
+ end
22
+
23
+ if encrypted
24
+ _dump(mail2archive)
25
+ else
26
+ Schleuder.log.error("Could not encrypt message with list's key to archive it. Skipping archiving of message...\n\nError Message: #{errmsg}")
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def _dump(mail)
33
+ now = Time.now
34
+ dump_dir = File.join(Schleuder.list.listdir,'archive',[:year,:month,:day].collect{|m| now.send(m).to_s })
35
+ require 'fileutils'
36
+ FileUtils.mkdir_p dump_dir unless File.directory? dump_dir
37
+ msg_file = File.join(dump_dir,"#{Time.now.strftime('%H%M%S')}-#{mail.message_id[1..-2]}")
38
+ Schleuder.log.info("Archiving message to #{msg_file}")
39
+ File.open(msg_file,"w") { |f| f << mail.to_s }
40
+ end
41
+
42
+ def _receiver
43
+ @receiver ||= Member.new('email' => Schleuder.list.config.myaddr)
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,188 @@
1
+ module Schleuder
2
+ # Wrapper for ruby-gpgme. Method naming is not strictly logical, this might
3
+ # change but aliases will be set up then.
4
+ class Crypt
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)
15
+ end
16
+ end
17
+
18
+ @password = password
19
+ if GPGME.respond_to? 'check_version'
20
+ GPGME::check_version('0.0.0')
21
+ end
22
+ @ctx = GPGME::Ctx.new
23
+ # feed the passphrase into the Context
24
+ @ctx.set_passphrase_cb(method(:passfunc))
25
+ end
26
+
27
+ # Verify a gpg-signature. Use +signed_string+ if the signature is
28
+ # detached. Returns a GPGME::SignatureResult
29
+ def verify(sig, signed_string='')
30
+ in_signed = ''
31
+ if signed_string.empty?
32
+ # verify +sig+ as cleartext (aka pgp/inline) signature
33
+ Schleuder.log.debug 'No extra signed_string, verifying cleartext signature'
34
+ output = GPGME.verify(sig) do |sig|
35
+ in_signed = sig
36
+ end
37
+ else
38
+ # verify detached signature
39
+ Schleuder.log.debug 'Verifying detached signature'
40
+ # Don't know why we need a GPGME::Data object this time but without gpgme throws exceptions
41
+ plain = GPGME::Data.new
42
+ GPGME.verify(sig, signed_string, plain) do |sig|
43
+ in_signed = sig
44
+ end
45
+ output = signed_string
46
+
47
+ end
48
+ Schleuder.log.debug 'verify_result: ' + in_signed.inspect
49
+
50
+ [output, in_signed]
51
+ end
52
+
53
+ # Decrypt a string.
54
+ def decrypt(str)
55
+ output = ""
56
+ in_encrypted = nil
57
+ in_signed = nil
58
+
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.
63
+
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
76
+ 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!")
84
+ end
85
+ # TODO: return mailadresses or keys instead of signature-objects?
86
+ end
87
+ [output, in_encrypted, in_signed]
88
+ end
89
+
90
+ # Encrypt a string to a single receiver and sign it. +receiver+ must be a
91
+ # Schleuder::Member
92
+ def encrypt_str(str, receiver)
93
+ # encrypt and sign and return encrypted data as string
94
+ # For some reason sometimes the last two characters of str are stolen
95
+ # unless we append a blank or newline... Life is hard...
96
+ GPGME.encrypt([receiver.key],
97
+ "#{str} ",
98
+ {:passphrase_callback => method(:passfunc),
99
+ :armor => true,
100
+ :sign => true,
101
+ :always_trust => true
102
+ })
103
+ end
104
+
105
+ # Lists all public keys matching +pattern+. Returns an array of
106
+ # GPGME::GpgKey's
107
+ def list_keys(pattern='')
108
+ GPGME.list_keys(pattern)
109
+ end
110
+
111
+ # Returns the GPGME::GpgKey matching +pattern+. Log an error if more than
112
+ # one matches, because duplicated user-ids is a sensitive issue.
113
+ def get_key(pattern, only_valid_keys=false)
114
+ pattern = "<#{pattern}>" if pattern =~ /.*@.*/ && !(pattern =~ /^<.*>$/)
115
+ keys = list_keys(pattern)
116
+
117
+ keys.reject! { |key| [:revoked,:expired].include?(key.trust) } if only_valid_keys
118
+
119
+ if keys.empty?
120
+ [false, "no key found for #{pattern}."]
121
+ elsif keys.length > 1
122
+ Schleuder.log.warn "There's more than one key matching the pattern you gave me!"
123
+ Schleuder.log.debug { "Pattern: #{pattern.inspect}" }
124
+ Schleuder.log.debug { "Keys: #{keys.inspect}" }
125
+ [false, "no distinct key for #{pattern.inspect} found. Matching keys: #{key_infos(keys,only_valid_keys).join(', ')}"]
126
+ else
127
+ [keys.first]
128
+ end
129
+ end
130
+
131
+ # Signs +string+ with the private key of the list (aka detached signature)
132
+ def sign(string)
133
+ GPGME::detach_sign(string, {:armor => true, :passphrase_callback => method(:passfunc)})
134
+ end
135
+
136
+ # Clearsigns +string+ with the private key of the list
137
+ def clearsign(string)
138
+ GPGME::clearsign(string, {:armor => true, :passphrase_callback => method(:passfunc)})
139
+ end
140
+
141
+ # Exports the public key matching +keyid+ as ascii key block.
142
+ def export(keyid)
143
+ GPGME.export(keyid, :armor=>:true)
144
+ end
145
+
146
+ # Delete the public key matching +pattern+ from the public key ring of the
147
+ # list
148
+ def delete_key(key)
149
+ msg = nil
150
+ key, msg = get_key(key) if key.kind_of?(String)
151
+
152
+ if key
153
+ @ctx.delete_key(key)
154
+ return true
155
+ else
156
+ msg
157
+ end
158
+ rescue => e
159
+ return e
160
+ end
161
+
162
+ # Import +keydata+ into public key ring of the list
163
+ def add_key(keydata)
164
+ GPGME.import(keydata)
165
+ end
166
+
167
+ def add_key_from_file(keyfile)
168
+ add_key(File.read(keyfile))
169
+ end
170
+
171
+ def key_descr(key)
172
+ key.to_s.split("\n").first.split[1..2].join(' ')
173
+ end
174
+
175
+ private
176
+
177
+ 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}*" : '' }
179
+ end
180
+
181
+ def passfunc(hook, uid_hint, passphrase_info, prev_was_bad, fd)
182
+ io = IO.for_fd(fd, 'w')
183
+ io.puts @password
184
+ io.flush
185
+ end
186
+
187
+ end
188
+ end
@@ -0,0 +1,5 @@
1
+
2
+ class SchleuderError < RuntimeError
3
+ end
4
+ class NewListError < SchleuderError
5
+ end
@@ -0,0 +1,177 @@
1
+ module Schleuder
2
+ # Represents a single list: name, config, members.
3
+ class List
4
+ # Name of this list. Must match the name of the subdirectory in
5
+ # +SchleuderConfig::lists_dir+
6
+ attr_reader :listname
7
+
8
+ # Prepare some variables, set up the SchleuderLogger and set GNUPGHOME
9
+ def initialize(listname,newlist=false)
10
+ @listname = listname
11
+ @config = _load_config(false) if newlist
12
+ @log = ListLogger.new listname, listdir, config
13
+
14
+ # setting GNUPGHOME to list's home, to make use of the keys there
15
+ Schleuder.log.debug "setting ENV[GNUPGHOME] to #{listdir}"
16
+ ENV["GNUPGHOME"] = listdir
17
+ @members = nil
18
+ end
19
+
20
+ # Provides an array of Schleuder::Member's, read from +members.conf+
21
+ def members
22
+ unless @members
23
+ Schleuder.log.debug("reading #{members_file}")
24
+ @members = YAML::load_file(members_file).collect do |h|
25
+ h.kind_of?(Schleuder::Member) ? h : Schleuder::Member.new(h,false)
26
+ end
27
+ end
28
+ @members
29
+ end
30
+
31
+ def members_file
32
+ @members_file ||= File.join(listdir, Schleuder.config.lists_memberfile)
33
+ end
34
+
35
+ # Saves an array of Schleuder::Member's into +members.conf++
36
+ def members=(arr)
37
+ Schleuder.log.debug 'writing members'
38
+ Schleuder.log.info("writing #{members_file}")
39
+ @members = arr.collect { |m| m.kind_of?(Hash) ? Member.new(m,false) : m }
40
+ _write(YAML.dump(@members.collect { |m| m.to_hash }), members_file)
41
+ @members
42
+ end
43
+
44
+ # Finds a member by email address.
45
+ def find_member_by_email(addresses)
46
+ addresses = Array(addresses)
47
+ members.detect { |m| addresses.include?(m.email) } || false
48
+ end
49
+
50
+ def find_admin_by_email(addresses)
51
+ addresses = Array(addresses)
52
+ if admin_email = self.config.admins.detect { |a| addresses.include?(a.email) }
53
+ Member.new(:email => admin_email)
54
+ else
55
+ false
56
+ end
57
+ end
58
+
59
+ def find_member_by_key(key)
60
+ Schleuder.log.debug "Looking for member for key #{key}"
61
+ find_by_key(members, key)
62
+ end
63
+
64
+ def find_admin_by_key(key)
65
+ Schleuder.log.debug "Looking for admin for key #{key}"
66
+ find_by_key(config.admins, key)
67
+ end
68
+
69
+ def find_admin_by_address(address)
70
+ config.admins.detect { |admin| admin.email == address }
71
+ end
72
+
73
+ def find_member_by_address(address)
74
+ members.detect { |member| member.email == address }
75
+ end
76
+
77
+ # Provides the list config as Schleuder::ListConfig-object
78
+ def config
79
+ @config ||= _load_config
80
+ end
81
+
82
+ # Saves +data+ into the list-config-file (default: list.conf). +data+ must
83
+ # be a Schleuder::ListConfig or valid input to +ListConfig.new+
84
+ def config=(data)
85
+ Schleuder.log.info("writing list-config for: #{listname}")
86
+ if data.is_a?(ListConfig)
87
+ @config = data
88
+ else
89
+ @config = ListConfig.new(data)
90
+ end
91
+ _write(YAML::dump(@config.to_hash), File.join(listdir, Schleuder.config.lists_configfile))
92
+ @config
93
+ end
94
+
95
+ # Builds the bounce-address for the list
96
+ def bounce_addr
97
+ @bounce_addr ||= self.config.myaddr.gsub(/^(.*)@(.*)$/, '\1-bounce@\2')
98
+ end
99
+
100
+ # Builds the owner-address for the list
101
+ def owner_addr
102
+ @owner_addr ||= self.config.myaddr.gsub(/^(.*)@(.*)$/, '\1-owner@\2')
103
+ end
104
+
105
+ # Builds the request-address for the list
106
+ def request_addr
107
+ @request_addr ||= self.config.myaddr.gsub(/^(.*)@(.*)$/, '\1-request@\2')
108
+ end
109
+
110
+ # builds the send-key-command-address for the list
111
+ def sendkey_addr
112
+ self.config.myaddr.gsub(/^(.*)@(.*)$/, '\1-sendkey@\2')
113
+ end
114
+
115
+ def self.listdir(listname)
116
+ name = listname.split('@')
117
+ if name.size < 2
118
+ Schleuder.log.warn 'Listname should be a full email-address, using only the local part is deprecated. Please fix this!'
119
+ end
120
+ File.expand_path(File.join([Schleuder.config.lists_dir, name.reverse].flatten))
121
+ end
122
+
123
+ def listdir
124
+ @listdir ||= List.listdir(@listname)
125
+ end
126
+
127
+ def listid
128
+ @listid ||= config.myaddr.gsub(/@/, '.')
129
+ end
130
+
131
+ def key
132
+ @key ||= lookup_list_key
133
+ end
134
+
135
+ def key_fingerprint
136
+ key.subkeys.first.fingerprint
137
+ end
138
+
139
+ def archive(mail)
140
+ @list_archiver ||= Schleuder::Archiver.new
141
+ @list_archiver.archive(mail)
142
+ end
143
+
144
+ private
145
+
146
+ # Loads the configuration
147
+ # fromfile = Whether to load the config from file.
148
+ def _load_config(fromfile=true)
149
+ Schleuder.log.debug("reading list-config for: #{@listname}") unless Schleuder.log.nil?
150
+ @config = ListConfig.new(File.join(listdir, Schleuder.config.lists_configfile),fromfile)
151
+ end
152
+
153
+ def find_by_key(ary, key)
154
+ return false unless key.kind_of?(GPGME::Key)
155
+ res = ary.detect { |elem| elem.kind_of?(Member) && elem.uses_key?(key) }
156
+ Schleuder.log.debug "Found #{res} for #{key}" unless res.nil?
157
+ res || false
158
+ end
159
+
160
+ def _write(data,filename)
161
+ File.open(filename, 'w') { |f| f << data }
162
+ end
163
+
164
+ def lookup_list_key
165
+ key, msg = crypt.get_key(config.key_fingerprint||config.myaddr)
166
+ unless key
167
+ raise "Could not find a key for this List! Reason: #{msg}"
168
+ end
169
+ key
170
+ end
171
+
172
+ def crypt
173
+ @@crypt ||= Crypt.new(nil)
174
+ end
175
+
176
+ end
177
+ end
@@ -0,0 +1,146 @@
1
+ # the list config class - a simple container
2
+
3
+ module Schleuder
4
+ class ListConfig < Storage
5
+
6
+ # Options and their defaults
7
+ # If you want to change the defaults, edit conf/default-list.conf
8
+
9
+ # Emailaddress of the list
10
+ schleuder_attr :myaddr, ''
11
+
12
+ # Realname of this list address (mainly used for gpg key)
13
+ schleuder_attr :myname, ''
14
+
15
+ # Listadmin's emailaddress(es). Must be an array.
16
+ schleuder_attr :admins, []
17
+
18
+ # Default mime setting
19
+ schleuder_attr :default_mime, 'MIME'
20
+
21
+ # The gpg password
22
+ schleuder_attr :gpg_password, nil
23
+
24
+ # The fingerprint of the key used for this list
25
+ schleuder_attr :key_fingerprint, nil
26
+
27
+ # Wether sending emails in the clear is allowed or not.
28
+ schleuder_attr :send_encrypted_only, false
29
+
30
+ # Wether to accept only incoming emails that are encrypted
31
+ schleuder_attr :receive_encrypted_only, false
32
+
33
+ # Wether to accept only emails that are validly signed
34
+ schleuder_attr :receive_signed_only, false
35
+
36
+ # Wether to accept only emails that are validly signed by a list-member's key
37
+ schleuder_attr :receive_authenticated_only, false
38
+
39
+ # Wether to accept only emails that are validly signed by a list-admin's key
40
+ schleuder_attr :receive_admin_only, false
41
+
42
+ # Whether to accept only emails that are sent from a members address.
43
+ # NOTE: better rely on :receive_authenticated_only and ignore that option.
44
+ schleuder_attr :receive_from_member_emailaddresses_only, false
45
+
46
+ # Wether to keep the msgid or not
47
+ schleuder_attr :keep_msgid, true
48
+
49
+ # Footer for outgoing mails
50
+ schleuder_attr :public_footer, ''
51
+
52
+ # Subject prefix for incoming (signed) mails from listmembers
53
+ schleuder_attr :prefix, ''
54
+
55
+ # Subject prefix for incoming mails
56
+ schleuder_attr :prefix_in, ''
57
+
58
+ # Subject prefix for outgoing mails
59
+ schleuder_attr :prefix_out, ''
60
+
61
+ # The log_level (ERROR || WARN || INFO || DEBUG)
62
+ schleuder_attr :log_level, 'ERROR'
63
+
64
+ # Log to SYSLOG?
65
+ schleuder_attr :log_syslog, false
66
+
67
+ # Log to IO (writing into STDIN of another process/executable)
68
+ schleuder_attr :log_io, false
69
+
70
+ # Log to a file? If the path doesn't start with a slash the list-dir will
71
+ # be prefixed.
72
+ schleuder_attr :log_file, 'list.log'
73
+
74
+ # Which headers from original mail to include into the internal meta data
75
+ schleuder_attr :headers_to_meta, [:from, :to, :cc, :date]
76
+
77
+ # Restrict specific plugins to admin
78
+ schleuder_attr :keywords_admin_only, ['SAVE-MEMBERS', 'DEL-KEY']
79
+
80
+ # Notify admin if these keywords triggered commands.
81
+ schleuder_attr :keywords_admin_notify, [ 'X-ADD-KEY' ]
82
+
83
+ # Drop any bounces (incoming email not passing the receive_*_only-rules)
84
+ schleuder_attr :bounces_drop_all, false
85
+
86
+ # Drop bounces if they match one of these headers. Must be a hash, keys and values are case insensitive.
87
+ schleuder_attr :bounces_drop_on_headers, {'x-spam-flag' => 'yes'}
88
+
89
+ # Send a notice to admin(s) on bouncing or dropping
90
+ schleuder_attr :bounces_notify_admin, true
91
+
92
+ # Include RFC-compliant List-* Headers into member mails
93
+ schleuder_attr :include_list_headers, true
94
+
95
+ # Include OpenPGP-Header
96
+ schleuder_attr :include_openpgp_header, true
97
+ # Preferred way to receive emails to note in OpenPGP-Header ('sign'|'encrypt'|'signencrypt'|'unprotected'|'none')
98
+ # 'none' to not include a preference
99
+ # default: 'signencrypt'
100
+ schleuder_attr :openpgp_header_preference, 'signencrypt'
101
+
102
+ # If we want to dump the original incoming mail.
103
+ # ATTENTION: this stores the incoming e-mail on disk!
104
+ schleuder_attr :dump_incoming_mail, false
105
+
106
+ # Maximum size of message allowed on the list in kilobyte. All others will be bounced.
107
+ schleuder_attr :max_message_size, 10240 # 10MB
108
+
109
+ # Whether to archive messages sent to list members or not.
110
+ # default: false
111
+ schleuder_attr :archive, false
112
+
113
+ ### END OF CONFIG OPTIONS
114
+
115
+ def initialize(config_file, fromfile=true)
116
+ # First Overload with default-list.conf then load our config
117
+ overload_from_file!(Schleuder.config.lists_default_conf)
118
+ # overload with config_file
119
+ super(config_file, fromfile)
120
+
121
+ # load admins as members
122
+ self.admins = self.admins
123
+ # compress fingerprint
124
+ self.key_fingerprint = self.key_fingerprint
125
+ end
126
+
127
+ def key_fingerprint=(fpr)
128
+ schleuder_attributes['key_fingerprint'] = Schleuder::Utils.compress_fingerprint(fpr)
129
+ end
130
+
131
+ def admins=(ary)
132
+ schleuder_attributes['admins'] = Array(ary).collect { |mem|
133
+ if mem.kind_of?(Member)
134
+ mem
135
+ else
136
+ if mem.kind_of?(Hash) && mem.has_key?("email")
137
+ Member.new(mem)
138
+ else
139
+ Schleuder.log.error "Wrong input: #{mem.inspect} is not suitable data for a Member."
140
+ nil
141
+ end
142
+ end
143
+ }.compact
144
+ end
145
+ end
146
+ end