schleuder 2.2.4 → 3.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.
Files changed (141) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +138 -0
  3. data/Rakefile +136 -0
  4. data/bin/pinentry-clearpassphrase +72 -0
  5. data/bin/schleuder +9 -89
  6. data/bin/schleuder-api-daemon +4 -0
  7. data/db/migrate/20140501103532_create_lists.rb +39 -0
  8. data/db/migrate/20140501112859_create_subscriptions.rb +21 -0
  9. data/db/migrate/201508092100_add_language_to_lists.rb +11 -0
  10. data/db/migrate/20150812165700_change_keywords_admin_only_defaults.rb +8 -0
  11. data/db/migrate/20150813235800_add_forward_all_incoming_to_admins.rb +11 -0
  12. data/db/migrate/201508141727_change_send_encrypted_only_default.rb +8 -0
  13. data/db/migrate/201508222143_add_logfiles_to_keep_to_lists.rb +11 -0
  14. data/db/migrate/201508261723_rename_delivery_disabled_to_delivery_enabled_and_change_default.rb +14 -0
  15. data/db/migrate/201508261815_strip_gpg_passphrase.rb +11 -0
  16. data/db/migrate/201508261827_remove_default_mime.rb +9 -0
  17. data/db/migrate/20160501172700_fix_headers_to_meta_defaults.rb +8 -0
  18. data/db/migrate/20170713215059_add_internal_footer_to_list.rb +11 -0
  19. data/db/schema.rb +62 -0
  20. data/etc/init.d/schleuder-api-daemon +87 -0
  21. data/etc/list-defaults.yml +123 -0
  22. data/etc/postfix/schleuder_sqlite.cf +28 -0
  23. data/etc/schleuder-api-daemon.service +10 -0
  24. data/etc/schleuder.cron.weekly +6 -0
  25. data/etc/schleuder.yml +61 -0
  26. data/lib/schleuder-api-daemon.rb +420 -0
  27. data/lib/schleuder.rb +81 -47
  28. data/lib/schleuder/cli.rb +334 -0
  29. data/lib/schleuder/cli/cert.rb +24 -0
  30. data/lib/schleuder/cli/schleuder_cert_manager.rb +84 -0
  31. data/lib/schleuder/cli/subcommand_fix.rb +11 -0
  32. data/lib/schleuder/conf.rb +131 -0
  33. data/lib/schleuder/errors/active_model_error.rb +15 -0
  34. data/lib/schleuder/errors/base.rb +17 -0
  35. data/lib/schleuder/errors/decryption_failed.rb +16 -0
  36. data/lib/schleuder/errors/fatal_error.rb +13 -0
  37. data/lib/schleuder/errors/file_not_found.rb +14 -0
  38. data/lib/schleuder/errors/invalid_listname.rb +13 -0
  39. data/lib/schleuder/errors/key_adduid_failed.rb +13 -0
  40. data/lib/schleuder/errors/key_generation_failed.rb +16 -0
  41. data/lib/schleuder/errors/keyword_admin_only.rb +13 -0
  42. data/lib/schleuder/errors/list_exists.rb +13 -0
  43. data/lib/schleuder/errors/list_not_found.rb +14 -0
  44. data/lib/schleuder/errors/list_property_missing.rb +14 -0
  45. data/lib/schleuder/errors/listdir_problem.rb +16 -0
  46. data/lib/schleuder/errors/loading_list_settings_failed.rb +14 -0
  47. data/lib/schleuder/errors/message_empty.rb +14 -0
  48. data/lib/schleuder/errors/message_not_from_admin.rb +13 -0
  49. data/lib/schleuder/errors/message_sender_not_subscribed.rb +13 -0
  50. data/lib/schleuder/errors/message_too_big.rb +14 -0
  51. data/lib/schleuder/errors/message_unauthenticated.rb +13 -0
  52. data/lib/schleuder/errors/message_unencrypted.rb +13 -0
  53. data/lib/schleuder/errors/message_unsigned.rb +13 -0
  54. data/lib/schleuder/errors/standard_error.rb +5 -0
  55. data/lib/schleuder/errors/too_many_keys.rb +17 -0
  56. data/lib/schleuder/errors/unknown_list_option.rb +14 -0
  57. data/lib/schleuder/filters/auth_filter.rb +39 -0
  58. data/lib/schleuder/filters/bounces_filter.rb +12 -0
  59. data/lib/schleuder/filters/forward_filter.rb +17 -0
  60. data/lib/schleuder/filters/forward_incoming.rb +13 -0
  61. data/lib/schleuder/filters/hotmail_message_filter.rb +25 -0
  62. data/lib/schleuder/filters/max_message_size.rb +14 -0
  63. data/lib/schleuder/filters/request_filter.rb +26 -0
  64. data/lib/schleuder/filters/send_key_filter.rb +20 -0
  65. data/lib/schleuder/filters/strip_alternative_filter.rb +21 -0
  66. data/lib/schleuder/filters_runner.rb +83 -0
  67. data/lib/schleuder/gpgme/ctx.rb +274 -0
  68. data/lib/schleuder/gpgme/import_status.rb +27 -0
  69. data/lib/schleuder/gpgme/key.rb +212 -0
  70. data/lib/schleuder/gpgme/sub_key.rb +13 -0
  71. data/lib/schleuder/gpgme/user_id.rb +22 -0
  72. data/lib/schleuder/list.rb +318 -127
  73. data/lib/schleuder/list_builder.rb +139 -0
  74. data/lib/schleuder/listlogger.rb +31 -0
  75. data/lib/schleuder/logger.rb +23 -0
  76. data/lib/schleuder/logger_notifications.rb +69 -0
  77. data/lib/schleuder/mail/message.rb +482 -0
  78. data/lib/schleuder/mail/parts_list.rb +9 -0
  79. data/lib/schleuder/plugin_runners/base.rb +91 -0
  80. data/lib/schleuder/plugin_runners/list_plugins_runner.rb +24 -0
  81. data/lib/schleuder/plugin_runners/request_plugins_runner.rb +27 -0
  82. data/lib/schleuder/plugins/attach_listkey.rb +17 -0
  83. data/lib/schleuder/plugins/get_version.rb +7 -0
  84. data/lib/schleuder/plugins/key_management.rb +113 -0
  85. data/lib/schleuder/plugins/list_management.rb +15 -0
  86. data/lib/schleuder/plugins/resend.rb +196 -0
  87. data/lib/schleuder/plugins/sign_this.rb +46 -0
  88. data/lib/schleuder/plugins/subscription_management.rb +140 -0
  89. data/lib/schleuder/runner.rb +130 -0
  90. data/lib/schleuder/subscription.rb +98 -0
  91. data/lib/schleuder/validators/boolean_validator.rb +7 -0
  92. data/lib/schleuder/validators/email_validator.rb +7 -0
  93. data/lib/schleuder/validators/fingerprint_validator.rb +7 -0
  94. data/lib/schleuder/validators/greater_than_zero_validator.rb +7 -0
  95. data/lib/schleuder/validators/no_line_breaks_validator.rb +7 -0
  96. data/lib/schleuder/version.rb +1 -1
  97. data/locales/de.yml +179 -0
  98. data/locales/en.yml +179 -0
  99. metadata +305 -108
  100. checksums.yaml.gz.sig +0 -3
  101. data.tar.gz.sig +0 -2
  102. data/LICENSE +0 -339
  103. data/README +0 -32
  104. data/bin/schleuder-fix-gem-dependencies +0 -37
  105. data/bin/schleuder-init-setup +0 -37
  106. data/bin/schleuder-migrate-v2.1-to-v2.2 +0 -225
  107. data/bin/schleuder-newlist +0 -413
  108. data/contrib/check-expired-keys.rb +0 -60
  109. data/contrib/mutt-schleuder-colors.rc +0 -10
  110. data/contrib/mutt-schleuder-resend.vim +0 -24
  111. data/contrib/smtpserver.rb +0 -76
  112. data/ext/default-list.conf +0 -149
  113. data/ext/default-members.conf +0 -7
  114. data/ext/list.conf.example +0 -14
  115. data/ext/schleuder.conf +0 -64
  116. data/lib/schleuder/archiver.rb +0 -46
  117. data/lib/schleuder/crypt.rb +0 -210
  118. data/lib/schleuder/errors.rb +0 -5
  119. data/lib/schleuder/list_config.rb +0 -146
  120. data/lib/schleuder/log/listlogger.rb +0 -57
  121. data/lib/schleuder/log/outputter/emailoutputter.rb +0 -120
  122. data/lib/schleuder/log/outputter/metaemailoutputter.rb +0 -50
  123. data/lib/schleuder/log/schleuderlogger.rb +0 -34
  124. data/lib/schleuder/mail.rb +0 -873
  125. data/lib/schleuder/mailer.rb +0 -26
  126. data/lib/schleuder/member.rb +0 -69
  127. data/lib/schleuder/plugin.rb +0 -54
  128. data/lib/schleuder/processor.rb +0 -363
  129. data/lib/schleuder/schleuder_config.rb +0 -75
  130. data/lib/schleuder/storage.rb +0 -84
  131. data/lib/schleuder/utils.rb +0 -80
  132. data/man/schleuder-newlist.8 +0 -174
  133. data/man/schleuder.8 +0 -416
  134. data/plugins/README +0 -20
  135. data/plugins/manage_keys_plugin.rb +0 -113
  136. data/plugins/manage_members_plugin.rb +0 -156
  137. data/plugins/manage_self_plugin.rb +0 -26
  138. data/plugins/resend_plugin.rb +0 -35
  139. data/plugins/sign_this_plugin.rb +0 -14
  140. data/plugins/version_plugin.rb +0 -12
  141. metadata.gz.sig +0 -0
