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
@@ -1,50 +0,0 @@
1
- module Schleuder
2
- class MetaEmailOutputter < EmailOutputter
3
- # TODO: refactor (merge?) with EmailOutputter
4
- def send_mail
5
- if @send_mail_lock
6
- self.level = Log4r::OFF # it's possible that the loglevel haven't yet been changed
7
- Schleuder.log.warn 'This is a loop in sending a mail in the MetaEmailOutputter, breaking!'
8
- return false
9
- end
10
- @send_mail_lock = true
11
- Schleuder.log.info { 'notifying superadmin' }
12
- begin
13
- mail = makeemail
14
- mail.to = Schleuder.config.superadminaddr
15
- r = Member.new('email' => Schleuder.config.superadminaddr)
16
- m = mail.individualize(r)
17
- if ! Processor.send(m, r, true, Schleuder.config.superadminaddr).first
18
- m.body = @altmsg
19
- m.content_type = 'text/plain'
20
- Processor.send(m, r, false, Schleuder.config.superadminaddr)
21
- end
22
- rescue => e
23
- Schleuder.log.warn "An error occurred while reporting an error: #{e.message}\n#{e.backtrace[0..3].join("\n")}"
24
- Schleuder.log.warn "Sending emergency notice to superadmin"
25
- mail = "From: root@localhost
26
- To: #{Schleuder.config.superadminaddr}
27
- Date: #{Time.now.strftime('%a, %d %b %Y %H:%M:%S %z')}
28
- Subject: Error
29
-
30
- Serious errors happened working for list #{Schleuder.listname}.
31
- Please check the logs!
32
- "
33
- Mailer.send mail, Schleuder.config.superadminaddr, 'root@localhost', true
34
- end
35
- Schleuder.log.info { 'Notifying done' }
36
- rescue => e
37
- # switch off logging per email, else we create loops here!
38
- self.level = Log4r::OFF
39
- if e.message.frozen?
40
- Schleuder.log.warn "A problematic error happened. I'll better dump the original message to preserve it:\n\n#{Schleuder.origmsg}"
41
- else
42
- e.message << "\nThis is very problematic, I'll better dump the original message to preserve it:\n\n#{Schleuder.origmsg}"
43
- end
44
- Schleuder.log.fatal { e }
45
- ensure
46
- @send_mail_lock = false
47
- @buff.clear
48
- end
49
- end
50
- end
@@ -1,34 +0,0 @@
1
- module Schleuder
2
- class SchleuderLogger < Log4r::Logger
3
- # Instantiates a logger to be used outside of list-contexts and sets
4
- # outputters and level.
5
- def initialize
6
- # The name 'log4r' is special: it is considered as meta-logger by Log4r
7
- # and receives log_internal()-messages.
8
- super 'log4r'
9
- # The initial log_level is inherited by all outputters.
10
- @level = eval("Log4r::#{Schleuder.config.log_level.upcase}")
11
- if Schleuder.config.log_file == 'syslog'
12
- formatter = Log4r::PatternFormatter.new(:pattern => "%M")
13
- require 'log4r/outputter/syslogoutputter'
14
- add Log4r::SyslogOutputter.new('syslog',
15
- { :level => @level,
16
- :ident => 'schleuder',
17
- :facility => "LOG_MAIL",
18
- :formatter => formatter }
19
- )
20
- else
21
- pattern = "%d Schleuder %l\t%M"
22
- formatter = Log4r::PatternFormatter.new(:pattern => pattern)
23
- add Log4r::FileOutputter.new('file',
24
- { :level => @level,
25
- :filename => Schleuder.config.log_file,
26
- :formatter => formatter }
27
- )
28
- end
29
- add MetaEmailOutputter.new('email',
30
- {:to => Schleuder.config.superadminaddr,
31
- :immediate_at => 'ERROR, FATAL'})
32
- end
33
- end
34
- end
@@ -1,873 +0,0 @@
1
- module Schleuder
2
- class Mail < TMail::Mail
3
-
4
- # Schleuder::Member's the mail shall be sent to
5
- attr_accessor :recipients
6
-
7
- # Additional data that is to be stored into internally sent
8
- # mails. Must be a Hash.
9
- attr_accessor :metadata
10
-
11
- # Indicator if the incoming mail was encrypted
12
- attr_accessor :in_encrypted
13
-
14
- # Indicator if (and by whom) the incoming mail was signed. Is +false+ or a
15
- # GPGME::Signature.
16
- attr_accessor :in_signed
17
-
18
- attr_accessor :resend_to
19
-
20
- # The incoming mail in raw form (string). Needed to verify detached
21
- # signatures (see +decrypt!+)
22
- attr_accessor :original_message
23
-
24
- # Shall we consider this email to be sent by the list-admin?
25
- attr_reader :from_admin
26
-
27
- # Shall we consider this email to be sent by a list-member?
28
- attr_reader :from_member
29
-
30
- # internal message_id we'll use for all outgoing mails
31
- @@schleuder_message_id = nil
32
-
33
- def initialize(*data)
34
- super(*data)
35
- @resend_to = []
36
- @metadata = {:resent_to => [], :notice => [], :warning => [], :error => []}
37
- end
38
-
39
- # Overwrites TMail.parse to store the original incoming data. We possibly
40
- # need that to verify pgp-signatures (TMail changes headers which
41
- # invalidates the signature)
42
- def self.parse(string)
43
- foo = super(string)
44
- foo.original_message = string
45
- foo
46
- end
47
-
48
- # Desctructivly decrypt/verify +self+.
49
- def decrypt!
50
- # Note: We don't recurse into nested Mime-parts. Only the first level
51
- # will be touched
52
- if self.multipart? && !self.type_param('protocol').nil? && self.type_param('protocol').match(/^application\/pgp.*/)
53
- Schleuder.log.debug 'mail is pgp/mime-formatted'
54
- if self.sub_type == 'signed'
55
- Schleuder.log.debug 'mail is signed but not encrypted'
56
- Schleuder.log.debug 'Parsing original_message to split content from signature'
57
-
58
- # first, identify the boundary
59
- bm = self.original_message.match(/boundary="?'?([^"';]*)/)
60
- boundary = "--" + Regexp.escape(bm[1])
61
- Schleuder.log.debug "Identified boundary: #{boundary}"
62
- # next, find the signed string between the first and the second-last boundary
63
- signed_string = self.original_message.match(/.*#{boundary}\r?\n(.*?)\r?\n#{boundary}.*?#{boundary}.*?/m)[1]
64
- # Add CRs, which probably have been stripped by the MTA
65
- signed_string.gsub!(/\n/, "\r\n") unless signed_string.include?("\r")
66
- Schleuder.log.debug "Identified signed string"
67
- # third, find the pgp-signature
68
- signature = self.original_message.match(/.*(-----BEGIN PGP SIGNATURE-----.*?-----END PGP SIGNATURE-----).*/m)[1] rescue ''
69
- Schleuder.log.debug "Identified signature"
70
- # finally, verify
71
- Schleuder.log.info "Verifying"
72
- foo, self.in_signed = self.crypt.verify(signature, signed_string)
73
- plaintext = self.parts[0].to_s
74
- elsif self.sub_type == 'encrypted'
75
- Schleuder.log.debug 'mail is encrypted (and possibly signed)'
76
- plaintext, self.in_encrypted, self.in_signed = self.crypt.decrypt(self.parts[1].body)
77
- else
78
- Schleuder.log.warn "Strange: message claims to be pgp/mime but neither to be signed nor encrypted"
79
- end
80
- # replace encrypted content with cleartext content
81
- plainmsg = Mail.parse(plaintext)
82
- #Schleuder.log.debug "plainmsg: #{plainmsg.to_s.inspect}"
83
- # test for signed content within the previously encrypted content
84
- if plainmsg._content_type_stripped == 'multipart/signed'
85
- Schleuder.log.debug "Found signed message as cleartext, recurseing once"
86
- plainmsg.original_message = plaintext
87
- plainmsg.decrypt!
88
- Schleuder.log.debug "End of recursion"
89
- self.in_signed = plainmsg.in_signed
90
- end
91
- # get headers and body(parts) into self
92
- if plainmsg.multipart?
93
- Schleuder.log.debug "plainmsg.multipart? => true"
94
- self.parts.clear
95
- plainmsg.parts.each do |p|
96
- self.parts.push(Mail.parse(p.to_s))
97
- end
98
- else
99
- Schleuder.log.debug "plainmsg.multipart? => false"
100
- self.body = plainmsg.body
101
- end
102
- self.content_type = plainmsg._content_type
103
- self.disposition = plainmsg._disposition
104
- else
105
- Schleuder.log.debug 'no pgp-mime found, looking for pgp-inline'
106
- # Do we simply push everything to crypt as that should return plain
107
- # text if it is feeded plain text? Or does crypt return only an error
108
- # if there is nothing to do for it?
109
- if self.multipart?
110
- Schleuder.log.debug 'multipart found, looking for pgp content in each part'
111
- self.parts.each do |part|
112
- _decrypt_pgp_inline(part)
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
- partsigs = self.parts.map do |p|
117
- # Can't use to_s here, gpgme bombs.
118
- p.in_signed.respond_to?(:fpr) ? p.in_signed.fpr : p.in_signed
119
- end
120
- if partsigs.uniq.size == 1
121
- self.in_signed = self.parts.first.in_signed
122
- end
123
- else
124
- Schleuder.log.debug 'single inline content found, looking for pgp content'
125
- _decrypt_pgp_inline(self)
126
- end
127
- end
128
- _parse_signature
129
- end
130
-
131
- # Destructivly encrypt and sign. +receiver+ must be a string or a Schleuder::Member
132
- def encrypt!(receiver)
133
- if receiver.kind_of?(String)
134
- receiver = Member.new({'email' => receiver})
135
- elsif ! receiver.kind_of?(Member)
136
- raise "need a Member-object as argument, got '#{receiver.inspect}'"
137
- end
138
-
139
- Schleuder.log.info "Encrypting for #{receiver.inspect}"
140
-
141
- key, msg = receiver.key
142
- if key == false
143
- Schleuder.log.warn msg.capitalize
144
- return [false, msg]
145
- elsif key.nil?
146
- Schleuder.log.warn "No public key found for '#{keystring}'"
147
- return [false, 'no public key found']
148
- end
149
-
150
- # choose pgp-variant from addresses value oder config.default_mime
151
- pgpvariant = receiver.mime || Schleuder.list.config.default_mime
152
-
153
- case pgpvariant.upcase
154
- when 'PLAIN'
155
- Schleuder.log.debug "encrypting as text/plain"
156
- if self.multipart?
157
- self.parts.each_with_index do |p,i|
158
- self.parts[i] = _encrypt_pgp_inline(p, receiver)
159
- end
160
- self.content_type = 'multipart/mixed'
161
- self.encoding = nil
162
- else
163
- foo = _encrypt_pgp_inline(self, receiver)
164
- self.body = foo.body
165
- foo.header.each do |k,v|
166
- self[k] = v.to_s rescue nil
167
- end
168
- end
169
- when 'MIME'
170
- Schleuder.log.debug "encrypting pgp-mime"
171
- gpgcontent = Mail.new
172
- if self.multipart?
173
- self.parts.each do |p|
174
- gpgcontent.parts.push(p)
175
- end
176
- else
177
- gpgcontent.body = self.body
178
- gpgcontent.content_type = self._content_type
179
- gpgcontent.charset = self.charset || 'UTF-8'
180
- end
181
-
182
- gpgcontainer = _new_part(crypt.encrypt_str(gpgcontent.to_s, receiver), 'application/octet-stream', self.charset)
183
- gpgcontainer.set_disposition('inline', {:filename => 'message.asc'})
184
-
185
- mimecontainer = _new_part("Version: 1\n", 'application/pgp-encrypted')
186
- mimecontainer.disposition = 'attachment'
187
-
188
- self.encoding = nil
189
- self.body = ""
190
- self.parts.clear
191
- self.parts.push(mimecontainer)
192
- self.parts.push(gpgcontainer)
193
- self.set_content_type('multipart', 'encrypted', {:protocol => 'application/pgp-encrypted'})
194
- self.mime_version = 1.0
195
-
196
- when 'APPL'
197
- Schleuder.log.error "mime-setting 'APPL' is deprecated, use 'MIME' instead for user '#{receiver.email}'"
198
- else
199
- Schleuder.log.error "Unknown mime-setting for user '#{receiver.email}': #{pgpvariant}"
200
- end
201
- return true
202
- end
203
-
204
- # Clearsign the message. Returns a string (raw msg, which must not be
205
- # changed anymore) or a Schleuder::Mail.
206
- def sign
207
- Schleuder.log.debug "signing message"
208
- if (self.to || []).length != 1
209
- Schleuder.log.error "I need exactly one recipient in To-header. Found this: #{self.to.inspect}"
210
- return false
211
- end
212
- member = Schleuder.list.find_member_by_email(self.to.to_s)
213
- # ugly but necessary: member might be false and member.mime might be nil
214
- pgpvariant = member.mime || Schleuder.list.config.default_mime rescue Schleuder.list.config.default_mime
215
- case pgpvariant.upcase
216
- when 'MIME'
217
- Schleuder.log.debug "signing MIME"
218
- gpgcontainer = Mail.new
219
- if self.multipart?
220
- self.parts.each do |p|
221
- if p.main_type.eql?('text')
222
- # Encode QP (there might be trailing white space
223
- p.body = p.body.to_s.split("\n").map do |line|
224
- [line].pack("M")
225
- end.join("\n")
226
- p.encoding = 'Quoted-Printable'
227
- end
228
- gpgcontainer.parts.push(p)
229
- end
230
- self.parts.clear
231
- else
232
- # Encode QP (there might be trailing white space which is illegal
233
- # according to RFC 3156).
234
- # Don't call body() as that returns the decoded string
235
- gpgcontainer.body = body.to_s.split("\n").map do |line|
236
- [line].pack("M")
237
- end.join("\n")
238
- gpgcontainer.encoding = 'Quoted-Printable'
239
- gpgcontainer.content_type = self._content_type
240
- gpgcontainer.disposition = self._disposition
241
- self.body = ''
242
- end
243
- self.encoding = ''
244
- self.parts.push gpgcontainer
245
-
246
- # This following part is quite complicated. We have to do it this way
247
- # because TMail changes the mime-boundary on every encoded() (implicit
248
- # in to_s()), which invalidates the signature
249
-
250
- # TODO: refactor with encrypt!()
251
-
252
- # First we create the signature-attachment and fill it with a dummy
253
- dummy = "dummytobereplaced-#{Time.now.to_f}"
254
- sigpart = _new_part(dummy, 'application/pgp-signature')
255
- self.parts.push(sigpart)
256
-
257
- # TODO: take care of micalg: the digest used for hashing the plaintext.
258
- # RFC 3156 requires it to be set in the content-type. ruby-gpgme
259
- # doesn't provide it to us, though.
260
- # (Don't use symbols with TMail, it expects strings.)
261
- self.set_content_type('multipart', 'signed', {'protocol' => 'application/pgp-signature', 'micalg' => 'pgp-sha1'})
262
-
263
- # Then we dump the crafted msg
264
- rawmsg = self.encoded
265
-
266
- # get mime boundary from raw mail
267
- bm = rawmsg.match(/boundary="?'?([^"';\r\n]*)/)
268
- boundary = "--" + Regexp.escape(bm[1])
269
-
270
- # Now get the to be signed string from the raw message
271
- parts = rawmsg.match(/(.*#{boundary}\r?\n)(.*\r?\n)(\r?\n#{boundary}.*?#{boundary}.*)/m)
272
-
273
- if parts
274
- # For some reason I don't manage to do this in one gsub-call... *snif*
275
- tobesigned_crlf = parts[2].gsub(/\n/, "\r\n").gsub(/\r\r/, "\r")
276
- rawmsg = "#{parts[1]}#{tobesigned_crlf}#{parts[3]}"
277
-
278
- # sign the extracted part
279
- sig = self.crypt.sign(tobesigned_crlf)
280
-
281
- # replace dummy with signature
282
- rawmsg.gsub!(/(.*)#{dummy}(.*?#{boundary}.*?)/m, "\\1#{sig}\\2")
283
- rawmsg
284
- else
285
- Schleuder.log.error "Detection of mime parts in mail for #{self.to} with boundary #{bm} failed. This should not happen. -> Cannot sign outgoing mail."
286
-
287
- false
288
- end
289
- when 'PLAIN'
290
- Schleuder.log.debug "signing PLAIN"
291
- if self.multipart?
292
- self.parts.each_with_index do |p,i|
293
- if p.disposition == 'attachment' || !p.disposition_param('filename').nil?
294
- Schleuder.log.debug "found attachment, creating detached signature"
295
- # attachment, need detached sig
296
- container = Mail.new
297
- container.parts.push Mail.parse(p.to_s)
298
-
299
- sig = crypt.sign(p.body)
300
- Schleuder.log.debug "sig: #{sig.inspect}"
301
- sigpart = _new_part(sig, 'text/plain')
302
- sigpart.set_disposition('attachment', {:filename => p.disposition_param('filename') + '.asc'})
303
- container.parts.push Mail.parse(sigpart.to_s)
304
-
305
- container.content_type = 'multipart/mixed'
306
-
307
- self.parts[i] = Mail.parse(container.to_s)
308
- else
309
- p.body = crypt.clearsign(p.body)
310
- end
311
- end
312
- else
313
- self.body = crypt.clearsign(self.body)
314
- end
315
- self
316
-
317
- else
318
- Schleuder.log.error "Strange mime-setting: #{pgpvariant}. Don't know what to do with that, ignoring it."
319
- self
320
- end
321
- end
322
-
323
- # Create a new Schleuder::Mail-instance and copy/set selected headers.
324
- def individualize(recv)
325
- Schleuder.log.debug { "Individualizing mail for #{recv.inspect}" }
326
- new = Mail.new
327
- new.crypt = crypt
328
- # add some headers we want to keep
329
- new.content_type = self['content-type'].to_s if self['content-type'] && !self['content-type'].illegal?
330
- new.disposition = self._disposition
331
- new.encoding = self.encoding.to_s
332
- new.subject = _quote_if_necessary(self.subject.to_s,'UTF-8')
333
-
334
- new.message_id = Mail._schleuder_message_id
335
-
336
- new.to = recv.email.to_s
337
- new.date = Time.now
338
- new.from = Schleuder.list.config.myaddr
339
- new = _add_openpgp_header(new) if Schleuder.list.config.include_openpgp_header
340
- if self.multipart?
341
- self.parts.each do |p|
342
- new.parts.push(Mail.parse(p.to_s))
343
- end
344
- else
345
- new.body = self.body
346
- end
347
- new
348
- end
349
-
350
- def individualize_member(recv)
351
- new = self.individualize(recv)
352
- new = _message_ids(new) if Schleuder.list.config.keep_msgid
353
- new = _list_headers(new) if Schleuder.list.config.include_list_headers
354
- new
355
- end
356
-
357
- # Strips keywords from self and stores them in @+keywords+
358
- def keywords
359
- unless @keywords
360
- @keywords = []
361
- # we're looking for keywords only in the first mime-part
362
- b = self
363
- while b.multipart?
364
- b = b.parts.first
365
- end
366
- # split to array to ease parsing
367
- a = b.body.split("\n")
368
-
369
- a.map! do |line|
370
- if line.match(/^X-.*/i)
371
- Schleuder.log.debug "found keyword-line: #{line.chomp}"
372
- key, val = $&.split(/:|: | /, 2)
373
- keyword = key.slice(2..-1).strip
374
- if !self.from_admin && Schleuder.list.config.keywords_admin_only.include?(keyword)
375
- Schleuder.log.info "Keyword '#{keyword}' is listed as admin only and mail is not from admin, skipping"
376
- self.metadata[:error] << "Keyword '#{keyword}' is configured as admin-only."
377
- # return the line to have it stay in the body
378
- line
379
- else
380
- # Split values to catch multiple values separated by comma or the like (deprecated).
381
- values = val.to_s.strip.split(/[,;]+/)
382
- values << ' ' if values.empty?
383
- values.each do |v|
384
- Schleuder.log.info "Storing keyword #{keyword} with value #{v.inspect}"
385
- @keywords << {keyword => v.to_s.strip}
386
- end
387
- nil
388
- end
389
- elsif line.chomp.empty?
390
- line
391
- else
392
- # break on the first non-empty and non-command line so we don't parse
393
- # the whole message (could be much data)
394
- break
395
- end
396
- end
397
- # delete nil's (lines formerly containing X-Commands) from array
398
- a.compact!
399
- # rejoin to string
400
- b.body = a.join("\n")
401
- # The whole procedure makes TMail parse and reformat the message, so now
402
- # it's decoded utf-8
403
- b.charset = 'UTF-8'
404
- b.encoding = nil
405
- Schleuder.log.debug "Inspecting found keywords: #{@keywords.inspect}"
406
- end
407
- @keywords
408
- end
409
-
410
- def request?
411
- @request ||= self.to.to_a.include?(Schleuder.list.request_addr)
412
- end
413
-
414
- def bounce?
415
- self.to.to_a.include?(Schleuder.list.bounce_addr) || \
416
- ( # empty Return-Path
417
- ['<>'].include?(self['Return-path'].to_s) || \
418
- ( # Auto-Submitted exists and does not equal 'no'
419
- ! ["no", ""].include?(self['Auto-Submitted'].to_s) && \
420
- self['X-Cron-Env'].nil? # cron mails are also autosubmitted
421
- )
422
- )
423
- end
424
-
425
- # +require+s all found plugins, tests SomePlugin.match and if that returns
426
- # true runs SomePlugin.process
427
- def process_plugins!
428
- if self.keywords.empty?
429
- Schleuder.log.info 'No keywords present, skipping plugins'
430
- else
431
- Schleuder.config.plugins_dir.each do |plugins_dir|
432
- if File.directory?(plugins_dir)
433
- replymsg = ""
434
- # We might want to replace the object later, which we can't do with self.
435
- mail = self
436
- plugins = {:request => [], :list => []}
437
- ptype = (self.request? ? :request : :list)
438
- used_keywords = []
439
- Plugin.signing_key(self)
440
- Dir[plugins_dir + '/*_plugin.rb'].each do |plugfile|
441
- Schleuder.log.debug "Instanciating #{plugfile} as plugin"
442
- require plugfile
443
- # interpreting class name from file name
444
- classname = File.basename(plugfile, '.rb').split('_').collect { |p| p.capitalize }.join
445
- plugin = instance_eval(classname).new
446
- Schleuder.log.debug "Storing plugin in plugin-list '#{plugin.plugin_type}'"
447
- plugins[plugin.plugin_type] << plugin
448
- end
449
- # This is neither elegant nor fast, but it's got to live only until the rewrite.
450
- Schleuder.log.debug "Processing #{ptype}-plugins"
451
- self.keywords.each do |kwhash|
452
- keyword = kwhash.keys.first
453
- value = kwhash.values.first
454
- Schleuder.log.debug "Looping for keyword #{keyword}"
455
- plugins[ptype].each do |plugin|
456
- command = keyword.downcase.gsub(/-/, '_')
457
- Schleuder.log.debug "Does #{plugin.class}.respond_to? '#{command}'"
458
- if plugin.respond_to?(command)
459
- Schleuder.log.debug "Yes it does, executing #{plugin.class}.#{command}"
460
- used_keywords << keyword
461
- foo = plugin.send(command, mail, value)
462
- case foo
463
- when String
464
- Schleuder.log.debug "Method returned a string, saving it for reply to sender."
465
- replymsg << foo << "\n\n\n"
466
- when Mail
467
- Schleuder.log.debug "Method returned a Mail-object, replacing myself by it."
468
- mail = foo
469
- else
470
- Schleuder.log.debug "Method returned '#{foo.inspect}', don't know how to handle, skipping."
471
- end
472
- next
473
- else
474
- Schleuder.log.debug "No it doesn't."
475
- end
476
- end
477
- # Generate error-message if no plugin was executed
478
- unless used_keywords.include?(keyword)
479
- msg = "No #{ptype}-plugin responded to keyword #{keyword}"
480
- Schleuder.log.debug msg
481
- if mail.request?
482
- replymsg << "Error: #{msg}.\n\n"
483
- else
484
- mail.metadata[:error] << msg
485
- end
486
- end
487
- end
488
- # Decide how to go on: reply or list?
489
- if mail.request?
490
- Schleuder.log.debug "Message is a request, sending plugins-output back to sender."
491
- if replymsg.empty?
492
- replymsg = 'The keywords you sent did not produce any output. If you feel this is an error please contact the adminisrators of this list.'.fmt
493
- end
494
- # This exits.
495
- Plugin.reply(mail, replymsg, used_keywords)
496
- end
497
- else
498
- Schleuder.log.error "#{plugins_dir} does not exist or is not readable!"
499
- end
500
- end
501
- end
502
- end
503
-
504
- def add_prefix!
505
- # only add if it's not already present
506
- unless self.subject.index(Schleuder.list.config.prefix)
507
- Schleuder.log.debug "adding prefix"
508
- self.subject = Schleuder.list.config.prefix + " " + self.subject.to_s
509
- end
510
- end
511
-
512
- def add_prefix_in!
513
- # only add if it's not already present
514
- unless self.subject.index(Schleuder.list.config.prefix_in)
515
- Schleuder.log.debug "adding prefix_in"
516
- self.subject = Schleuder.list.config.prefix_in + " " + self.subject.to_s
517
- end
518
- end
519
-
520
- def add_prefix_out!
521
- # only add if it's not already present
522
- unless self.subject.index(Schleuder.list.config.prefix_out)
523
- Schleuder.log.debug "adding prefix_out"
524
- self.subject = Schleuder.list.config.prefix_out + " " + self.subject.to_s
525
- end
526
- end
527
-
528
- def add_metadata!
529
- Schleuder.log.info "Adding meta-information on old mail to new mail"
530
- Schleuder.log.debug 'Generating meta-information'
531
- meta = ''
532
- Schleuder.list.config.headers_to_meta.each do |h|
533
- h = h.to_s.capitalize
534
- val = self.header_string(h) rescue '(not parsable)'
535
- next if val.nil?
536
- val = TMail::Unquoter.unquote_and_convert_to(val,'utf-8')
537
- meta << "#{h}: #{val}\n"
538
- end
539
- meta << "Enc: #{_enc_str @in_encrypted}\n"
540
- meta << "Sig: #{_sig_str @in_signed}\n"
541
- if self.multipart?
542
- self.parts.each_with_index do |p,i|
543
- unless p.in_encrypted.nil? && p.in_signed.nil?
544
- meta << "part #{i+1}:\n"
545
- meta << " enc: #{_enc_str p.in_encrypted}\n"
546
- meta << " sig: #{_sig_str p.in_signed}\n"
547
- end
548
- end
549
- end
550
-
551
- Schleuder.log.debug 'Adding extra meta data'
552
- @metadata.each do |name, content|
553
- meta << "#{name.to_s.gsub(/_/, '-').capitalize}: #{(content.join('; ') || content.to_s)}\n" unless content.empty?
554
- end
555
- meta << "\n"
556
-
557
- # insert oder prepend to the message
558
- if self.first_part._content_type_stripped == 'text/plain'
559
- Schleuder.log.debug "Glueing meta data into first part of message"
560
- self.first_part.body = meta + self.first_part.body
561
- # body is now utf-8 and decoded!
562
- self.first_part.charset = 'UTF-8'
563
- self.first_part.encoding = ''
564
- else
565
- # make the message multipart and prepend the meta-part
566
- self.to_multipart! unless self.multipart?
567
- Schleuder.log.debug "Prepending meta data as own mime part to message"
568
- self.parts.unshift _new_part(meta, 'text/plain', 'UTF-8')
569
- end
570
-
571
- end
572
-
573
- # Adds Schleuder::ListConfig.public_footer to the end of the body of self
574
- # or the body of the first mimepart (if one of those is text/plain) or
575
- # appends it as a new mimepart.
576
- def add_public_footer!
577
- if Schleuder.list.config.public_footer.strip.empty?
578
- return false
579
- end
580
- Schleuder.log.debug "appending public footer"
581
- footer = "\n\n-- \n#{Schleuder.list.config.public_footer}\n"
582
-
583
- if self.first_part._content_type_stripped == 'text/plain'
584
- self.first_part.body = self.first_part.body.to_s + footer
585
- else
586
- self.to_multipart! unless self.multipart?
587
- self.parts.push _new_part(footer, 'text/plain', 'UTF-8')
588
- end
589
- end
590
-
591
-
592
- def to_multipart!
593
- # skip if already multipart
594
- return false if self.multipart?
595
- Schleuder.log.debug "Making message multipart"
596
- # else move the body into a mime-part
597
- p = _new_part(self.body, self['content-type'].to_s, self.charset, self.encoding)
598
- p.disposition = self._disposition
599
- self.parts.push p
600
- self.body = ''
601
- self.set_content_type 'multipart', 'mixed', {:boundary => TMail.new_boundary}
602
- self.disposition = ''
603
- end
604
-
605
- def first_part
606
- if self.multipart?
607
- self.parts.first
608
- else
609
- self
610
- end
611
- end
612
-
613
- def _content_type(default = 'text/plain')
614
- (self['content-type'] || default).to_s rescue default
615
- end
616
-
617
- def _content_type_stripped(default = nil)
618
- ( default && _content_type(default) || _content_type() ).to_s.split(';').first
619
- end
620
-
621
- def _disposition
622
- (self['content-disposition'] || '').to_s rescue ''
623
- end
624
-
625
- def from_member_address?
626
- from.all? { |a| Schleuder.list.find_member_by_address(a) }
627
- end
628
-
629
- def from_admin_address?
630
- from.all? { |a| Schleuder.list.find_admin_by_address(a) }
631
- end
632
-
633
- # An instance of Schleuder::Crypt
634
- attr_writer :crypt
635
- def crypt
636
- @crypt ||= Crypt.new(Schleuder.list.config.gpg_password)
637
- end
638
-
639
- private
640
-
641
- def _decrypt_pgp_inline(msg)
642
- if msg._content_type_stripped == 'text/html'
643
- Schleuder.log.debug "Content-type is text/html, can't handle that, skipping"
644
- elsif msg.body =~ /^-----BEGIN PGP.*/ || ['pgp', 'gpg'].include?(msg.disposition_param('filename').to_s.split('.').last.to_s.downcase)
645
- Schleuder.log.debug 'found pgp-inline in input'
646
- # We need to do this in three steps:
647
- # 1. get the decoded body from TMail
648
- # 2. delete the encoding-header
649
- # 3. put the plaintext back into TMail
650
- # Else TMail either can't decode the body correctly or 'over-decodes' it
651
- # on next output
652
- # TMail decodes QP if body() is called, Umlauts if to_s() is called.
653
- # Life with TMail is hard...
654
- if msg.encoding.to_s.downcase.eql?('quoted-printable') || !msg.main_type.eql?('text')
655
- str = msg.body
656
- else
657
- str = msg.to_s
658
- end
659
- ptxt, enc, sig = crypt.decrypt(str)
660
- if ptxt == str
661
- Schleuder.log.debug "Output equals input, content was obviously not suitable for gpg. Passing it untouched."
662
- else
663
- #Schleuder.log.debug "ptxt.mime_encoding: #{FileMagic.fm(:mime_encoding).buffer(ptxt).inspect}"
664
- Schleuder.log.debug "Processing plaincontent: #{ptxt.mime}"
665
- msg.in_encrypted = enc
666
- msg.in_signed = sig
667
- if ! msg.disposition_param('filename').nil?
668
- msg['content-disposition'].params['filename'] = msg.disposition_param('filename').gsub(/\.(gpg|pgp|asc)$/, '')
669
- end
670
- if ptxt.content_type.split('/').first == 'text'
671
- msg['content-type'] = ptxt.mime
672
- msg.encoding = nil
673
- msg.body = ptxt
674
- else
675
- # TMail handles binary data in an "interesting" way, so we manuall encode before handing Tmail the data.
676
- msg['content-type'] = ptxt.content_type
677
- msg.encoding = 'Base64'
678
- msg.body = [ptxt].pack('m')
679
- end
680
- end
681
- msg
682
- else
683
- Schleuder.log.debug 'no pgp-inline-data found, doing nothing'
684
- end
685
- end
686
-
687
- def _encrypt_pgp_inline(msg, receiver)
688
- msg.body = crypt.encrypt_str(msg.body.to_s, receiver)
689
- # reset encoding, TMail has converted the content
690
- msg.encoding = nil
691
- # if attachment add '.gpg'-suffix to attachments file names
692
- # (The query for 'filename' is a work around against buggy client
693
- # formatting that doesn't disposition attachments as attachments. This
694
- # variant works ok.)
695
- unless msg.disposition_param('filename').nil?
696
- msg.set_content_type('application', 'octet-stream', {'x-action' => 'pgp-encrypted'})
697
- msg.set_disposition('attachment', {:filename => msg.disposition_param('filename') + '.gpg'})
698
- else
699
- msg.set_content_type('text', 'plain', {'x-action' => 'pgp-encrypted'})
700
- msg.charset = 'UTF-8'
701
- end
702
- msg
703
- end
704
-
705
- def _enc_str(arg)
706
- arg ? 'encrypted' : 'unencrypted'
707
- end
708
-
709
- def _sig_str(arg)
710
- # gpgme breaks if we use arg.to_s.empty? here.
711
- if arg.nil? || (arg.is_a?(String) && arg.empty?)
712
- 'No signature'
713
- else
714
- if crypt.get_key(arg.fpr).first
715
- arg.to_s
716
- else
717
- #Schleuder.log.debug arg.inspect
718
- "Unknown signature from #{arg.fpr} (public key not present)"
719
- end
720
- end
721
- end
722
-
723
- # Tests if the signature of the incoming mail (if any) belongs to a
724
- # list-member or an admin by testing all gpg-key-ids of the signing
725
- # key for matches
726
- def _parse_signature
727
- @from_admin = @from_member = false
728
-
729
- Schleuder.log.debug 'Testing for valid signature'
730
- if !in_signed
731
- Schleuder.log.info 'No signature found'
732
- elsif in_signed.status != 0
733
- Schleuder.log.info 'Invalid or unknown signature found'
734
- else
735
- Schleuder.log.info 'Valid signature found'
736
- key, msg = crypt.get_key(in_signed.fpr)
737
- if key
738
- Schleuder.log.debug "Testing key for matching some member's or admin's keys"
739
- if Schleuder.list.find_admin_by_key(key)
740
- @from_admin = true
741
- end
742
- if Schleuder.list.find_member_by_key(key)
743
- @from_member = true
744
- end
745
- else
746
- Schleuder.log.debug "Keylookup for signature failed! Reason: #{msg}"
747
- end
748
- end
749
- end
750
-
751
- # Convert the given text into quoted printable format, with an instruction
752
- # that the text be eventually interpreted in the given charset.
753
- def _quoted_printable(text, charset)
754
- text = text.gsub( /[^a-z ]/i ) { _quoted_printable_encode($&) }.
755
- gsub( / /, "_" )
756
- "=?#{charset}?Q?#{text}?="
757
- end
758
-
759
- # Convert the given character to quoted printable format, taking into
760
- # account multi-byte characters (if executing with $KCODE="u", for instance)
761
- def _quoted_printable_encode(character)
762
- result = ""
763
- character.each_byte { |b| result << "=%02X" % b }
764
- result
765
- end
766
-
767
- # A quick-and-dirty regexp for determining whether a string contains any
768
- # characters that need escaping.
769
- if !defined?(CHARS_NEEDING_QUOTING)
770
- CHARS_NEEDING_QUOTING = /[\000-\011\013\014\016-\037\177-\377]/
771
- end
772
-
773
- # Quote the given text if it contains any "illegal" characters
774
- def _quote_if_necessary(text, charset)
775
- text = text.dup.force_encoding(Encoding::ASCII_8BIT) if text.respond_to?(:force_encoding)
776
-
777
- (text =~ CHARS_NEEDING_QUOTING) ?
778
- _quoted_printable(text, charset) :
779
- text
780
- end
781
-
782
- # Helper for creating new message-parts
783
- def _new_part(body, content_type, charset='', encoding='')
784
- p = Mail.new
785
- p.body = body.to_s
786
- p.content_type = content_type
787
- p.charset = charset.to_s unless charset.to_s.empty?
788
- p.encoding = encoding.to_s unless encoding.to_s.empty?
789
- p
790
- end
791
-
792
- def _collect_message_ids(ids)
793
- return nil if ids.nil? || !ids.is_a?(Array) || ids.empty?
794
- ids.select{ |id| Utils.schleuder_id?(id, Schleuder.list.listid) }
795
- end
796
-
797
- def self._schleuder_message_id
798
- @@schleuder_message_id = Utils.generate_message_id(Schleuder.list.listid) unless @@schleuder_message_id
799
- @@schleuder_message_id
800
- end
801
-
802
- def _message_ids(mail)
803
- Schleuder.log.debug "Copying msgid to in-reply-to/references"
804
- mail.in_reply_to = _collect_message_ids(self.in_reply_to)
805
- mail.references = _collect_message_ids(self.references)
806
- mail
807
- end
808
-
809
- def _list_headers(mail)
810
- Schleuder.log.debug "Generating list-ids"
811
- mail['List-Id'] = "<#{Schleuder.list.listid}>"
812
- mail['List-Owner'] = _list_owner
813
- mail['List-Post'] = _list_post
814
- # TODO: adapt URL to version.
815
- mail['List-Help'] = '<https://schleuder2.nadir.org/documentation.html>'
816
- mail
817
- end
818
-
819
- def _list_owner
820
- "<mailto:#{Schleuder.list.owner_addr}> (Use list's public key)"
821
- end
822
-
823
- def _list_post
824
- if Schleuder.list.config.receive_admin_only
825
- "NO (Admins only)"
826
- elsif Schleuder.list.config.receive_authenticated_only
827
- "<mailto:#{Schleuder.list.config.myaddr}> (Subscribers only)"
828
- else
829
- "<mailto:#{Schleuder.list.config.myaddr}>"
830
- end
831
- end
832
-
833
- def _add_openpgp_header(mail)
834
- Schleuder.log.debug "Add OpenPGP-Headers"
835
- mail['OpenPGP'] = "id=#{Schleuder.list.key_fingerprint} "+
836
- "(Send an email to #{Schleuder.list.sendkey_addr} to receive the public-key)"+
837
- _gen_openpgp_pref_header
838
- mail
839
- end
840
-
841
- def _gen_openpgp_pref_header
842
- unless Schleuder.list.config.openpgp_header_preference == 'none'
843
- pref_str = "; preference=#{Schleuder.list.config.openpgp_header_preference} ("
844
- if Schleuder.list.config.receive_admin_only
845
- pref_str << 'Only encrypted and signed emails by list-admins are accepted'
846
- elsif !Schleuder.list.config.receive_authenticated_only
847
- if Schleuder.list.config.receive_encrypted_only \
848
- && Schleuder.list.config.receive_signed_only
849
- pref_str << 'Only encrypted and signed emails are accepted'
850
- elsif Schleuder.list.config.receive_encrypted_only \
851
- && !Schleuder.list.config.receive_signed_only
852
- pref_str << 'Only encrypted emails are accepted'
853
- elsif !Schleuder.list.config.receive_encrypted_only \
854
- && Schleuder.list.config.receive_signed_only
855
- pref_str << 'Only signed emails are accepted'
856
- else
857
- pref_str << 'All kind of emails are accepted'
858
- end
859
- elsif Schleuder.list.config.receive_authenticated_only
860
- if Schleuder.list.config.receive_encrypted_only
861
- pref_str << 'Only encrypted and signed emails by list-members are accepted'
862
- else
863
- pref_str << 'Only signed emails by list-members are accepted'
864
- end
865
- else
866
- pref_str << 'All kind of emails are accepted'
867
- end
868
- pref_str << ')'
869
- end
870
- pref_str || ''
871
- end
872
- end
873
- end