kuvert 0.0.1
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.
- checksums.yaml +7 -0
- data/.gems +1 -0
- data/Makefile +4 -0
- data/README.markdown +28 -0
- data/kuvert.gemspec +14 -0
- data/lib/kuvert.rb +439 -0
- data/test/kuvert_test.rb +235 -0
- data/test/testfile.txt +0 -0
- data/test/testsheet.xls +0 -0
- metadata +67 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: c8954c2b806b2e4ecdea6b2b3e897e618424c756
|
4
|
+
data.tar.gz: e06e1405adf0777d7ea12e6be06ced1fbacd6a38
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: da810f127283d91127053e78c6a2edff3d7b273c8613ae431519ef350241a9303112cdbc0ad4418a918fa9be5b06949cd267e0ea2a54b45763c9148cccf3a298
|
7
|
+
data.tar.gz: 6f8f63c165c89c8bd959cff74be1154a129b27d7e87f88146f575d4df4ea556fca3173d081036d8ea7e593a8603cbeee928e1406381b4c529d063577042bc9fc
|
data/.gems
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
mime-types -v 2.4.3
|
data/Makefile
ADDED
data/README.markdown
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
kuvert
|
2
|
+
======
|
3
|
+
|
4
|
+
Continuing the legacy of [MailFactory][1].
|
5
|
+
|
6
|
+
## Usage
|
7
|
+
|
8
|
+
```ruby
|
9
|
+
mail = Kuvert.new
|
10
|
+
mail.to = 'dst@mailinator.com'
|
11
|
+
mail.from = 'src@mailinator.com'
|
12
|
+
mail.subject = 'hello'
|
13
|
+
mail.text = 'wonderful day to you!'
|
14
|
+
mail.attach('/path/to/your.pdf')
|
15
|
+
mail.to_s
|
16
|
+
```
|
17
|
+
|
18
|
+
## Credit
|
19
|
+
|
20
|
+
All of the credit goes to David Powers for his original
|
21
|
+
work on [MailFactory][1].
|
22
|
+
|
23
|
+
## License
|
24
|
+
|
25
|
+
[Ruby License][2]
|
26
|
+
|
27
|
+
[1]: http://rubygems.org/gems/mailfactory
|
28
|
+
[2]: https://www.ruby-lang.org/en/about/license.txt
|
data/kuvert.gemspec
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = "kuvert"
|
3
|
+
s.version = "0.0.1"
|
4
|
+
s.summary = %{Continuing the MailFactory legacy}
|
5
|
+
s.description = %{Keep MailFactory updated with the times}
|
6
|
+
s.authors = ["David Powers", "Cyril David"]
|
7
|
+
s.email = ["cyx@cyx.is"]
|
8
|
+
s.homepage = "http://github.com/cyx/kuvert"
|
9
|
+
s.license = "MIT"
|
10
|
+
|
11
|
+
s.files = `git ls-files`.split("\n")
|
12
|
+
|
13
|
+
s.add_dependency "mime-types", "~> 2.4"
|
14
|
+
end
|
data/lib/kuvert.rb
ADDED
@@ -0,0 +1,439 @@
|
|
1
|
+
# = Overview:
|
2
|
+
# A simple to use module for generating RFC compliant MIME mail
|
3
|
+
# ---
|
4
|
+
# = License:
|
5
|
+
# Author:: David Powers
|
6
|
+
# Contributors: Cyril David
|
7
|
+
# Copyright:: May, 2005
|
8
|
+
# License:: Ruby License
|
9
|
+
# ---
|
10
|
+
# = Usage:
|
11
|
+
# require 'net/smtp'
|
12
|
+
# require 'rubygems'
|
13
|
+
# require 'mailfactory'
|
14
|
+
#
|
15
|
+
#
|
16
|
+
# mail = Kuvert.new()
|
17
|
+
# mail.to = "test@test.com"
|
18
|
+
# mail.from = "sender@sender.com"
|
19
|
+
# mail.subject = "Here are some files for you!"
|
20
|
+
# mail.text = "This is what people with plain text mail readers will see"
|
21
|
+
# mail.html = "A little something <b>special</b> for people with HTML readers"
|
22
|
+
# mail.attach("/etc/fstab")
|
23
|
+
# mail.attach("/some/other/file")
|
24
|
+
#
|
25
|
+
# Net::SMTP.start('smtp1.testmailer.com', 25, 'mail.from.domain', fromaddress, password, :cram_md5) { |smtp|
|
26
|
+
# mail.to = toaddress
|
27
|
+
# smtp.send_message(mail.to_s(), fromaddress, toaddress)
|
28
|
+
# }
|
29
|
+
|
30
|
+
require 'pathname'
|
31
|
+
require 'mime/types'
|
32
|
+
|
33
|
+
# An easy class for creating a mail message
|
34
|
+
class Kuvert
|
35
|
+
|
36
|
+
VERSION = '1.4.0'
|
37
|
+
|
38
|
+
attr :bcc
|
39
|
+
|
40
|
+
def initialize()
|
41
|
+
@headers = Array.new()
|
42
|
+
@attachments = Array.new()
|
43
|
+
@attachmentboundary = generate_boundary()
|
44
|
+
@bodyboundary = generate_boundary()
|
45
|
+
@html = nil
|
46
|
+
@text = nil
|
47
|
+
@charset = 'utf-8'
|
48
|
+
@bcc = []
|
49
|
+
end
|
50
|
+
|
51
|
+
# adds a header to the bottom of the headers
|
52
|
+
def add_header(header, value)
|
53
|
+
value = quoted_printable_with_instruction(value, @charset) if header == 'subject'
|
54
|
+
value = quote_address_if_necessary(value, @charset) if %w[from to cc bcc reply-to].include?(header.downcase)
|
55
|
+
@headers << "#{header}: #{value}"
|
56
|
+
end
|
57
|
+
|
58
|
+
# removes the named header - case insensitive
|
59
|
+
def remove_header(header)
|
60
|
+
@headers.each_index() { |i|
|
61
|
+
if(@headers[i] =~ /^#{Regexp.escape(header)}:/i)
|
62
|
+
@headers.delete_at(i)
|
63
|
+
end
|
64
|
+
}
|
65
|
+
end
|
66
|
+
|
67
|
+
# sets a header (removing any other versions of that header)
|
68
|
+
def set_header(header, value)
|
69
|
+
remove_header(header)
|
70
|
+
add_header(header, value)
|
71
|
+
end
|
72
|
+
|
73
|
+
def replyto=(newreplyto)
|
74
|
+
remove_header("Reply-To")
|
75
|
+
add_header("Reply-To", newreplyto)
|
76
|
+
end
|
77
|
+
|
78
|
+
def replyto()
|
79
|
+
return(get_header("Reply-To")[0])
|
80
|
+
end
|
81
|
+
|
82
|
+
def bcc=(bcc)
|
83
|
+
@bcc.push(bcc)
|
84
|
+
end
|
85
|
+
|
86
|
+
def recipients
|
87
|
+
[].tap do |ret|
|
88
|
+
ret.push(*to)
|
89
|
+
ret.push(*cc)
|
90
|
+
ret.push(*bcc)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# sets the plain text body of the message
|
95
|
+
def text=(newtext)
|
96
|
+
@text = newtext
|
97
|
+
end
|
98
|
+
|
99
|
+
# sets the HTML body of the message. Only the body of the
|
100
|
+
# html should be provided
|
101
|
+
def html=(newhtml)
|
102
|
+
@html = "<html>\n<head>\n<meta content=\"text/html;charset=#{@charset}\" http-equiv=\"Content-Type\">\n</head>\n<body bgcolor=\"#ffffff\" text=\"#000000\">\n#{newhtml}\n</body>\n</html>"
|
103
|
+
end
|
104
|
+
|
105
|
+
# sets the HTML body of the message. The entire HTML section should be provided
|
106
|
+
def rawhtml=(newhtml)
|
107
|
+
@html = newhtml
|
108
|
+
end
|
109
|
+
|
110
|
+
# implement method missing to provide helper methods for setting and getting headers.
|
111
|
+
# Headers with '-' characters may be set/gotten as 'x_mailer' or 'XMailer' (splitting
|
112
|
+
# will occur between capital letters or on '_' chracters)
|
113
|
+
def method_missing(methId, *args)
|
114
|
+
name = methId.id2name()
|
115
|
+
|
116
|
+
# mangle the name if we have to
|
117
|
+
if(name =~ /_/)
|
118
|
+
name = name.gsub(/_/, '-')
|
119
|
+
elsif(name =~ /[A-Z]/)
|
120
|
+
name = name.gsub(/([a-zA-Z])([A-Z])/, '\1-\2')
|
121
|
+
end
|
122
|
+
|
123
|
+
# determine if it sets or gets, and do the right thing
|
124
|
+
if(name =~ /=$/)
|
125
|
+
if(args.length != 1)
|
126
|
+
super(methId, args)
|
127
|
+
end
|
128
|
+
set_header(name[/^(.*)=$/, 1], args[0])
|
129
|
+
else
|
130
|
+
if(args.length != 0)
|
131
|
+
super(methId, args)
|
132
|
+
end
|
133
|
+
headers = get_header(name)
|
134
|
+
return(get_header(name))
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# returns the value (or values) of the named header in an array
|
139
|
+
def get_header(header)
|
140
|
+
headers = Array.new()
|
141
|
+
headerregex = /^#{Regexp.escape(header)}:/i
|
142
|
+
@headers.each() { |h|
|
143
|
+
if(headerregex.match(h))
|
144
|
+
headers << h[/^[^:]+:(.*)/i, 1].strip()
|
145
|
+
end
|
146
|
+
}
|
147
|
+
|
148
|
+
return(headers)
|
149
|
+
end
|
150
|
+
|
151
|
+
# returns true if the email is multipart
|
152
|
+
def multipart?()
|
153
|
+
if(@attachments.length > 0 or @html != nil)
|
154
|
+
return(true)
|
155
|
+
else
|
156
|
+
return(false)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
# builds an email and returns it as a string. Takes the following options:
|
161
|
+
# <tt>:messageid</tt>:: Adds a message id to the message based on the from header (defaults to false)
|
162
|
+
# <tt>:date</tt>:: Adds a date to the message if one is not present (defaults to true)
|
163
|
+
def construct(options = Hash.new)
|
164
|
+
if(options[:date] == nil)
|
165
|
+
options[:date] = true
|
166
|
+
end
|
167
|
+
|
168
|
+
if(options[:messageid])
|
169
|
+
# add a unique message-id
|
170
|
+
remove_header("Message-ID")
|
171
|
+
sendingdomain = get_header('from')[0].to_s()[/@([-a-zA-Z0-9._]+)/,1].to_s()
|
172
|
+
add_header("Message-ID", "<#{Time.now.to_f()}.#{Process.euid()}.#{String.new.object_id()}@#{sendingdomain}>")
|
173
|
+
end
|
174
|
+
|
175
|
+
if(options[:date])
|
176
|
+
if(get_header("Date").length == 0)
|
177
|
+
add_header("Date", Time.now.strftime("%a, %d %b %Y %H:%M:%S %z"))
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
# Add a mime header if we don't already have one and we have multiple parts
|
182
|
+
if(multipart?())
|
183
|
+
if(get_header("MIME-Version").length == 0)
|
184
|
+
add_header("MIME-Version", "1.0")
|
185
|
+
end
|
186
|
+
|
187
|
+
if(get_header("Content-Type").length == 0)
|
188
|
+
if(@attachments.length == 0)
|
189
|
+
add_header("Content-Type", "multipart/alternative;boundary=\"#{@bodyboundary}\"")
|
190
|
+
else
|
191
|
+
add_header("Content-Type", "multipart/mixed; boundary=\"#{@attachmentboundary}\"")
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
return("#{headers_to_s()}#{body_to_s()}")
|
197
|
+
end
|
198
|
+
|
199
|
+
# returns a formatted email - equivalent to construct(:messageid => true)
|
200
|
+
def to_s()
|
201
|
+
return(construct(:messageid => true))
|
202
|
+
end
|
203
|
+
|
204
|
+
# generates a unique boundary string
|
205
|
+
def generate_boundary()
|
206
|
+
randomstring = Array.new()
|
207
|
+
1.upto(25) {
|
208
|
+
whichglyph = rand(100)
|
209
|
+
if(whichglyph < 40)
|
210
|
+
randomstring << (rand(25) + 65).chr()
|
211
|
+
elsif(whichglyph < 70)
|
212
|
+
randomstring << (rand(25) + 97).chr()
|
213
|
+
elsif(whichglyph < 90)
|
214
|
+
randomstring << (rand(10) + 48).chr()
|
215
|
+
elsif(whichglyph < 95)
|
216
|
+
randomstring << '.'
|
217
|
+
else
|
218
|
+
randomstring << '_'
|
219
|
+
end
|
220
|
+
}
|
221
|
+
return("----=_NextPart_#{randomstring.join()}")
|
222
|
+
end
|
223
|
+
|
224
|
+
# adds an attachment to the mail. Type may be given as a mime type. If it
|
225
|
+
# is left off and the MIME::Types module is available it will be determined automagically.
|
226
|
+
# If the optional attachemntheaders is given, then they will be added to the attachment
|
227
|
+
# boundary in the email, which can be used to produce Content-ID markers. attachmentheaders
|
228
|
+
# can be given as an Array or a String.
|
229
|
+
def add_attachment(filename, type=nil, attachmentheaders = nil)
|
230
|
+
attachment = Hash.new()
|
231
|
+
attachment['filename'] = Pathname.new(filename).basename
|
232
|
+
if(type == nil)
|
233
|
+
attachment['mimetype'] = MIME::Types.of(filename).first.to_s
|
234
|
+
else
|
235
|
+
attachment['mimetype'] = type
|
236
|
+
end
|
237
|
+
|
238
|
+
# Open in rb mode to handle Windows, which mangles binary files opened in a text mode
|
239
|
+
File.open(filename, "rb") { |fp|
|
240
|
+
attachment['attachment'] = file_encode(fp.read())
|
241
|
+
}
|
242
|
+
|
243
|
+
if(attachmentheaders != nil)
|
244
|
+
if(!attachmentheaders.kind_of?(Array))
|
245
|
+
attachmentheaders = attachmentheaders.split(/\r?\n/)
|
246
|
+
end
|
247
|
+
attachment['headers'] = attachmentheaders
|
248
|
+
end
|
249
|
+
|
250
|
+
@attachments << attachment
|
251
|
+
end
|
252
|
+
|
253
|
+
# adds an attachment to the mail as emailfilename. Type may be given as a mime type. If it
|
254
|
+
# is left off and the MIME::Types module is available it will be determined automagically.
|
255
|
+
# file may be given as an IO stream (which will be read until the end) or as a filename.
|
256
|
+
# If the optional attachemntheaders is given, then they will be added to the attachment
|
257
|
+
# boundary in the email, which can be used to produce Content-ID markers. attachmentheaders
|
258
|
+
# can be given as an Array of a String.
|
259
|
+
def add_attachment_as(file, emailfilename, type=nil, attachmentheaders = nil)
|
260
|
+
attachment = Hash.new()
|
261
|
+
attachment['filename'] = emailfilename
|
262
|
+
|
263
|
+
if(type != nil)
|
264
|
+
attachment['mimetype'] = type.to_s()
|
265
|
+
elsif(file.kind_of?(String) or file.kind_of?(Pathname))
|
266
|
+
attachment['mimetype'] = MIME::Types.of(file.to_s()).first.to_s
|
267
|
+
else
|
268
|
+
attachment['mimetype'] = ''
|
269
|
+
end
|
270
|
+
|
271
|
+
if(file.kind_of?(String) or file.kind_of?(Pathname))
|
272
|
+
# Open in rb mode to handle Windows, which mangles binary files opened in a text mode
|
273
|
+
File.open(file.to_s(), "rb") { |fp|
|
274
|
+
attachment['attachment'] = file_encode(fp.read())
|
275
|
+
}
|
276
|
+
elsif(file.respond_to?(:read))
|
277
|
+
attachment['attachment'] = file_encode(file.read())
|
278
|
+
else
|
279
|
+
raise(Exception, "file is not a supported type (must be a String, Pathnamem, or support read method)")
|
280
|
+
end
|
281
|
+
|
282
|
+
if(attachmentheaders != nil)
|
283
|
+
if(!attachmentheaders.kind_of?(Array))
|
284
|
+
attachmentheaders = attachmentheaders.split(/\r?\n/)
|
285
|
+
end
|
286
|
+
attachment['headers'] = attachmentheaders
|
287
|
+
end
|
288
|
+
|
289
|
+
@attachments << attachment
|
290
|
+
end
|
291
|
+
|
292
|
+
alias attach add_attachment
|
293
|
+
alias attach_as add_attachment_as
|
294
|
+
|
295
|
+
protected
|
296
|
+
|
297
|
+
# returns the @headers as a properly formatted string
|
298
|
+
def headers_to_s()
|
299
|
+
return("#{@headers.join("\r\n")}\r\n\r\n")
|
300
|
+
end
|
301
|
+
|
302
|
+
# returns the body as a properly formatted string
|
303
|
+
def body_to_s()
|
304
|
+
body = Array.new()
|
305
|
+
|
306
|
+
# simple message with one part
|
307
|
+
if(!multipart?())
|
308
|
+
return(@text)
|
309
|
+
else
|
310
|
+
body << "This is a multi-part message in MIME format.\r\n\r\n--#{@attachmentboundary}\r\nContent-Type: multipart/alternative; boundary=\"#{@bodyboundary}\""
|
311
|
+
|
312
|
+
if(@attachments.length > 0)
|
313
|
+
# text part
|
314
|
+
body << "#{buildbodyboundary("text/plain; charset=#{@charset}; format=flowed", 'quoted-printable')}\r\n\r\n#{quote_if_necessary(@text, @charset)}"
|
315
|
+
|
316
|
+
# html part if one is provided
|
317
|
+
if @html
|
318
|
+
body << "#{buildbodyboundary("text/html; charset=#{@charset}", 'quoted-printable')}\r\n\r\n#{quote_if_necessary(@html, @charset)}"
|
319
|
+
end
|
320
|
+
|
321
|
+
body << "--#{@bodyboundary}--"
|
322
|
+
|
323
|
+
# and, the attachments
|
324
|
+
if(@attachments.length > 0)
|
325
|
+
@attachments.each() { |attachment|
|
326
|
+
body << "#{buildattachmentboundary(attachment)}\r\n\r\n#{attachment['attachment']}"
|
327
|
+
}
|
328
|
+
body << "\r\n--#{@attachmentboundary}--"
|
329
|
+
end
|
330
|
+
else
|
331
|
+
# text part
|
332
|
+
body << "#{buildbodyboundary("text/plain; charset=#{@charset}; format=flowed", 'quoted-printable')}\r\n\r\n#{quote_if_necessary(@text, @charset)}"
|
333
|
+
|
334
|
+
# html part
|
335
|
+
body << "#{buildbodyboundary("text/html; charset=#{@charset}", 'quoted-printable')}\r\n\r\n#{quote_if_necessary(@html, @charset)}"
|
336
|
+
|
337
|
+
body << "--#{@bodyboundary}--"
|
338
|
+
end
|
339
|
+
|
340
|
+
return(body.join("\r\n\r\n"))
|
341
|
+
end
|
342
|
+
end
|
343
|
+
|
344
|
+
# builds a boundary string for including attachments in the body, expects an attachment hash as built by
|
345
|
+
# add_attachment and add_attachment_as
|
346
|
+
def buildattachmentboundary(attachment)
|
347
|
+
disposition = "Content-Disposition: inline; filename=\"#{attachment['filename']}\""
|
348
|
+
boundary = "--#{@attachmentboundary}\r\nContent-Type: #{attachment['mimetype']}; name=\"#{attachment['filename']}\"\r\nContent-Transfer-Encoding: base64\r\n#{disposition}"
|
349
|
+
if(attachment['headers'])
|
350
|
+
boundary = boundary + "\r\n#{attachment['headers'].join("\r\n")}"
|
351
|
+
end
|
352
|
+
|
353
|
+
return(boundary)
|
354
|
+
end
|
355
|
+
|
356
|
+
# builds a boundary string for inclusion in the body of a message
|
357
|
+
def buildbodyboundary(type, encoding)
|
358
|
+
return("--#{@bodyboundary}\r\nContent-Type: #{type}\r\nContent-Transfer-Encoding: #{encoding}")
|
359
|
+
end
|
360
|
+
|
361
|
+
# returns a base64 encoded version of the contents of str
|
362
|
+
def file_encode(str)
|
363
|
+
collection = Array.new()
|
364
|
+
enc = [str].pack('m')
|
365
|
+
# while(enc.length > 60)
|
366
|
+
# collection << enc.slice!(0..59)
|
367
|
+
# end
|
368
|
+
# collection << enc
|
369
|
+
# return(collection.join("\n"))
|
370
|
+
return(enc)
|
371
|
+
end
|
372
|
+
|
373
|
+
# Convert the given text into quoted printable format, with an instruction
|
374
|
+
# that the text be eventually interpreted in the given charset.
|
375
|
+
|
376
|
+
def quoted_printable_with_instruction(text, charset)
|
377
|
+
text = quoted_printable_encode_header(text)
|
378
|
+
"=?#{charset}?Q?#{text}?="
|
379
|
+
end
|
380
|
+
|
381
|
+
# rfc2045 compatible. use rfc2047 for headers (such as the Subject line) instead
|
382
|
+
def quoted_printable_encode(text)
|
383
|
+
[text].pack('M').gsub(/\n/, "\r\n").chomp.gsub(/=$/, '')
|
384
|
+
end
|
385
|
+
|
386
|
+
# Convert the given character to quoted printable format
|
387
|
+
# see http://tools.ietf.org/html/rfc2047
|
388
|
+
|
389
|
+
require 'enumerator' unless ''.respond_to? :enum_for
|
390
|
+
|
391
|
+
def quoted_printable_encode_header(text)
|
392
|
+
text.enum_for(:each_byte).map do |ord|
|
393
|
+
if ord < 128 and ord != 61 # 61 is ascii '='
|
394
|
+
ord.chr
|
395
|
+
else
|
396
|
+
'=%X' % ord
|
397
|
+
end
|
398
|
+
end.join('').
|
399
|
+
chomp.
|
400
|
+
gsub(/=$/,'').
|
401
|
+
gsub('?', '=3F').
|
402
|
+
gsub('_', '=5F').
|
403
|
+
gsub(/ /, '_')
|
404
|
+
end
|
405
|
+
|
406
|
+
# A quick-and-dirty regexp for determining whether a string contains any
|
407
|
+
# characters that need escaping.
|
408
|
+
#--
|
409
|
+
# Jun18-08: deprecated, since all multipart blocks are marked quoted-printable, quoting is required
|
410
|
+
|
411
|
+
# if !defined?(CHARS_NEEDING_QUOTING)
|
412
|
+
# CHARS_NEEDING_QUOTING = /[\000-\011\013\014\016-\037\177-\377]/
|
413
|
+
# end
|
414
|
+
|
415
|
+
# Quote the given text if it contains any "illegal" characters
|
416
|
+
def quote_if_necessary(text, charset, instruction = false)
|
417
|
+
return unless text
|
418
|
+
text = text.dup.force_encoding(Encoding::ASCII_8BIT) if text.respond_to?(:force_encoding)
|
419
|
+
#(text =~ CHARS_NEEDING_QUOTING) ? (instruction ? quoted_printable_with_instruction(text, charset) : quoted_printable_encode(text)) : text
|
420
|
+
instruction ? quoted_printable_with_instruction(text, charset) : quoted_printable_encode(text)
|
421
|
+
end
|
422
|
+
|
423
|
+
# Quote the given address if it needs to be. The address may be a
|
424
|
+
# regular email address, or it can be a phrase followed by an address in
|
425
|
+
# brackets. The phrase is the only part that will be quoted, and only if
|
426
|
+
# it needs to be. This allows extended characters to be used in the
|
427
|
+
# "to", "from", "cc", and "bcc" headers.
|
428
|
+
def quote_address_if_necessary(address, charset)
|
429
|
+
if Array === address
|
430
|
+
address.map { |a| quote_address_if_necessary(a, charset) }
|
431
|
+
elsif address =~ /^(\S.*)\s+(<.*>)$/
|
432
|
+
address = $2
|
433
|
+
phrase = quote_if_necessary($1.gsub(/^['"](.*)['"]$/, '\1'), charset, true)
|
434
|
+
"\"#{phrase}\" #{address}"
|
435
|
+
else
|
436
|
+
address
|
437
|
+
end
|
438
|
+
end
|
439
|
+
end
|
data/test/kuvert_test.rb
ADDED
@@ -0,0 +1,235 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# coding:utf-8
|
3
|
+
|
4
|
+
require 'test/unit/ui/console/testrunner'
|
5
|
+
require_relative '../lib/kuvert'
|
6
|
+
|
7
|
+
def get_options()
|
8
|
+
options = Hash.new()
|
9
|
+
|
10
|
+
opts = OptionParser.new() { |opts|
|
11
|
+
opts.on_tail("-h", "--help", "Print this message") {
|
12
|
+
print(opts)
|
13
|
+
exit()
|
14
|
+
}
|
15
|
+
|
16
|
+
opts.on("-s", "--smtpserver SERVER", "SMTP server to use for remote tests") { |server|
|
17
|
+
options['smtpserver'] = server
|
18
|
+
}
|
19
|
+
|
20
|
+
opts.on("-f", "--from ADDRESS", "address to send the mail from") { |address|
|
21
|
+
options['from'] = address
|
22
|
+
}
|
23
|
+
|
24
|
+
opts.on("-t", "--to ADDRESS", "address to send the mail to") { |address|
|
25
|
+
options['to'] = address
|
26
|
+
}
|
27
|
+
|
28
|
+
opts.on("-u", "--username USERNAME", "username for smtp auth (required)") { |username|
|
29
|
+
options['username'] = username
|
30
|
+
}
|
31
|
+
|
32
|
+
opts.on("-p", "--password PASSWORD", "password for smtp auth (required)") { |password|
|
33
|
+
options['password'] = password
|
34
|
+
}
|
35
|
+
|
36
|
+
}
|
37
|
+
|
38
|
+
opts.parse(ARGV)
|
39
|
+
|
40
|
+
return(options)
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
|
45
|
+
class TC_Kuvert < Test::Unit::TestCase
|
46
|
+
|
47
|
+
def setup()
|
48
|
+
@mail = Kuvert.new
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
def test_set_to
|
53
|
+
assert_nothing_raised("exception raised while setting to=") {
|
54
|
+
@mail.to = "test@test.com"
|
55
|
+
}
|
56
|
+
|
57
|
+
assert_equal(@mail.to, ["test@test.com"], "to does not equal what it was set to")
|
58
|
+
|
59
|
+
assert_nothing_raised("exception raised while setting to=") {
|
60
|
+
@mail.to = "test@test2.com"
|
61
|
+
}
|
62
|
+
|
63
|
+
# count to headers in the final message to make sure we have only one
|
64
|
+
count = 0
|
65
|
+
@mail.to_s().each_line() { |line|
|
66
|
+
if(line =~ /^To:/i)
|
67
|
+
count = count + 1
|
68
|
+
end
|
69
|
+
}
|
70
|
+
assert_equal(1, count, "Count of To: headers expected to be 1, but was #{count}")
|
71
|
+
end
|
72
|
+
|
73
|
+
|
74
|
+
def test_set_from
|
75
|
+
assert_nothing_raised("exception raised while setting from=") {
|
76
|
+
@mail.from = "test@test.com"
|
77
|
+
}
|
78
|
+
|
79
|
+
assert_equal(@mail.from, ["test@test.com"], "from does not equal what it was set to")
|
80
|
+
|
81
|
+
assert_nothing_raised("exception raised while setting from=") {
|
82
|
+
@mail.from = "test@test2.com"
|
83
|
+
}
|
84
|
+
|
85
|
+
# count to headers in the final message to make sure we have only one
|
86
|
+
count = 0
|
87
|
+
@mail.to_s().each_line() { |line|
|
88
|
+
if(line =~ /^From:/i)
|
89
|
+
count = count + 1
|
90
|
+
end
|
91
|
+
}
|
92
|
+
assert_equal(1, count, "Count of From: headers expected to be 1, but was #{count}")
|
93
|
+
end
|
94
|
+
|
95
|
+
|
96
|
+
def test_set_subject
|
97
|
+
assert_nothing_raised("exception raised while setting subject=") {
|
98
|
+
@mail.subject = "Test Subject"
|
99
|
+
}
|
100
|
+
|
101
|
+
assert_equal(["=?utf-8?Q?Test_Subject?="], @mail.subject, "subject does not equal what it was set to")
|
102
|
+
|
103
|
+
assert_nothing_raised("exception raised while setting subject=") {
|
104
|
+
@mail.subject = "A Different Subject"
|
105
|
+
}
|
106
|
+
|
107
|
+
# count to headers in the final message to make sure we have only one
|
108
|
+
count = 0
|
109
|
+
@mail.to_s().each_line() { |line|
|
110
|
+
if(line =~ /^Subject:/i)
|
111
|
+
count = count + 1
|
112
|
+
end
|
113
|
+
}
|
114
|
+
assert_equal(1, count, "Count of Subject: headers expected to be 1, but was #{count}")
|
115
|
+
end
|
116
|
+
|
117
|
+
|
118
|
+
def test_set_header
|
119
|
+
assert_nothing_raised("exception raised while setting arbitrary header") {
|
120
|
+
@mail.set_header("arbitrary", "some value")
|
121
|
+
}
|
122
|
+
|
123
|
+
assert_equal("some value", @mail.get_header("arbitrary")[0], "arbitrary header does not equal \"some value\"")
|
124
|
+
|
125
|
+
assert_nothing_raised("exception raised while setting arbitrary header with _") {
|
126
|
+
@mail.arbitrary_header = "some _ value"
|
127
|
+
}
|
128
|
+
|
129
|
+
assert_equal(["some _ value"], @mail.get_header("arbitrary-header"), "arbitrary header does not equal \"some _ value\"")
|
130
|
+
assert_equal(["some _ value"], @mail.arbitrary_header, "arbitrary header does not equal \"some _ value\"")
|
131
|
+
|
132
|
+
|
133
|
+
assert_nothing_raised("exception raised while setting arbitraryHeader") {
|
134
|
+
@mail.arbitraryHeader = "someValue"
|
135
|
+
}
|
136
|
+
|
137
|
+
assert_equal(["someValue"], @mail.get_header("arbitrary-header"), "arbitrary header does not equal \"someValue\"")
|
138
|
+
assert_equal(["someValue"], @mail.arbitraryHeader, "arbitrary header does not equal \"someValue\"")
|
139
|
+
end
|
140
|
+
|
141
|
+
|
142
|
+
def test_boundary_generator
|
143
|
+
1.upto(50) {
|
144
|
+
assert_match(/^----=_NextPart_[a-zA-Z0-9\._]{25}$/, @mail.generate_boundary(), "illegal message boundary generated")
|
145
|
+
}
|
146
|
+
end
|
147
|
+
|
148
|
+
|
149
|
+
def test_email
|
150
|
+
@mail.to="test@test.com"
|
151
|
+
@mail.from="test@othertest.com"
|
152
|
+
@mail.subject="This is a test"
|
153
|
+
@mail.text = "This is a test message with\na few\n\nlines."
|
154
|
+
|
155
|
+
@mail.attach(File.dirname(__FILE__) + '/testfile.txt')
|
156
|
+
@mail.attach(File.dirname(__FILE__) + '/testsheet.xls')
|
157
|
+
|
158
|
+
if($options['smtpserver'] != nil and $options['to'] != nil and $options['from'] != nil)
|
159
|
+
assert_nothing_raised() {
|
160
|
+
require('net/smtp')
|
161
|
+
Net::SMTP.start($options['smtpserver'], 25, 'mail.from.domain', $options['username'], $options['password'], :cram_md5) { |smtp|
|
162
|
+
smtp.send_message(@mail.to_s(), $options['from'], $options['to'])
|
163
|
+
}
|
164
|
+
}
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
|
169
|
+
def test_attach_as
|
170
|
+
@mail.to="test@test.com"
|
171
|
+
@mail.from="test@othertest.com"
|
172
|
+
@mail.subject="This is a test"
|
173
|
+
@mail.text = "This is a test message with\na few\n\nlines."
|
174
|
+
|
175
|
+
@mail.add_attachment_as(File.dirname(__FILE__) + '/testfile.txt', 'newname.txt')
|
176
|
+
@mail.add_attachment_as(File.open(File.dirname(__FILE__) + '/testsheet.xls', 'rb'), 'newname.xls', 'application/vnd.ms-excel')
|
177
|
+
|
178
|
+
if($options['smtpserver'] != nil and $options['to'] != nil and $options['from'] != nil)
|
179
|
+
assert_nothing_raised() {
|
180
|
+
require('net/smtp')
|
181
|
+
Net::SMTP.start($options['smtpserver'], 25, 'mail.from.domain', $options['username'], $options['password'], :cram_md5) { |smtp|
|
182
|
+
smtp.send_message(@mail.to_s(), $options['from'], $options['to'])
|
183
|
+
}
|
184
|
+
}
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
def test_quoted_printable_with_instruction
|
189
|
+
@mail.to="test@test.com"
|
190
|
+
@mail.from="test@othertest.com"
|
191
|
+
@mail.subject="My email subject has a ? in it and also an = and a _ too... Also some non-quoted junk ()!@\#\{\$\%\}"
|
192
|
+
@mail.text = "This is a test message with\na few\n\nlines."
|
193
|
+
assert_equal(["=?utf-8?Q?My_email_subject_has_a_=3F_in_it_and_also_an_=3D_and_a_=5F_too..._Also_some_non-quoted_junk_()!@\#\{\$\%\}?="], @mail.subject)
|
194
|
+
end
|
195
|
+
|
196
|
+
def test_scandinavian_subject_quoting
|
197
|
+
@mail.to="test@test.com"
|
198
|
+
@mail.from="test@othertest.com"
|
199
|
+
# Three a with dots and three o with dots.
|
200
|
+
@mail.subject="\303\244\303\244\303\244\303\266\303\266\303\266"
|
201
|
+
@mail.text = "This is a test message with\na few\n\nlines."
|
202
|
+
assert_equal(["=?utf-8?Q?=C3=A4=C3=A4=C3=A4=C3=B6=C3=B6=C3=B6?="], @mail.subject)
|
203
|
+
end
|
204
|
+
|
205
|
+
def test_utf8_quoted_printable_with_instruction
|
206
|
+
@mail.to="test@test.com"
|
207
|
+
@mail.from="test@othertest.com"
|
208
|
+
@mail.subject="My email subject has a à which is utf8."
|
209
|
+
@mail.text = "This is a test message with\na few\n\nlines."
|
210
|
+
assert_equal(["=?utf-8?Q?My_email_subject_has_a_=C3=83_which_is_utf8.?="], @mail.subject)
|
211
|
+
end
|
212
|
+
|
213
|
+
def test_quoted_printable_html
|
214
|
+
@mail.to="test@test.com"
|
215
|
+
@mail.from="test@othertest.com"
|
216
|
+
@mail.subject="some html"
|
217
|
+
@mail.html="<a href=\"http://google.com\">click here</a>"
|
218
|
+
assert_match('<a href=3D"http://google.com">click here</a>', @mail.to_s)
|
219
|
+
end
|
220
|
+
|
221
|
+
def test_bcc_not_in_headers
|
222
|
+
@mail.bcc = 'bcc@test.com'
|
223
|
+
assert_no_match(/bcc@test\.com/, @mail.to_s)
|
224
|
+
end
|
225
|
+
|
226
|
+
def test_recipients
|
227
|
+
@mail.to="test@test.com"
|
228
|
+
@mail.cc="cc@test.com"
|
229
|
+
@mail.bcc="bcc@test.com"
|
230
|
+
assert_equal(['test@test.com', 'cc@test.com', 'bcc@test.com'], @mail.recipients)
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
$options = get_options()
|
235
|
+
Test::Unit::UI::Console::TestRunner.run(TC_Kuvert)
|
data/test/testfile.txt
ADDED
File without changes
|
data/test/testsheet.xls
ADDED
File without changes
|
metadata
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: kuvert
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- David Powers
|
8
|
+
- Cyril David
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2015-03-12 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: mime-types
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - "~>"
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '2.4'
|
21
|
+
type: :runtime
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - "~>"
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '2.4'
|
28
|
+
description: Keep MailFactory updated with the times
|
29
|
+
email:
|
30
|
+
- cyx@cyx.is
|
31
|
+
executables: []
|
32
|
+
extensions: []
|
33
|
+
extra_rdoc_files: []
|
34
|
+
files:
|
35
|
+
- ".gems"
|
36
|
+
- Makefile
|
37
|
+
- README.markdown
|
38
|
+
- kuvert.gemspec
|
39
|
+
- lib/kuvert.rb
|
40
|
+
- test/kuvert_test.rb
|
41
|
+
- test/testfile.txt
|
42
|
+
- test/testsheet.xls
|
43
|
+
homepage: http://github.com/cyx/kuvert
|
44
|
+
licenses:
|
45
|
+
- MIT
|
46
|
+
metadata: {}
|
47
|
+
post_install_message:
|
48
|
+
rdoc_options: []
|
49
|
+
require_paths:
|
50
|
+
- lib
|
51
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
56
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
requirements: []
|
62
|
+
rubyforge_project:
|
63
|
+
rubygems_version: 2.4.5
|
64
|
+
signing_key:
|
65
|
+
specification_version: 4
|
66
|
+
summary: Continuing the MailFactory legacy
|
67
|
+
test_files: []
|