manveru-mailit 2009.06.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/README.md +63 -0
- data/lib/mailit/mail.rb +367 -0
- data/lib/mailit/mailer.rb +129 -0
- data/lib/mailit/mime.rb +49 -0
- data/lib/mailit.rb +9 -0
- data/mailit.gemspec +25 -0
- data/spec/helper.rb +4 -0
- data/spec/mailit/mail.rb +108 -0
- data/spec/mailit/mailer.rb +48 -0
- metadata +61 -0
data/README.md
ADDED
|
@@ -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
|
data/lib/mailit/mail.rb
ADDED
|
@@ -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, file, 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.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
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
module Mailit
|
|
2
|
+
# The Mailer is an abstraction layer for different SMTP clients, it provides
|
|
3
|
+
# #send and #defer_send methods
|
|
4
|
+
#
|
|
5
|
+
# At the time of writing, Net::SMTP and EventMachine::SmtpClient are
|
|
6
|
+
# supported, it should be trivial to add support for any other client.
|
|
7
|
+
#
|
|
8
|
+
# The difference between #send and #defer_send depends on the backend, but
|
|
9
|
+
# usually #send will block until the mail was sent while #defer_send does it
|
|
10
|
+
# in the background and allows you to continue execution immediately.
|
|
11
|
+
#
|
|
12
|
+
# @example Usage
|
|
13
|
+
#
|
|
14
|
+
# mail = Mailit::Mail.new
|
|
15
|
+
# mail.to = 'test@test.com'
|
|
16
|
+
# mail.from = 'sender@sender.com'
|
|
17
|
+
# mail.subject 'Here are some files for you!'
|
|
18
|
+
# mail.text = 'This is what you see with a plaintext mail reader'
|
|
19
|
+
# mail.attach('/home/manveru/.vimrc')
|
|
20
|
+
#
|
|
21
|
+
# # Send and wait until sending finished
|
|
22
|
+
# Mailit::Mailer.send(mail)
|
|
23
|
+
#
|
|
24
|
+
# # Send in background thread and continue doing other things
|
|
25
|
+
# Mailit::Mailer.defer_send(mail)
|
|
26
|
+
#
|
|
27
|
+
# The default Mailer backend is Mailit::Mailer::NetSmtp, you can change the
|
|
28
|
+
# default by including another module into Mailit::mailer
|
|
29
|
+
#
|
|
30
|
+
# @example Using Mailt::Mailer::EventMachine by inclusion
|
|
31
|
+
#
|
|
32
|
+
# class Mailit::Mailer
|
|
33
|
+
# include Mailit::Mailer::EventMachine
|
|
34
|
+
# end
|
|
35
|
+
#
|
|
36
|
+
# Another way is to extend an instance of Mailer with the backend you want to
|
|
37
|
+
# use, which will not affect other instances.
|
|
38
|
+
#
|
|
39
|
+
# @example Using Mailit::mailer::EventMachine by extension
|
|
40
|
+
#
|
|
41
|
+
# mailer = Mailit::Mailer.new
|
|
42
|
+
# mailer.extend(Mailit::Mailer::EventMachine)
|
|
43
|
+
class Mailer
|
|
44
|
+
OPTIONS = {
|
|
45
|
+
:server => 'smtp.localhost',
|
|
46
|
+
:port => 25,
|
|
47
|
+
:domain => 'localhost',
|
|
48
|
+
:username => 'foo',
|
|
49
|
+
:password => 'foo',
|
|
50
|
+
:noop => false,
|
|
51
|
+
:auth_type => :login, # :plain, :login, :cram_md5
|
|
52
|
+
:starttls => false, # only useful for EventMachine::SmtpClient
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
def self.send(mail, options = {})
|
|
56
|
+
new.send(mail, options)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def self.defer_send(mail, options = {})
|
|
60
|
+
new.defer_send(mail, options)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
undef :send
|
|
64
|
+
|
|
65
|
+
attr_reader :options
|
|
66
|
+
|
|
67
|
+
def initialize(options = {})
|
|
68
|
+
@options = OPTIONS.merge(options)
|
|
69
|
+
extend NetSmtp unless respond_to?(:send)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def settings(override, *keys)
|
|
75
|
+
options.merge(override).values_at(*keys)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
module NetSmtp
|
|
79
|
+
def send(mail, override = {})
|
|
80
|
+
require 'net/smtp'
|
|
81
|
+
|
|
82
|
+
server, port, domain, username, password, auth_type =
|
|
83
|
+
settings(override, :server, :port, :domain, :username, :password, :auth_type)
|
|
84
|
+
|
|
85
|
+
::Net::SMTP.start(server, port, domain, username, password, auth_type) do |smtp|
|
|
86
|
+
return if noop
|
|
87
|
+
smtp.send_message(mail.to_s, mail.from, mail.to)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def defer_send(mail, override = {})
|
|
92
|
+
Thread.new{ send(mail, override) }
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Allows you to comfortably use the EventMachine::SmtpClient.
|
|
97
|
+
# In order to use it, you have to first include this module into
|
|
98
|
+
# Mailit::Mailer or extend the Mailit::Mailer instance with it.
|
|
99
|
+
module EventMachine
|
|
100
|
+
|
|
101
|
+
# This assumes that EventMachine was required and we are inside the
|
|
102
|
+
# EventMachine::run block.
|
|
103
|
+
#
|
|
104
|
+
# Since EM implements some parts of the mail building we'll have to
|
|
105
|
+
# deconstruct our mail a bit.
|
|
106
|
+
# On the upside, it seems like it supports STARTTLS (without certificate
|
|
107
|
+
# options though)
|
|
108
|
+
def send(mail, options = {})
|
|
109
|
+
server, port, domain, username, password, auth_type =
|
|
110
|
+
settings(override, :server, :port, :domain, :username, :password, :auth_type)
|
|
111
|
+
|
|
112
|
+
mail.construct # prepare headers and body
|
|
113
|
+
|
|
114
|
+
em_options = { :port => port, :host => host, :domain => domain,
|
|
115
|
+
:from => mail.from, :to => mail.to, :header => mail.header_string,
|
|
116
|
+
:body => mail.body_string }
|
|
117
|
+
|
|
118
|
+
if auth_type
|
|
119
|
+
em_options[:auth] = {
|
|
120
|
+
:type => auth_type, :username => username, :password => password }
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
::EventMachine::SmtpClient.send(em_options)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
alias defer_send send
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
data/lib/mailit/mime.rb
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
module Mailit
|
|
2
|
+
module Mime
|
|
3
|
+
module_function
|
|
4
|
+
|
|
5
|
+
def mime_for(filename)
|
|
6
|
+
detect_handler unless defined?(@mime_handler)
|
|
7
|
+
send(@mime_handler, filename)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def detect_handler
|
|
11
|
+
try_require('rubygems')
|
|
12
|
+
|
|
13
|
+
if try_require('mime/types')
|
|
14
|
+
@mime_handler = :from_mime_types
|
|
15
|
+
elsif try_require('rack') and try_require('rack/mime')
|
|
16
|
+
@mime_handler = :from_rack
|
|
17
|
+
else
|
|
18
|
+
require 'webrick/httputils'
|
|
19
|
+
@webrick_types = WEBrick::HTTPUtils::DefaultMimeTypes.dup
|
|
20
|
+
try_extend_webrick('/etc/mime.types')
|
|
21
|
+
@mime_handler = :from_webrick
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def from_mime_types(filename)
|
|
26
|
+
MIME::Types.type_for(filename) || 'application/octet-stream'
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def from_rack(filename)
|
|
30
|
+
Rack::Mime.mime_type(File.extname(filename))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def from_webrick(filename)
|
|
34
|
+
WEBrick::HTTPUtils.mime_type(filename, @webrick_types)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def try_extend_webrick(file)
|
|
38
|
+
hash = WEBrick::HTTPUtils.load_mime_types(file)
|
|
39
|
+
@webrick_types.merge!(hash)
|
|
40
|
+
rescue
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def try_require(lib)
|
|
44
|
+
require lib
|
|
45
|
+
true
|
|
46
|
+
rescue LoadError
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
data/lib/mailit.rb
ADDED
data/mailit.gemspec
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Gem::Specification.new do |s|
|
|
2
|
+
s.name = "mailit"
|
|
3
|
+
s.version = "2009.06.08"
|
|
4
|
+
|
|
5
|
+
s.summary = "RFC compliant MIME email generation and SMTP mailing."
|
|
6
|
+
s.description = "Simple to use class to generate RFC compliant MIME email."
|
|
7
|
+
s.platform = "ruby"
|
|
8
|
+
s.has_rdoc = true
|
|
9
|
+
s.author = "Michael 'manveru' Fellinger"
|
|
10
|
+
s.email = "m.fellinger@gmail.com"
|
|
11
|
+
s.homepage = "http://github.com/manveru/mailit"
|
|
12
|
+
s.require_path = "lib"
|
|
13
|
+
|
|
14
|
+
s.files = [
|
|
15
|
+
"README.md",
|
|
16
|
+
"lib/mailit.rb",
|
|
17
|
+
"lib/mailit/mail.rb",
|
|
18
|
+
"lib/mailit/mailer.rb",
|
|
19
|
+
"lib/mailit/mime.rb",
|
|
20
|
+
"mailit.gemspec",
|
|
21
|
+
"spec/helper.rb",
|
|
22
|
+
"spec/mailit/mail.rb",
|
|
23
|
+
"spec/mailit/mailer.rb",
|
|
24
|
+
]
|
|
25
|
+
end
|
data/spec/helper.rb
ADDED
data/spec/mailit/mail.rb
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Encoding: UTF-8
|
|
2
|
+
require 'spec/helper'
|
|
3
|
+
|
|
4
|
+
# The specs are translated from the Test::Unit tests of MailFactory.
|
|
5
|
+
#
|
|
6
|
+
# TODO:
|
|
7
|
+
# * test_attach_as
|
|
8
|
+
# * test_email
|
|
9
|
+
|
|
10
|
+
describe Mailit::Mail do
|
|
11
|
+
should 'set and get headers' do
|
|
12
|
+
mail = Mailit::Mail.new
|
|
13
|
+
|
|
14
|
+
mail.set_header('arbitrary', 'some value')
|
|
15
|
+
mail.get_header('arbitrary').should == ['some value']
|
|
16
|
+
|
|
17
|
+
mail.set_header('arbitrary-header', 'some _ value')
|
|
18
|
+
mail.get_header('arbitrary-header').should == ['some _ value']
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
should 'generate valid boundaries' do
|
|
22
|
+
50.times do
|
|
23
|
+
boundary = Mailit::Mail.generate_boundary
|
|
24
|
+
boundary.should =~ /^----=_NextPart_[a-zA-Z0-9_.]{25}$/
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
should 'make mail with recipient' do
|
|
29
|
+
mail = Mailit::Mail.new
|
|
30
|
+
|
|
31
|
+
mail.to = 'test@test.com'
|
|
32
|
+
mail.to.should == 'test@test.com'
|
|
33
|
+
|
|
34
|
+
mail.to = 'test@test2.com'
|
|
35
|
+
mail.to.should == 'test@test2.com'
|
|
36
|
+
|
|
37
|
+
mail.headers.size.should == 1 # make sure the previous was deleted
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
should 'make mail with sender' do
|
|
41
|
+
mail = Mailit::Mail.new
|
|
42
|
+
|
|
43
|
+
mail.from = 'test@test.com'
|
|
44
|
+
mail.from.should == 'test@test.com'
|
|
45
|
+
|
|
46
|
+
mail.from = 'test@test2.com'
|
|
47
|
+
mail.from.should == 'test@test2.com'
|
|
48
|
+
|
|
49
|
+
mail.headers.size.should == 1 # make sure the previous was deleted
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
should 'set correct subject' do
|
|
53
|
+
mail = Mailit::Mail.new
|
|
54
|
+
|
|
55
|
+
mail.subject = 'Test Subject'
|
|
56
|
+
mail.subject.should == '=?utf-8?Q?Test_Subject?='
|
|
57
|
+
|
|
58
|
+
mail.subject = 'A Different Subject'
|
|
59
|
+
mail.subject.should == '=?utf-8?Q?A_Different_Subject?='
|
|
60
|
+
|
|
61
|
+
mail.headers.size.should == 1 # make sure the previous was deleted
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
should 'use quoted printable with instruction' do
|
|
65
|
+
mail = Mailit::Mail.new
|
|
66
|
+
|
|
67
|
+
mail.to = 'test@test.com'
|
|
68
|
+
mail.from = 'test@othertest.com'
|
|
69
|
+
mail.subject = "My email subject has a ? in it and also an = and a _ too... Also some non-quoted junk ()!@\#\{\$\%\}"
|
|
70
|
+
mail.text = "This is a test message with\na few\n\nlines."
|
|
71
|
+
|
|
72
|
+
mail.subject.should == "=?utf-8?Q?My_email_subject_has_a_=3F_in_it_and_also_an_=3D_and_a_=5F_too..._Also_some_non-quoted_junk_()!@\#\{\$\%\}?="
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
should 'use subject quoting for scandinavian string' do
|
|
76
|
+
mail = Mailit::Mail.new
|
|
77
|
+
|
|
78
|
+
mail.to = "test@test.com"
|
|
79
|
+
mail.from = "test@othertest.com"
|
|
80
|
+
# Three a with dots and three o with dots.
|
|
81
|
+
mail.subject = "\303\244\303\244\303\244\303\266\303\266\303\266"
|
|
82
|
+
mail.text = "This is a test message with\na few\n\nlines."
|
|
83
|
+
|
|
84
|
+
mail.subject.should == "=?utf-8?Q?=C3=A4=C3=A4=C3=A4=C3=B6=C3=B6=C3=B6?="
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
should 'use subject quoting for utf-8 string' do
|
|
88
|
+
mail = Mailit::Mail.new
|
|
89
|
+
|
|
90
|
+
mail.to = "test@test.com"
|
|
91
|
+
mail.from = "test@othertest.com"
|
|
92
|
+
mail.subject = "My email subject has a à which is utf8."
|
|
93
|
+
mail.text = "This is a test message with\na few\n\nlines."
|
|
94
|
+
|
|
95
|
+
mail.subject.should == "=?utf-8?Q?My_email_subject_has_a_=C3=83_which_is_utf8.?="
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
should 'encode html as quoted printable' do
|
|
99
|
+
mail = Mailit::Mail.new
|
|
100
|
+
|
|
101
|
+
mail.to = "test@test.com"
|
|
102
|
+
mail.from = "test@othertest.com"
|
|
103
|
+
mail.subject = "some html"
|
|
104
|
+
mail.html = "<a href=\"http://google.com\">click here</a>"
|
|
105
|
+
|
|
106
|
+
mail.to_s.should.include('<a href=3D"http://google.com">click here</a>')
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
require 'spec/helper'
|
|
2
|
+
|
|
3
|
+
class MockSMTP
|
|
4
|
+
INSTANCES = []
|
|
5
|
+
|
|
6
|
+
def self.start(*args, &block)
|
|
7
|
+
INSTANCES << new(*args, &block)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
attr_reader :start_args, :result, :send_message_args
|
|
11
|
+
|
|
12
|
+
def initialize(*args, &block)
|
|
13
|
+
@start_args = args
|
|
14
|
+
yield(self)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def send_message(*args)
|
|
18
|
+
@send_message_args = args
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
Mailit::Mail::OPTIONS[:message_id] = lambda{|mail| '1234' }
|
|
23
|
+
|
|
24
|
+
describe Mailit::Mailer do
|
|
25
|
+
it 'sends a mail' do
|
|
26
|
+
mail = Mailit::Mail.new(
|
|
27
|
+
:to => 'test@example.com',
|
|
28
|
+
:from => 'sender@example.com',
|
|
29
|
+
:subject => 'Here are some files for you!',
|
|
30
|
+
:text => 'Some text about that')
|
|
31
|
+
|
|
32
|
+
mailer = Mailit::Mailer.new
|
|
33
|
+
|
|
34
|
+
mailer.send(mail, :server => 'smtp.example.com', :port => 25,
|
|
35
|
+
:domain => 'example.com', :password => 'foo',
|
|
36
|
+
:mailer => MockSMTP)
|
|
37
|
+
|
|
38
|
+
mock = MockSMTP::INSTANCES.last
|
|
39
|
+
mock.start_args.should == [
|
|
40
|
+
'smtp.example.com', 25,
|
|
41
|
+
'example.com',
|
|
42
|
+
'sender@example.com',
|
|
43
|
+
'foo',
|
|
44
|
+
:cram_md5
|
|
45
|
+
]
|
|
46
|
+
mock.send_message_args.should == [mail.to_s, mail.from, mail.to]
|
|
47
|
+
end
|
|
48
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: manveru-mailit
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 2009.06.08
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Michael 'manveru' Fellinger
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
|
|
12
|
+
date: 2009-05-16 00:00:00 -07:00
|
|
13
|
+
default_executable:
|
|
14
|
+
dependencies: []
|
|
15
|
+
|
|
16
|
+
description: Simple to use class to generate RFC compliant MIME email.
|
|
17
|
+
email: m.fellinger@gmail.com
|
|
18
|
+
executables: []
|
|
19
|
+
|
|
20
|
+
extensions: []
|
|
21
|
+
|
|
22
|
+
extra_rdoc_files: []
|
|
23
|
+
|
|
24
|
+
files:
|
|
25
|
+
- README.md
|
|
26
|
+
- lib/mailit.rb
|
|
27
|
+
- lib/mailit/mail.rb
|
|
28
|
+
- lib/mailit/mailer.rb
|
|
29
|
+
- lib/mailit/mime.rb
|
|
30
|
+
- mailit.gemspec
|
|
31
|
+
- spec/helper.rb
|
|
32
|
+
- spec/mailit/mail.rb
|
|
33
|
+
- spec/mailit/mailer.rb
|
|
34
|
+
has_rdoc: true
|
|
35
|
+
homepage: http://github.com/manveru/mailit
|
|
36
|
+
post_install_message:
|
|
37
|
+
rdoc_options: []
|
|
38
|
+
|
|
39
|
+
require_paths:
|
|
40
|
+
- lib
|
|
41
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
42
|
+
requirements:
|
|
43
|
+
- - ">="
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: "0"
|
|
46
|
+
version:
|
|
47
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
48
|
+
requirements:
|
|
49
|
+
- - ">="
|
|
50
|
+
- !ruby/object:Gem::Version
|
|
51
|
+
version: "0"
|
|
52
|
+
version:
|
|
53
|
+
requirements: []
|
|
54
|
+
|
|
55
|
+
rubyforge_project:
|
|
56
|
+
rubygems_version: 1.2.0
|
|
57
|
+
signing_key:
|
|
58
|
+
specification_version: 2
|
|
59
|
+
summary: RFC compliant MIME email generation and SMTP mailing.
|
|
60
|
+
test_files: []
|
|
61
|
+
|