@@ -0,0 +1,139 @@
1
+ module Schleuder
2
+ class ListBuilder
3
+ def initialize(list_attributes, adminemail=nil, adminfingerprint=nil, adminkey=nil)
4
+ @list_attributes = list_attributes.with_indifferent_access
5
+ @listname = list_attributes[:email]
6
+ @fingerprint = list_attributes[:fingerprint]
7
+ @adminemail = adminemail
8
+ @adminfingerprint = adminfingerprint
9
+ @adminkey = adminkey
10
+ end
11
+
12
+ def read_default_settings
13
+ hash = YAML.load_file(ENV['SCHLEUDER_LIST_DEFAULTS'])
14
+ if ! hash.kind_of?(Hash)
15
+ raise Errors::LoadingListSettingsFailed.new
16
+ end
17
+ hash
18
+ rescue Psych::SyntaxError
19
+ raise Errors::LoadingListSettingsFailed.new
20
+ end
21
+
22
+ def run
23
+ Schleuder.logger.info "Building new list"
24
+
25
+ if @listname.blank? || ! @listname.match(Conf::EMAIL_REGEXP)
26
+ return [nil, {'email' => ["'#{@listname}' is not a valid email address"]}]
27
+ end
28
+
29
+ settings = read_default_settings.merge(@list_attributes)
30
+ list = List.new(settings)
31
+
32
+ @list_dir = list.listdir
33
+ create_or_test_dir(@list_dir)
34
+ # In case listlogs_dir != lists_dir we have to create the basedir of the
35
+ # list's log-file.
36
+ create_or_test_dir(File.dirname(list.logfile))
37
+
38
+ if list.fingerprint.blank?
39
+ list_key = gpg.keys("<#{list.email}>").first
40
+ if list_key.nil?
41
+ list_key = create_key(list)
42
+ end
43
+ list.fingerprint = list_key.fingerprint
44
+ end
45
+
46
+ if ! list.valid?
47
+ return list
48
+ end
49
+
50
+ list.save!
51
+
52
+ if @adminemail.blank?
53
+ msg = nil
54
+ else
55
+ sub, msg = list.subscribe(@adminemail, @adminfingerprint, true, true, @adminkey)
56
+ if sub.errors.present?
57
+ raise Errors::ActiveModelError.new(sub.errors)
58
+ end
59
+ end
60
+
61
+ [list, msg]
62
+ end
63
+
64
+ def gpg
65
+ @gpg_ctx ||= begin
66
+ ENV["GNUPGHOME"] = @list_dir
67
+ GPGME::Ctx.new
68
+ end
69
+ end
70
+
71
+ def create_key(list)
72
+ Schleuder.logger.info "Generating key-pair, this could take a while..."
73
+ gpg.generate_key(key_params(list))
74
+
75
+ # Get key without knowing the fingerprint yet.
76
+ keys = list.keys(@listname)
77
+ if keys.empty?
78
+ raise Errors::KeyGenerationFailed.new(@list_dir, @listname)
79
+ elsif keys.size > 1
80
+ raise Errors::TooManyKeys.new(@list_dir, @listname)
81
+ else
82
+ adduids(list, keys.first)
83
+ end
84
+
85
+ keys.first
86
+ end
87
+
88
+ def adduids(list, key)
89
+ # Add UIDs for -owner and -request.
90
+ [list.request_address, list.owner_address].each do |address|
91
+ err = key.adduid(list.email, address)
92
+ if err.present?
93
+ raise err
94
+ end
95
+ end
96
+ # Go through list.key() to re-fetch the key from the keyring, otherwise
97
+ # we don't see the new UIDs.
98
+ errors = list.key.set_primary_uid(list.email)
99
+ if errors.present?
100
+ raise errors
101
+ end
102
+ rescue => exc
103
+ raise Errors::KeyAdduidFailed.new(exc.to_s)
104
+ end
105
+
106
+ def key_params(list)
107
+ "
108
+ <GnupgKeyParms format=\"internal\">
109
+ Key-Type: RSA
110
+ Key-Length: 4096
111
+ Key-Usage: sign
112
+ Subkey-Type: RSA
113
+ Subkey-Length: 4096
114
+ Subkey-Usage: encrypt
115
+ Name-Real: #{list.email}
116
+ Name-Email: #{list.email}
117
+ Expire-Date: 0
118
+ %no-protection
119
+ </GnupgKeyParms>
120
+
121
+ "
122
+ end
123
+
124
+ def create_or_test_dir(dir)
125
+ if File.exists?(dir)
126
+ if ! File.directory?(dir)
127
+ raise Errors::ListdirProblem.new(dir, :not_a_directory)
128
+ end
129
+
130
+ if ! File.writable?(dir)
131
+ raise Errors::ListdirProblem.new(dir, :not_writable)
132
+ end
133
+ else
134
+ FileUtils.mkdir_p(dir)
135
+ end
136
+ end
137
+
138
+ end
139
+ end
@@ -0,0 +1,31 @@
1
+ module Schleuder
2
+ class Listlogger < ::Logger
3
+ include LoggerNotifications
4
+ def initialize(list)
5
+ super(list.logfile, 'daily')
6
+ @from = list.email
7
+ @list = list
8
+ @adminaddresses = list.admins.map { |sub| [sub.email, sub.key] }
9
+ @level = ::Logger.const_get(list.log_level.upcase)
10
+ remove_old_logfiles(list)
11
+ end
12
+
13
+ # Logger rotates but doesn't delete older files, so we're helping
14
+ # ourselves.
15
+ def remove_old_logfiles(list)
16
+ logfiles_to_keep = list.logfiles_to_keep.to_i
17
+ if logfiles_to_keep < 1
18
+ logfiles_to_keep = list.class.column_defaults['logfiles_to_keep']
19
+ end
20
+ suffix_now = Time.now.strftime("%Y%m%d").to_i
21
+ del_older_than = suffix_now - logfiles_to_keep
22
+ Pathname.glob("#{list.logfile}.????????").each do |file|
23
+ if file.basename.to_s.match(/\.([0-9]{8})$/)
24
+ if del_older_than.to_i >= $1.to_i
25
+ file.unlink
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,23 @@
1
+ module Schleuder
2
+ def logger
3
+ @logger ||= Logger.new
4
+ end
5
+ module_function :logger
6
+
7
+ class Logger < Syslog::Logger
8
+ include LoggerNotifications
9
+ def initialize
10
+ if RUBY_VERSION.to_f < 2.1
11
+ super('Schleuder')
12
+ else
13
+ super('Schleuder', Syslog::LOG_MAIL)
14
+ end
15
+ # We need some sender-address different from the superadmin-address.
16
+ @from = "#{`whoami`.chomp}@#{`hostname`.chomp}"
17
+ @adminaddresses = Conf.superadmin
18
+ @level = ::Logger.const_get(Conf.log_level.upcase)
19
+ end
20
+ end
21
+
22
+ end
23
+
@@ -0,0 +1,69 @@
1
+ module Schleuder
2
+ module LoggerNotifications
3
+ def adminaddresses
4
+ @adminaddresses.presence || superadmin
5
+ end
6
+
7
+ def superadmin
8
+ Conf.superadmin.presence
9
+ end
10
+
11
+ def error(string)
12
+ super(string)
13
+ notify_admin(string)
14
+ end
15
+
16
+ def fatal(string, original_message=nil)
17
+ super(string.to_s + append_original_message(original_message))
18
+ notify_admin(string, original_message)
19
+ end
20
+
21
+ def notify_admin(thing, original_message=nil, subject='Error')
22
+ # Minimize using other classes here, we don't know what caused the error.
23
+ msg_parts = convert_to_msg_parts(thing, original_message)
24
+ Array(adminaddresses).each do |address, key|
25
+ mail = Mail.new
26
+ mail.from = @from
27
+ mail.to = address
28
+ mail.subject = subject
29
+ mail[:Errors_To] = superadmin
30
+ mail.sender = superadmin
31
+ msg_parts.each do |msg_part|
32
+ mail.add_part(msg_part)
33
+ end
34
+ if @list.present?
35
+ gpg_opts = @list.gpg_sign_options
36
+ if key.present? && key.usable?
37
+ gpg_opts.merge!(encrypt: true, keys: { address => key.fingerprint })
38
+ end
39
+ mail.gpg gpg_opts
40
+ end
41
+ mail.deliver
42
+ end
43
+ true
44
+ end
45
+
46
+ private
47
+
48
+ def convert_to_msg_parts(thing, original_message)
49
+ msg_parts = Mail::Message.all_to_message_part(thing)
50
+ if original_message.present?
51
+ orig_part = Mail::Part.new
52
+ orig_part.content_type = 'message/rfc822'
53
+ orig_part.content_description = 'The originally incoming message'
54
+ orig_part.body = original_message.to_s
55
+ msg_parts << orig_part
56
+ end
57
+ msg_parts
58
+ end
59
+
60
+ def append_original_message(original_message)
61
+ if original_message
62
+ "\n\nOriginal message:\n\n#{original_message.to_s}"
63
+ else
64
+ ''
65
+ end
66
+ end
67
+ end
68
+ end
69
+
@@ -0,0 +1,482 @@
1
+ module Mail
2
+ # creates a Mail::Message likes schleuder
3
+ def self.create_message_to_list(msg, recipient, list)
4
+ mail = Mail.new(msg)
5
+ mail.list = list
6
+ mail.recipient = recipient
7
+ # don't freeze here, as the mail might not be fully
8
+ # parsed as body is lazy evaluated and might still
9
+ # be changed later.
10
+ mail.original_message = mail.dup #.freeze
11
+ mail
12
+ end
13
+
14
+ # TODO: Test if subclassing breaks integration of mail-gpg.
15
+ class Message
16
+ attr_accessor :recipient
17
+ attr_accessor :original_message
18
+ attr_accessor :list
19
+
20
+ # TODO: This should be in initialize(), but I couldn't understand the
21
+ # strange errors about wrong number of arguments when overriding
22
+ # Message#initialize.
23
+ def setup
24
+ if self.encrypted?
25
+ new = self.decrypt(verify: true)
26
+ ## Work around a bug in mail-gpg: when decrypting pgp/mime the
27
+ ## Date-header is not copied.
28
+ #new.date ||= self.date
29
+ # Test if there's a signed multipart inside the ciphertext
30
+ # ("encapsulated" format of pgp/mime).
31
+ if new.signed?
32
+ new = new.verify
33
+ end
34
+ elsif self.signed?
35
+ new = self.verify
36
+ else
37
+ new = self
38
+ end
39
+
40
+ new.list = self.list
41
+ new.recipient = self.recipient
42
+
43
+ new.gpg list.gpg_sign_options
44
+ new.original_message = self.dup.freeze
45
+ # Trigger method early to save the information. Later some information
46
+ # might be gone (e.g. request-keywords that delete subscriptions or
47
+ # keys).
48
+ new.signer
49
+ self.dynamic_pseudoheaders.each do |str|
50
+ new.add_pseudoheader(str)
51
+ end
52
+ new
53
+ end
54
+
55
+ def clean_copy(with_pseudoheaders=false)
56
+ clean = Mail.new
57
+ clean.list = self.list
58
+ clean.gpg self.list.gpg_sign_options
59
+ clean.from = list.email
60
+ clean.subject = self.subject
61
+
62
+ clean.add_msgids(list, self)
63
+ clean.add_list_headers(list)
64
+ clean.add_openpgp_headers(list)
65
+
66
+ if with_pseudoheaders
67
+ new_part = Mail::Part.new
68
+ new_part.body = self.pseudoheaders(list)
69
+ clean.add_part new_part
70
+ end
71
+
72
+ # Attach body or mime-parts in a new wrapper-part, to preserve the
73
+ # original mime-structure.
74
+ # We can't use self.to_s here — that includes all the headers we *don't*
75
+ # want to copy.
76
+ wrapper_part = Mail::Part.new
77
+ # Copy headers to are relevant for the mime-structure.
78
+ wrapper_part.content_type = self.content_type
79
+ wrapper_part.content_transfer_encoding = self.content_transfer_encoding if self.content_transfer_encoding
80
+ wrapper_part.content_disposition = self.content_disposition if self.content_disposition
81
+ wrapper_part.content_description = self.content_description if self.content_description
82
+ # Copy contents.
83
+ if self.multipart?
84
+ self.parts.each do |part|
85
+ wrapper_part.add_part(part)
86
+ end
87
+ else
88
+ # We copied the content-headers, so we need to copy the body encoded.
89
+ # Otherwise the content might become unlegible.
90
+ wrapper_part.body = self.body.encoded
91
+ end
92
+ clean.add_part(wrapper_part)
93
+
94
+ clean
95
+ end
96
+
97
+ def prepend_part(part)
98
+ self.add_part(part)
99
+ self.parts.unshift(parts.delete_at(parts.size-1))
100
+ end
101
+
102
+ def add_public_footer!
103
+ # Add public_footer unless it's empty?.
104
+ add_footer!(:public_footer)
105
+ end
106
+
107
+ def add_internal_footer!
108
+ add_footer!(:internal_footer)
109
+ end
110
+
111
+ def was_encrypted?
112
+ Mail::Gpg.encrypted?(original_message)
113
+ end
114
+
115
+ def signature
116
+ case signatures.size
117
+ when 0
118
+ if multipart?
119
+ signature_multipart_inline
120
+ else
121
+ nil
122
+ end
123
+ when 1
124
+ signatures.first
125
+ else
126
+ raise "Multiple signatures found! Cannot handle!"
127
+ end
128
+ end
129
+
130
+ def was_validly_signed?
131
+ signature.present? && signature.valid? && signer.present?
132
+ end
133
+
134
+ def signer
135
+ @signer ||= begin
136
+ if signing_key.present?
137
+ list.subscriptions.where(fingerprint: signing_key.fingerprint).first
138
+ end
139
+ end
140
+ end
141
+
142
+ # The fingerprint of the signature might be the one of a sub-key, but the
143
+ # subscription-assigned fingerprints are (should be) the ones of the
144
+ # primary keys, so we need to look up the key.
145
+ def signing_key
146
+ if signature.present?
147
+ @signing_key ||= list.keys(signature.fpr).first
148
+ end
149
+ end
150
+
151
+ def reply_to_signer(output)
152
+ reply = self.reply
153
+ self.class.all_to_message_part(output).each do |part|
154
+ reply.add_part(part)
155
+ end
156
+ self.signer.send_mail(reply)
157
+ end
158
+
159
+ def self.all_to_message_part(input)
160
+ Array(input).map do |thing|
161
+ case thing
162
+ when Mail::Part
163
+ thing
164
+ when String, StandardError
165
+ Mail::Part.new do
166
+ body thing.to_s
167
+ end
168
+ else
169
+ raise "Don't know how to handle input: #{thing.inspect}"
170
+ end
171
+ end
172
+ end
173
+
174
+ def sendkey_request?
175
+ @recipient.match(/-sendkey@/)
176
+ end
177
+
178
+ def to_owner?
179
+ @recipient.match(/-owner@/)
180
+ end
181
+
182
+ def request?
183
+ @recipient.match(/-request@/)
184
+ end
185
+
186
+ def automated_message?
187
+ @recipient.match(/-bounce@/).present? ||
188
+ # Empty Return-Path
189
+ self.return_path.to_s == '<>' ||
190
+ # Auto-Submitted exists and does not equal 'no' and no cron header
191
+ # present, as cron emails have the auto-submitted header.
192
+ ( self['Auto-Submitted'].present? && \
193
+ self['Auto-Submitted'].to_s.downcase != 'no' && \
194
+ !self['X-Cron-Env'].present?)
195
+ end
196
+
197
+ def keywords
198
+ return @keywords if @keywords
199
+
200
+ part = first_plaintext_part
201
+ if part.blank?
202
+ return []
203
+ end
204
+
205
+ @keywords = []
206
+ lines = part.decoded.lines.map do |line|
207
+ # TODO: Find multiline arguments (add-key). Currently add-key has to
208
+ # read the whole body and hope for the best.
209
+ if line.match(/^x-([^:\s]*)[:\s]*(.*)/i)
210
+ command = $1.strip.downcase
211
+ arguments = $2.to_s.strip.downcase.split(/[,; ]{1,}/)
212
+ @keywords << [command, arguments]
213
+ nil
214
+ else
215
+ line
216
+ end
217
+ end
218
+
219
+ # Work around problems with re-encoding the body. If we delete the
220
+ # content-transfer-encoding prior to re-assigning the body, and let Mail
221
+ # decide itself how to encode, it works. If we don't, some
222
+ # character-sequences are not properly re-encoded.
223
+ part.content_transfer_encoding = nil
224
+ # Make the converted strings (now UTF-8) match what mime-part's headers say,
225
+ # fall back to US-ASCII if none is set.
226
+ # https://tools.ietf.org/html/rfc2046#section-4.1.2
227
+ # -> Default charset is US-ASCII
228
+ part.body = lines.compact.join.encode(part.charset||'US-ASCII')
229
+
230
+ @keywords
231
+ end
232
+
233
+ def add_subject_prefix!
234
+ _add_subject_prefix(nil)
235
+ end
236
+
237
+ def add_subject_prefix_in!
238
+ _add_subject_prefix(:in)
239
+ end
240
+
241
+ def add_subject_prefix_out!
242
+ _add_subject_prefix(:out)
243
+ end
244
+
245
+ def add_pseudoheader(string_or_key, value=nil)
246
+ @dynamic_pseudoheaders ||= []
247
+ if value.present?
248
+ @dynamic_pseudoheaders << make_pseudoheader(string_or_key, value)
249
+ else
250
+ @dynamic_pseudoheaders << string_or_key.to_s
251
+ end
252
+ end
253
+
254
+ def make_pseudoheader(key, value)
255
+ "#{key.to_s.camelize}: #{value.to_s}"
256
+ end
257
+
258
+ def dynamic_pseudoheaders
259
+ @dynamic_pseudoheaders || []
260
+ end
261
+
262
+ def standard_pseudoheaders(list)
263
+ if @standard_pseudoheaders.present?
264
+ return @standard_pseudoheaders
265
+ else
266
+ @standard_pseudoheaders = []
267
+ end
268
+
269
+ Array(list.headers_to_meta).each do |field|
270
+ @standard_pseudoheaders << make_pseudoheader(field.to_s, self.header[field.to_s])
271
+ end
272
+
273
+ # Careful to add information about the incoming signature. GPGME
274
+ # throws exceptions if it doesn't know the key.
275
+ if self.signature.present?
276
+ # Some versions of gpgme return nil if the key is unknown, so we check
277
+ # for that manually and provide our own fallback. (Calling
278
+ # `signature.key` results in an EOFError in that case.)
279
+ if signing_key.present?
280
+ msg = signature.to_s
281
+ else
282
+ # TODO: I18n
283
+ msg = "Unknown signature by unknown key 0x#{self.signature.fingerprint}"
284
+ end
285
+ else
286
+ # TODO: I18n
287
+ msg = "Unsigned"
288
+ end
289
+ @standard_pseudoheaders << make_pseudoheader(:sig, msg)
290
+
291
+ # TODO: I18n
292
+ @standard_pseudoheaders << make_pseudoheader(
293
+ :enc,
294
+ was_encrypted? ? 'Encrypted' : 'Unencrypted'
295
+ )
296
+
297
+ @standard_pseudoheaders
298
+ end
299
+
300
+ def pseudoheaders(list)
301
+ (standard_pseudoheaders(list) + dynamic_pseudoheaders).flatten.join("\n") + "\n"
302
+ end
303
+
304
+ def add_msgids(list, orig)
305
+ if list.keep_msgid
306
+ # Don't use `orig['in-reply-to']` here, because that sometimes fails to
307
+ # parse the original value and then returns it without the
308
+ # angle-brackets.
309
+ self.message_id = clutch_anglebrackets(orig.message_id)
310
+ self.in_reply_to = clutch_anglebrackets(orig.in_reply_to)
311
+ self.references = clutch_anglebrackets(orig.references)
312
+ end
313
+ end
314
+
315
+ def add_list_headers(list)
316
+ if list.include_list_headers
317
+ self['List-Id'] = "<#{list.email.gsub('@', '.')}>"
318
+ self['List-Owner'] = "<mailto:#{list.owner_address}> (Use list's public key)"
319
+ self['List-Help'] = '<https://schleuder.nadir.org/>'
320
+
321
+ postmsg = if list.receive_admin_only
322
+ "NO (Admins only)"
323
+ elsif list.receive_authenticated_only
324
+ "<mailto:#{list.email}> (Subscribers only)"
325
+ else
326
+ "<mailto:#{list.email}>"
327
+ end
328
+
329
+ self['List-Post'] = postmsg
330
+ end
331
+ end
332
+
333
+ def add_openpgp_headers(list)
334
+ if list.include_openpgp_header
335
+
336
+ if list.openpgp_header_preference == 'none'
337
+ pref = ''
338
+ else
339
+ pref = "preference=#{list.openpgp_header_preference}"
340
+
341
+ # TODO: simplify.
342
+ pref << ' ('
343
+ if list.receive_admin_only
344
+ pref << 'Only encrypted and signed emails by list-admins are accepted'
345
+ elsif ! list.receive_authenticated_only
346
+ if list.receive_encrypted_only && list.receive_signed_only
347
+ pref << 'Only encrypted and signed emails are accepted'
348
+ elsif list.receive_encrypted_only && ! list.receive_signed_only
349
+ pref << 'Only encrypted emails are accepted'
350
+ elsif ! list.receive_encrypted_only && list.receive_signed_only
351
+ pref << 'Only signed emails are accepted'
352
+ else
353
+ pref << 'All kind of emails are accepted'
354
+ end
355
+ elsif list.receive_authenticated_only
356
+ if list.receive_encrypted_only
357
+ pref << 'Only encrypted and signed emails by subscribers are accepted'
358
+ else
359
+ pref << 'Only signed emails by subscribers are accepted'
360
+ end
361
+ else
362
+ pref << 'All kind of emails are accepted'
363
+ end
364
+ pref << ')'
365
+ end
366
+
367
+ fingerprint = list.fingerprint
368
+ comment = "(Send an email to #{list.sendkey_address} to receive the public-key)"
369
+
370
+ self['OpenPGP'] = "id=0x#{fingerprint} #{comment}; #{pref}"
371
+ end
372
+ end
373
+
374
+ def empty?
375
+ if self.multipart?
376
+ if self.parts.empty?
377
+ return true
378
+ else
379
+ # Test parts recursively. E.g. Thunderbird with activated
380
+ # memoryhole-headers send nested parts that might still be empty.
381
+ return parts.inject(true) { |result, part| result && part.empty? }
382
+ end
383
+ else
384
+ return self.body.empty?
385
+ end
386
+ end
387
+
388
+ def first_plaintext_part(part=nil)
389
+ part ||= self
390
+ if part.multipart?
391
+ first_plaintext_part(part.parts.first)
392
+ elsif part.mime_type == 'text/plain'
393
+ part
394
+ else
395
+ nil
396
+ end
397
+ end
398
+
399
+
400
+ def attach_list_key!(list)
401
+ filename = "#{list.email}.asc"
402
+ self.add_file({
403
+ filename: filename,
404
+ content: list.export_key
405
+ })
406
+ self.attachments[filename].content_type = 'application/pgp-keys'
407
+ self.attachments[filename].content_description = 'OpenPGP public key'
408
+ true
409
+ end
410
+
411
+ private
412
+
413
+
414
+ def add_footer!(footer_attribute)
415
+ if self.list.blank? || self.list.send(footer_attribute).to_s.empty?
416
+ return
417
+ end
418
+ footer_part = Mail::Part.new
419
+ footer_part.body = self.list.send(footer_attribute).to_s
420
+ if wrapped_single_text_part?
421
+ self.parts.first.add_part footer_part
422
+ else
423
+ self.add_part footer_part
424
+ end
425
+ end
426
+
427
+ def wrapped_single_text_part?
428
+ parts.size == 1 &&
429
+ parts.first.mime_type == 'multipart/mixed' &&
430
+ parts.first.parts.size == 1 &&
431
+ parts.first.parts.first.mime_type == 'text/plain'
432
+ end
433
+
434
+ def _add_subject_prefix(suffix)
435
+ attrib = "subject_prefix"
436
+ if suffix
437
+ attrib << "_#{suffix}"
438
+ end
439
+ if ! self.list.respond_to?(attrib)
440
+ return false
441
+ end
442
+
443
+ string = self.list.send(attrib).to_s.strip
444
+ if ! string.empty?
445
+ prefix = "#{string} "
446
+ # Only insert prefix if it's not present already.
447
+ if self.subject.nil?
448
+ self.subject = string
449
+ elsif ! self.subject.include?(prefix)
450
+ self.subject = "#{prefix}#{self.subject}"
451
+ end
452
+ end
453
+ end
454
+
455
+ # Looking for signatures in each part. They are not aggregated into the main part.
456
+ # We only return the signature if all parts are validly signed by the same key.
457
+ def signature_multipart_inline
458
+ fingerprints = parts.map do |part|
459
+ if part.signature_valid?
460
+ part.signature.fpr
461
+ else
462
+ nil
463
+ end
464
+ end
465
+ if fingerprints.uniq.size == 1
466
+ parts.first.signature
467
+ else
468
+ nil
469
+ end
470
+ end
471
+
472
+ def clutch_anglebrackets(input)
473
+ Array(input).map do |string|
474
+ if string.first == '<'
475
+ string
476
+ else
477
+ "<#{string}>"
478
+ end
479
+ end.join(' ')
480
+ end
481
+ end
482
+ end