schleuder 2.2.4 → 3.2.2

Sign up to get free protection for your applications and to get access to all the features.
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