2 # Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
4 # Permission is hereby granted, free of charge, to any person obtaining
5 # a copy of this software and associated documentation files (the
6 # "Software"), to deal in the Software without restriction, including
7 # without limitation the rights to use, copy, modify, merge, publish,
8 # distribute, sublicense, and/or sell copies of the Software, and to
9 # permit persons to whom the Software is furnished to do so, subject to
10 # the following conditions:
12 # The above copyright notice and this permission notice shall be
13 # included in all copies or substantial portions of the Software.
15 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 # Note: Originally licensed under LGPL v2+. Using MIT license for Rails
24 # with permission of Minero Aoki.
27 # = TMail - The EMail Swiss Army Knife for Ruby
29 # The TMail library provides you with a very complete way to handle and manipulate EMails
30 # from within your Ruby programs.
32 # Used as the backbone for email handling by the Ruby on Rails and Nitro web frameworks as
33 # well as a bunch of other Ruby apps including the Ruby-Talk mailing list to newsgroup email
34 # gateway, it is a proven and reliable email handler that won't let you down.
36 # Originally created by Minero Aoki, TMail has been recently picked up by Mikel Lindsaar and
37 # is being actively maintained. Numerous backlogged bug fixes have been applied as well as
38 # Ruby 1.9 compatibility and a swath of documentation to boot.
40 # TMail allows you to treat an email totally as an object and allow you to get on with your
41 # own programming without having to worry about crafting the perfect email address validation
42 # parser, or assembling an email from all it's component parts.
44 # TMail handles the most complex part of the email - the header. It generates and parses
45 # headers and provides you with instant access to their innards through simple and logically
46 # named accessor and setter methods.
48 # TMail also provides a wrapper to Net/SMTP as well as Unix Mailbox handling methods to
49 # directly read emails from your unix mailbox, parse them and use them.
51 # Following is the comprehensive list of methods to access TMail::Mail objects. You can also
52 # check out TMail::Mail, TMail::Address and TMail::Headers for other lists.
55 # Provides an exception to throw on errors in Syntax within TMail's parsers
56 class SyntaxError
< StandardError
; end
58 # Provides a new email boundary to separate parts of the email. This is a random
59 # string based off the current time, so should be fairly unique.
64 # #=> "mimepart_47bf656968207_25a8fbb80114"
66 # #=> "mimepart_47bf66051de4_25a8fbb80240"
67 def TMail
.new_boundary
68 'mimepart_' + random_tag
71 # Provides a new email message ID. You can use this to generate unique email message
72 # id's for your email so you can track them.
74 # Optionally takes a fully qualified domain name (default to the current hostname
75 # returned by Socket.gethostname) that will be appended to the message ID.
79 # email.message_id = TMail.new_message_id
80 # #=> "<47bf66845380e_25a8fbb80332@baci.local.tmail>"
82 # #=> "Message-Id: <47bf668b633f1_25a8fbb80475@baci.local.tmail>\n\n"
83 # email.message_id = TMail.new_message_id("lindsaar.net")
84 # #=> "<47bf668b633f1_25a8fbb80475@lindsaar.net.tmail>"
86 # #=> "Message-Id: <47bf668b633f1_25a8fbb80475@lindsaar.net.tmail>\n\n"
87 def TMail
.new_message_id( fqdn
= nil )
88 fqdn
||= ::Socket.gethostname
89 "<#{random_tag()}@#{fqdn}.tmail>"
93 def TMail
.random_tag
#:nodoc:
96 sprintf('%x%x_%x%x%d%x',
98 $$
, Thread
.current
.object_id
, @uniq, rand(255))
100 private_class_method
:random_tag
106 # Text Utils provides a namespace to define TOKENs, ATOMs, PHRASEs and CONTROL characters that
107 # are OK per RFC 2822.
109 # It also provides methods you can call to determine if a string is safe
112 aspecial
= %Q
|()<>[]:;.\\,"|
113 tspecial = %Q|()<>[];:\\,"/?=|
115 control
= %Q
|\x00-\x1f\x7f-\xff|
117 CONTROL_CHAR
= /[#{control}]/n
118 ATOM_UNSAFE
= /[#{Regexp.quote aspecial}#{control}#{lwsp}]/n
119 PHRASE_UNSAFE
= /[#{Regexp.quote aspecial}#{control}]/n
120 TOKEN_UNSAFE
= /[#{Regexp.quote tspecial}#{control}#{lwsp}]/n
122 # Returns true if the string supplied is free from characters not allowed as an ATOM
123 def atom_safe
?( str
)
124 not ATOM_UNSAFE
=== str
127 # If the string supplied has ATOM unsafe characters in it, will return the string quoted
128 # in double quotes, otherwise returns the string unmodified
129 def quote_atom( str
)
130 (ATOM_UNSAFE
=== str
) ? dquote(str
) : str
133 # If the string supplied has PHRASE unsafe characters in it, will return the string quoted
134 # in double quotes, otherwise returns the string unmodified
135 def quote_phrase( str
)
136 (PHRASE_UNSAFE
=== str
) ? dquote(str
) : str
139 # Returns true if the string supplied is free from characters not allowed as a TOKEN
140 def token_safe
?( str
)
141 not TOKEN_UNSAFE
=== str
144 # If the string supplied has TOKEN unsafe characters in it, will return the string quoted
145 # in double quotes, otherwise returns the string unmodified
146 def quote_token( str
)
147 (TOKEN_UNSAFE
=== str
) ? dquote(str
) : str
150 # Wraps supplied string in double quotes unless it is already wrapped
151 # Returns double quoted string
152 def dquote( str
) #:nodoc:
153 unless str
=~
/^".*?"$/
154 '"' + str
.gsub(/["\\]/n
) {|s
| '\\' + s
} + '"'
161 # Unwraps supplied string from inside double quotes
162 # Returns unquoted string
164 str
=~
/^"(.*?)"$/ ? $1 : str
167 # Provides a method to join a domain name by it's parts and also makes it
168 # ATOM safe by quoting it as needed
169 def join_domain( arr
)
171 if /\A\[.*\]\z/ === i
188 'nst' => -(3 * 60 + 30),
227 # Takes a time zone string from an EMail and converts it to Unix Time (seconds)
228 def timezone_string_to_unixtime( str
)
229 if m
= /([\+\-])(\d\d?)(\d\d)/.match(str
)
230 sec
= (m
[2].to_i
* 60 + m
[3].to_i
) * 60
231 m
[1] == '-' ? -sec
: sec
233 min
= ZONESTR_TABLE
[str
.downcase
] or
234 raise SyntaxError
, "wrong timezone format '#{str}'"
240 WDAY
= %w( Sun Mon Tue Wed Thu Fri Sat TMailBUG
)
241 MONTH
= %w( TMailBUG Jan Feb Mar Apr May Jun
242 Jul Aug Sep Oct Nov Dec TMailBUG
)
246 gmt
= Time
.at(tm
.to_i
)
248 offset
= tm
.to_i
- Time
.local(*gmt
.to_a
[0,6].reverse
).to_i
250 # DO NOT USE strftime: setlocale() breaks it
251 sprintf
'%s, %s %s %d %02d:%02d:%02d %+.2d%.2d',
252 WDAY
[tm
.wday
], tm
.mday
, MONTH
[tm
.month
],
253 tm
.year
, tm
.hour
, tm
.min
, tm
.sec
,
254 *(offset
/ 60).divmod(60)
258 MESSAGE_ID
= /<[^\@>]+\@[^>\@]+>/
260 def message_id
?( str
)
265 MIME_ENCODED
= /=\?[^\s?=]+\?[QB]\?[^\s?=]+\?=/i
267 def mime_encoded
?( str
)
272 def decode_params( hash
)
275 hash
.each
do |key
, value
|
276 if m
= /\*(?:(\d+)\*)?\z/.match(key
)
277 ((encoded
||= {})[m
.pre_match
] ||= [])[(m
[1] || 0).to_i
] = value
279 new
[key
] = to_kcode(value
)
283 encoded
.each
do |key
, strings
|
284 new
[key
] = decode_RFC2231(strings
.join(''))
297 flag
= NKF_FLAGS
[TMail
.KCODE
] or return str
301 RFC2231_ENCODED
= /\A(?:iso-2022-jp|euc-jp|shift_jis|us-ascii)?'[a-z]*'/in
303 def decode_RFC2231( str
)
304 m
= RFC2231_ENCODED
.match(str
) or return str
306 to_kcode(m
.post_match
.gsub(/%[\da-f]{2}/in) {|s
| s
[1,2].hex
.chr
})
308 m
.post_match
.gsub(/%[\da-f]{2}/in, "")
313 # Make sure the Content-Type boundary= parameter is quoted if it contains illegal characters
314 # (to ensure any special characters in the boundary text are escaped from the parser
315 # (such as = in MS Outlook's boundary text))
316 if @body =~
/^(.*)boundary=(.*)$/m
320 remainder
=~
/^(.*?)(;.*)$/m
324 boundary_text
= remainder
.chomp
326 if boundary_text
=~
/[\/\?\
=]/
327 boundary_text
= "\"#{boundary_text}\"" unless boundary_text
=~
/^".*?"$/
328 @body = "#{preamble}boundary=#{boundary_text}#{post}"