mailit 2009.08

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/AUTHORS ADDED
@@ -0,0 +1,6 @@
1
+ Following persons have contributed to mailit.
2
+ (Sorted by number of submitted patches, then alphabetically)
3
+
4
+ 15 Michael Fellinger <mf@rubyists.com>
5
+ 2 Jake Douglas <jakecdouglas@gmail.com>
6
+ 1 Kevin Berry <kb@rubyists.com>
@@ -0,0 +1,76 @@
1
+ [78c844d | 2009-08-25 21:40:40 UTC] Michael Fellinger <m.fellinger@gmail.com>
2
+
3
+ * Another fix
4
+
5
+ [fb25c19 | 2009-08-25 21:40:01 UTC] Michael Fellinger <m.fellinger@gmail.com>
6
+
7
+ * Version 2009.08
8
+
9
+ [2e6d77a | 2009-08-25 21:38:53 UTC] Michael Fellinger <m.fellinger@gmail.com>
10
+
11
+ * Fix Rakefile
12
+
13
+ [585554d | 2009-08-25 05:11:42 UTC] Kevin Berry <kevin@opensourcealchemist.com>
14
+
15
+ * Take in Pistos's patch, make my own patch to get running for current project, and add seedling structure.
16
+
17
+ [db73002 | 2009-08-06 06:50:35 UTC] Jake Douglas <jakecdouglas@gmail.com>
18
+
19
+ * NetSMTP and EventMachine version both actually send mail now. Previous configuration didn't override the #send method correctly so the EM methods weren't loaded. The EM module may only be included now, not used with extend.
20
+
21
+ Signed-off-by: Michael Fellinger <m.fellinger@gmail.com>
22
+
23
+ [9a36e2f | 2009-08-06 05:39:26 UTC] Jake Douglas <jakecdouglas@gmail.com>
24
+
25
+ * get the value of :noop from the options hash in NetSmtp#send
26
+
27
+ Signed-off-by: Michael Fellinger <m.fellinger@gmail.com>
28
+
29
+ [c6da60b | 2009-06-08 13:52:33 UTC] Michael Fellinger <m.fellinger@gmail.com>
30
+
31
+ * Release new version
32
+
33
+ [185cf45 | 2009-06-08 13:51:07 UTC] Michael Fellinger <m.fellinger@gmail.com>
34
+
35
+ * Fix long outstanding issues
36
+
37
+ [e429f3b | 2009-04-12 13:38:16 UTC] Michael Fellinger <m.fellinger@gmail.com>
38
+
39
+ * Major refactor of Mailer, add support for EventMachine::SmtpClient and remove render_send
40
+
41
+ [341d76a | 2009-04-12 13:23:33 UTC] Michael Fellinger <m.fellinger@gmail.com>
42
+
43
+ * Make Mail#header_string and Mail#body_string public
44
+
45
+ [5716124 | 2009-03-15 14:26:27 UTC] Michael Fellinger <m.fellinger@gmail.com>
46
+
47
+ * Add auth_type and username options for Mailer#send, should work on windows now
48
+
49
+ [ff38041 | 2009-03-10 21:02:58 UTC] Michael Fellinger <m.fellinger@gmail.com>
50
+
51
+ * Add gemspec
52
+
53
+ [07b92d0 | 2009-03-10 20:49:36 UTC] Michael Fellinger <m.fellinger@gmail.com>
54
+
55
+ * Add Mailit::Mime - detects and uses available mime providers
56
+
57
+ [584398c | 2009-03-10 20:48:49 UTC] Michael Fellinger <m.fellinger@gmail.com>
58
+
59
+ * Little improvment to style
60
+
61
+ [194c990 | 2009-03-10 20:19:08 UTC] Michael Fellinger <m.fellinger@gmail.com>
62
+
63
+ * Add warning if no mime-type detection is possible
64
+
65
+ [184b20d | 2009-03-10 20:18:54 UTC] Michael Fellinger <m.fellinger@gmail.com>
66
+
67
+ * Add Version
68
+
69
+ [74e9ca3 | 2009-03-10 20:18:35 UTC] Michael Fellinger <m.fellinger@gmail.com>
70
+
71
+ * Add readme
72
+
73
+ [0a4bcad | 2009-03-10 20:02:28 UTC] Michael Fellinger <m.fellinger@gmail.com>
74
+
75
+ * Initial commit
76
+
@@ -0,0 +1,28 @@
1
+ AUTHORS
2
+ CHANGELOG
3
+ MANIFEST
4
+ README.md
5
+ Rakefile
6
+ lib/mailit.rb
7
+ lib/mailit/mail.rb
8
+ lib/mailit/mailer.rb
9
+ lib/mailit/mime.rb
10
+ lib/mailit/version.rb
11
+ lib/version.rb
12
+ mailit.gemspec
13
+ spec/helper.rb
14
+ spec/mailit/mail.rb
15
+ spec/mailit/mailer.rb
16
+ tasks/authors.rake
17
+ tasks/bacon.rake
18
+ tasks/changelog.rake
19
+ tasks/copyright.rake
20
+ tasks/gem.rake
21
+ tasks/gem_installer.rake
22
+ tasks/install_dependencies.rake
23
+ tasks/manifest.rake
24
+ tasks/rcov.rake
25
+ tasks/release.rake
26
+ tasks/reversion.rake
27
+ tasks/setup.rake
28
+ tasks/yard.rake
@@ -0,0 +1,63 @@
1
+ # Mailit
2
+
3
+ Mailit is a simple to use library to create and send MIME compliant e-mail with
4
+ attachments and various encodings.
5
+
6
+ This is a fork of MailFactory and provides a mostly identical API but has been
7
+ cleaned up, simplified, and made compliant to common Ruby idioms. I would like
8
+ to thank David Powers for the original MailFactory, it served me well for many
9
+ years.
10
+
11
+ Copyright (c) 2005-2008 David Powers.
12
+ Copyright (c) 2009 Michael Fellinger.
13
+
14
+ This program is free software. You can re-distribute and/or modify this program
15
+ under the same terms as Ruby itself.
16
+
17
+
18
+ ## Dependencies
19
+
20
+ Any Ruby since 1.8.4 should work.
21
+ Mailit can use the Rack or Mime::Types libraries to determine the mime-type of
22
+ attachments automatically, but they are optional.
23
+
24
+
25
+ ## Usage of Mailit::Mail
26
+
27
+ require 'net/smtp'
28
+ require 'mailit'
29
+
30
+ mail = Mailit::Mail.new
31
+ mail.to = 'test@test.com'
32
+ mail.from = 'sender@sender.com'
33
+ mail.subject 'Here are some files for you!'
34
+ mail.text = 'This is what people with plain text mail readers will see'
35
+ mail.html = "A little something <b>special</b> for people with HTML readers'
36
+ mail.attach('/etc/fstab')
37
+ mail.attach('/home/manveru/.vimrc')
38
+
39
+ puts mail
40
+
41
+
42
+ ## Usage of Mailit::Mailer
43
+
44
+ Using the mail variable from above example
45
+
46
+ mailer = Mailit::Mailer.new
47
+
48
+ mailer.send(mail, :server => 'smtp.example.com', :port => 25,
49
+ :domain => 'example.com', :password => 'foo')
50
+
51
+
52
+ ## Todo:
53
+
54
+ MailFactory has a method_missing that handles getting and setting of arbitrary
55
+ headers.
56
+ I went for the less magical #[] and #[]= methods, maybe someone can add the
57
+ MailFactory behaviour.
58
+
59
+ ## Thanks to
60
+
61
+ * Michael Thompson (AKA:nylon)
62
+
63
+ Making mailer work on windows
@@ -0,0 +1,84 @@
1
+ begin; require 'rubygems'; rescue LoadError; end
2
+
3
+ require 'rake'
4
+ require 'rake/clean'
5
+ require 'rake/gempackagetask'
6
+ require 'time'
7
+ require 'date'
8
+ require './lib/mailit'
9
+
10
+ PROJECT_SPECS = FileList[
11
+ 'spec/*/**/*.rb'
12
+ ]
13
+
14
+ PROJECT_MODULE = 'Mailit'
15
+ PROJECT_README = 'README'
16
+ #PROJECT_RUBYFORGE_GROUP_ID = 3034
17
+ PROJECT_COPYRIGHT_SUMMARY = [
18
+ "# Copyright (c) 2008-#{Time.now.year} The Rubyists, LLC (effortless systems) <rubyists@rubyists.com>",
19
+ "# Distributed under the terms of the MIT license.",
20
+ "# See the LICENSE file which accompanies this software for the full text",
21
+ "#"
22
+ ]
23
+ PROJECT_COPYRIGHT = PROJECT_COPYRIGHT_SUMMARY + [
24
+ "# Permission is hereby granted, free of charge, to any person obtaining a copy",
25
+ '# of this software and associated documentation files (the "Software"), to deal',
26
+ "# in the Software without restriction, including without limitation the rights",
27
+ "# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell",
28
+ "# copies of the Software, and to permit persons to whom the Software is",
29
+ "# furnished to do so, subject to the following conditions:",
30
+ "#",
31
+ "# The above copyright notice and this permission notice shall be included in",
32
+ "# all copies or substantial portions of the Software.",
33
+ "#",
34
+ '# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR',
35
+ "# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,",
36
+ "# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE",
37
+ "# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER",
38
+ "# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,",
39
+ "# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN",
40
+ "# THE SOFTWARE."
41
+ ]
42
+
43
+ PROJECT_VERSION =
44
+ if version = ENV['PROJECT_VERSION'] || ENV['VERSION']
45
+ version
46
+ else
47
+ ::VERSION rescue Date.today.strftime("%Y.%m.%d")
48
+ end
49
+
50
+ # To release the monthly version do:
51
+ # $ PROJECT_VERSION=2009.03 rake release
52
+
53
+ PROJECT_FILES = FileList[`git ls-files`.split("\n")].exclude('.gitignore')
54
+
55
+ GEMSPEC = Gem::Specification.new{|s|
56
+ s.name = "mailit"
57
+ s.author = "Kevin Berry"
58
+ s.summary = "The Mailit library, by Kevin Berry"
59
+ s.description = "The Mailit library, by Kevin Berry"
60
+ s.email = "kevinberry@nrs.us"
61
+ s.homepage = "http://github.com/manveru/mailit"
62
+ s.platform = Gem::Platform::RUBY
63
+ s.version = PROJECT_VERSION
64
+ s.files = PROJECT_FILES
65
+ s.has_rdoc = true
66
+ s.require_path = "lib"
67
+ }
68
+
69
+ Dir.glob('tasks/*.rake'){|f| import(f) }
70
+
71
+ task :default => [:bacon]
72
+
73
+ CLEAN.include %w[
74
+ **/.*.sw?
75
+ *.gem
76
+ .config
77
+ **/*~
78
+ **/{data.db,cache.yaml}
79
+ *.yaml
80
+ pkg
81
+ rdoc
82
+ ydoc
83
+ *coverage*
84
+ ]
@@ -0,0 +1,8 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+
3
+ module Mailit
4
+ end
5
+
6
+ require 'mailit/mime'
7
+ require 'mailit/mail'
8
+ require 'mailit/mailer'
@@ -0,0 +1,367 @@
1
+ require 'time'
2
+ require 'pathname'
3
+ require 'enumerator' unless 'String'.respond_to?(:enum_for)
4
+
5
+ module Mailit
6
+
7
+ # = Overview:
8
+ #
9
+ # A simple to use class to generate RFC compliant MIME email.
10
+ #
11
+ # MailIt is a fork of MailFactory and provides a mostly identical API but has
12
+ # been cleaned up, simplified, and made compliant to common Ruby idioms.
13
+ #
14
+ # Copyright (c) 2005-2008 David Powers.
15
+ # Copyright (c) 2009 Michael Fellinger.
16
+ #
17
+ # This program is free software. You can re-distribute and/or
18
+ # modify this program under the same terms as Ruby itself.
19
+ #
20
+ # = Usage:
21
+ #
22
+ # require 'net/smtp'
23
+ # require 'mailit'
24
+ #
25
+ # mail = Mailit::Mail.new
26
+ # mail.to = 'test@test.com'
27
+ # mail.from = 'sender@sender.com'
28
+ # mail.subject 'Here are some files for you!'
29
+ # mail.text = 'This is what people with plain text mail readers will see'
30
+ # mail.html = "A little something <b>special</b> for people with HTML readers'
31
+ # mail.attach('/etc/fstab')
32
+ # mail.attach('/home/manveru/.vimrc')
33
+ #
34
+ # server = 'smtp1.testmailer.com'
35
+ # port = 25
36
+ # domain = 'mail.from.domain'
37
+ # password = 'foo'
38
+ #
39
+ # Net::SMTP.start(server, port, domain, mail.from, password, :cram_md5) do |smtp|
40
+ # smtp.send_message(mail.to_s, mail.from, mail.to)
41
+ # end
42
+ #
43
+ # = Todo:
44
+ #
45
+ # * MailFactory has a method_missing that handles getting and setting of
46
+ # arbitrary headers.
47
+ # I went for the less magical #[] and #[]= methods.
48
+ # Maybe someone can add the MailFactory behaviour.
49
+ class Mail
50
+ VERSION = '2009.03.02'
51
+
52
+ BOUNDARY_CHARS = [*'a'..'z'] + [*'A'..'Z'] + [*'0'..'9'] + ['.', '_']
53
+ BOUNDARY_PREFIX = "----=_NextPart_"
54
+
55
+ # body_boundary, encoding
56
+ BODY_BOUNDARY = "--%s\r\nContent-Type: %s\r\nContent-Transfer-Encoding: %s"
57
+
58
+ # attachment_boundary, mimetype, filename, filename
59
+ ATTACHMENT_BOUNDARY = "--%s\r\nContent-Type: %s; name=%p\r\nContent-Transfer-Encoding: base64\r\nContent-Disposition: inline; filename=%p"
60
+
61
+ HTML_BODY = <<BODY.strip
62
+ <html>
63
+ <head>
64
+ <meta http-equiv="Content-Type" content="text/html; charset=%s">
65
+ </head>
66
+ <body bgcolor="#ffffff" text="#000000">
67
+ %s
68
+ </body>
69
+ </html>
70
+ BODY
71
+
72
+ OPTIONS = {
73
+ :date => true,
74
+ :message_id => lambda{|mail|
75
+ time = Time.now
76
+ domain = mail['from'].first.to_s.split('@').last || 'localhost'
77
+ message_id = "<%f.%d.%d@%s>" % [time, $$, time.object_id, domain]
78
+ }
79
+ }
80
+
81
+
82
+ attr_accessor :charset, :text, :html, :attachment_boundary, :body_boundary
83
+ attr_reader :headers, :attachments
84
+
85
+ # Create an instance of {Mailit::Mailer}.
86
+ #
87
+ # @option options [String] :to
88
+ # @option options [String] :from
89
+ # @option options [String] :subject
90
+ # @option options [String] :text
91
+ # @option options [String] :html
92
+ # @author manveru
93
+ def initialize(options = {})
94
+ @headers = []
95
+ @attachments = []
96
+ @attachment_boundary = self.class.generate_boundary
97
+ @body_boundary = self.class.generate_boundary
98
+ @charset = 'utf-8'
99
+ @html = @text = nil
100
+
101
+ options.each{|key, value| __send__("#{key}=", value) }
102
+ end
103
+
104
+ def construct(options = {})
105
+ options = OPTIONS.merge(options)
106
+ time = Time.now
107
+
108
+ if message_id = options[:message_id]
109
+ self['Message-ID'] = message_id.call(self) unless self['Message-Id'].any?
110
+ end
111
+
112
+ if options[:date]
113
+ self['Date'] = time.rfc2822 unless self['Date'].any?
114
+ end
115
+
116
+ if multipart?
117
+ self['MIME-Version'] = '1.0' unless self['MIME-Version'].any?
118
+
119
+ unless self['Content-Type'].any?
120
+ if @attachments.any?
121
+ content_type = ('multipart/alternative; boundary=%p' % body_boundary)
122
+ else
123
+ content_type = ('multipart/mixed; boundary=%p' % attachment_boundary)
124
+ end
125
+
126
+ self['Content-Type'] = content_type
127
+ end
128
+ end
129
+
130
+ "#{header_string}#{body_string}"
131
+ end
132
+ alias to_s construct
133
+
134
+ ## Attachments
135
+
136
+ def add_attachment(filename, mimetype = nil, headers = nil)
137
+ container = {
138
+ :filename => Pathname.new(filename).basename,
139
+ :mimetype => (mimetype || mime_type_for(filename)),
140
+ }
141
+
142
+ add_attachment_common(container, filename, headers)
143
+ end
144
+ alias attach add_attachment
145
+
146
+ def add_attachment_as(file, filename, mimetype = nil, headers = nil)
147
+ container = {
148
+ :filename => filename,
149
+ :mimetype => (mimetype || mime_type_for(file))
150
+ }
151
+
152
+ add_attachment_common(container, file, headers)
153
+ end
154
+ alias attach_as add_attachment_as
155
+
156
+ ## Shortcuts
157
+
158
+ def multipart?
159
+ html || attachments.size > 0
160
+ end
161
+
162
+ def html=(html)
163
+ @html = HTML_BODY % [charset, html]
164
+ end
165
+
166
+ def raw_html=(html)
167
+ @html = html
168
+ end
169
+
170
+ def message_id=(id)
171
+ self['Message-ID'] = id
172
+ end
173
+
174
+ def send(options = {})
175
+ Mailer.send(self, options)
176
+ end
177
+
178
+ def defer_send(options = {})
179
+ Mailer.defer_send(self, options)
180
+ end
181
+
182
+ ## Header handling
183
+
184
+ def add_header(header, value)
185
+ case header.to_s.downcase
186
+ when /^subject$/i
187
+ value = quoted_printable_with_instruction(value)
188
+ when /^(from|to|bcc|reply-to)$/i
189
+ value = quote_address_if_necessary(value, charset)
190
+ end
191
+
192
+ @headers << [header, value]
193
+ end
194
+
195
+ def set_header(header, value)
196
+ remove_header(header)
197
+ add_header(header, value)
198
+ end
199
+ alias []= set_header
200
+
201
+ def remove_header(header)
202
+ regex = /^#{Regexp.escape(header)}/i
203
+
204
+ @headers.reject!{|key, value| key =~ regex }
205
+ end
206
+
207
+ def get_header(header)
208
+ regex = /^#{Regexp.escape(header)}/i
209
+
210
+ @headers.map{|key, value| value if regex =~ key }.compact
211
+ end
212
+ alias [] get_header
213
+
214
+ def header_string
215
+ headers.map{|key,value| "#{key}: #{value}"}.join("\r\n") << "\r\n\r\n"
216
+ end
217
+
218
+ MIME_INDICATOR = "This is a multi-part message in MIME format.\r\n\r\n--%s\r\nContent-Type: multipart/alternative; boundary=%p"
219
+
220
+ def body_string
221
+ return text unless multipart?
222
+
223
+ body = [ MIME_INDICATOR % [attachment_boundary, body_boundary] ]
224
+ body << build_body_boundary("text/plain; charset=#{charset} format=flowed")
225
+ body << "\r\n\r\n" << quote_if_necessary(text, charset)
226
+
227
+ if html
228
+ body << build_body_boundary("text/html; charset=#{charset}")
229
+ body << "\r\n\r\n" << quote_if_necessary(html, charset)
230
+ end
231
+
232
+ body << "--#{body_boundary}--"
233
+
234
+ attachments.each do |attachment|
235
+ body << build_attachment_boundary(attachment)
236
+ body << "\r\n\r\n" << attachment[:attachment]
237
+ body << "\r\n--#{attachment_boundary}--"
238
+ end
239
+
240
+ body.join("\r\n\r\n")
241
+ end
242
+
243
+ private
244
+
245
+ def add_attachment_common(container, file, headers)
246
+ container[:attachment] = file_read(file)
247
+ container[:headers] = headers_prepare(headers)
248
+ self.attachments << container
249
+ end
250
+
251
+ def headers_prepare(headers)
252
+ case headers
253
+ when Array
254
+ container[:headers] = headers
255
+ else
256
+ container[:headers] = headers.split(/\r?\n/)
257
+ end
258
+ end
259
+
260
+ def quoted_printable_with_instruction(text)
261
+ text = encode_quoted_printable_rfc2047(text)
262
+ "=?#{charset}?Q?#{text}?="
263
+ end
264
+
265
+ def quote_if_necessary(text, charset, instruction = false)
266
+ return unless text
267
+
268
+ if text.respond_to?(:force_encoding)
269
+ text = text.dup.force_encoding(Encoding::ASCII_8BIT)
270
+ end
271
+
272
+ if instruction
273
+ quoted_printable_with_instruction(text)
274
+ else
275
+ encode_quoted_printable_rfc2045(text)
276
+ end
277
+ end
278
+
279
+ def quote_address_if_necessary(address, charset)
280
+ case address
281
+ when Array
282
+ address.map{|addr| quote_address_if_necessary(addr, charset) }
283
+ when /^(\S.*)\s+(<.*>)$/
284
+ phrase = $1.gsub(/^['"](.*)['"]$/, '\1')
285
+ address = $2
286
+
287
+ phrase = quote_if_necessary(phrase, charset, true)
288
+
289
+ "%p %s" % [phrase, address]
290
+ else
291
+ address
292
+ end
293
+ end
294
+
295
+ def encode_file(string)
296
+ [string].pack('m')
297
+ end
298
+
299
+ def file_read(file)
300
+ case file
301
+ when String, Pathname
302
+ File.open(file.to_s, 'rb') do |io|
303
+ encode_file(io.read)
304
+ end
305
+ else
306
+ encode_file(file.read)
307
+ end
308
+ end
309
+
310
+ def encode_quoted_printable_rfc2045(string)
311
+ [string].pack('M').gsub(/\n/, "\r\n").chomp.gsub(/=$/, '')
312
+ end
313
+
314
+ def encode_quoted_printable_rfc2047(string)
315
+ string.enum_for(:each_byte).map{|ord|
316
+ if ord < 128 and ord != 61 # 61 is ascii '='
317
+ ord.chr
318
+ else
319
+ '=%X' % ord
320
+ end
321
+ }.join('').chomp.
322
+ gsub(/=$/,'').gsub('?', '=3F').gsub('_', '=5F').gsub(/ /, '_')
323
+ end
324
+
325
+ def build_body_boundary(type, encoding = 'quoted-printable')
326
+ BODY_BOUNDARY % [ body_boundary, type, encoding ]
327
+ end
328
+
329
+ def build_attachment_boundary(attachment)
330
+ mime, file, headers = attachment.values_at(:mimetype, :filename, :headers)
331
+
332
+ boundary = ATTACHMENT_BOUNDARY % [attachment_boundary, mime, file, file]
333
+ boundary << "\r\n%s" % headers.join("\r\n") if headers
334
+
335
+ boundary
336
+ end
337
+
338
+ # Try to get the
339
+ def mime_type_for(filename, override = nil)
340
+ override || Mime.type_for(filename)
341
+ end
342
+
343
+ ## Header shortcuts
344
+
345
+ def self.header_accessors(hash)
346
+ hash.each{|k,v| header_accessor(k, v) }
347
+ end
348
+
349
+ def self.header_accessor(method, header)
350
+ public
351
+ eval("def %s; self[%p].first; end" % [method, header.to_s])
352
+ eval("def %s=(o); self[%p] = o; end" % [method, header.to_s])
353
+ end
354
+
355
+ header_accessors(:reply_to => 'Reply-To', :to => :to, :from => :from,
356
+ :subject => :subject, :bcc => :bcc)
357
+
358
+ def self.generate_boundary(size = 25, chars = BOUNDARY_CHARS)
359
+ char_count = chars.size
360
+ postfix = Array.new(size){
361
+ chars[rand(char_count)]
362
+ }.join
363
+
364
+ return "#{BOUNDARY_PREFIX}#{postfix}"
365
+ end
366
+ end
367
+ end