mime 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +192 -140
- data/lib/mime/composite_media.rb +230 -0
- data/lib/mime/content_formats/text_flowed.rb +6 -6
- data/lib/mime/discrete_media.rb +73 -0
- data/lib/mime/discrete_media_factory.rb +19 -13
- data/lib/mime/header.rb +48 -0
- data/lib/mime/headers/internet.rb +124 -27
- data/lib/mime/headers/mime.rb +38 -27
- data/lib/mime/mail.rb +51 -0
- data/lib/mime/media.rb +30 -0
- data/lib/mime/parser.rb +1 -1
- data/lib/mime.rb +27 -9
- data/test/scaffold/application.msg +2 -5
- data/test/scaffold/audio.msg +2 -5
- data/test/scaffold/image.msg +0 -0
- data/test/scaffold/multipart_alternative.msg +7 -7
- data/test/scaffold/multipart_alternative_related.msg +0 -0
- data/test/scaffold/multipart_form_data_file_and_text.msg +0 -0
- data/test/scaffold/multipart_form_data_mixed.msg +0 -0
- data/test/scaffold/multipart_form_data_text.msg +12 -12
- data/test/scaffold/multipart_mixed_inline_and_attachment.msg +0 -0
- data/test/scaffold/multipart_mixed_inline_and_attachment2.msg +0 -0
- data/test/scaffold/multipart_related.msg +0 -0
- data/test/scaffold/rfc822_composite.msg +0 -0
- data/test/scaffold/{plain_text_email.msg → rfc822_discrete.msg} +5 -5
- data/test/scaffold/text.msg +2 -5
- data/test/scaffold/video.msg +2 -5
- data/test/test_mime.rb +323 -150
- data/test/test_text_flowed.rb +1 -1
- metadata +13 -12
- data/lib/mime/composite_media_type.rb +0 -169
- data/lib/mime/discrete_media_type.rb +0 -78
- data/lib/mime/header_container.rb +0 -32
- data/lib/mime/media_type.rb +0 -51
- data/lib/mime/message.rb +0 -61
@@ -0,0 +1,230 @@
|
|
1
|
+
module MIME
|
2
|
+
|
3
|
+
#
|
4
|
+
# Composite media types allow encapsulating, mixing, and hierarchical
|
5
|
+
# structuring of entities of different types within a single message.
|
6
|
+
# Therefore, a CompositeMedia body is composed of one or more CompositeMedia
|
7
|
+
# and/or DiscreteMedia objects.
|
8
|
+
#
|
9
|
+
# CompositeMedia implements Content-Disposition for dictating presentation
|
10
|
+
# style of body entities via #add, #attach, and #inline. For more information
|
11
|
+
# on disposition parameters, such as filename, size, and modification-date,
|
12
|
+
# see https://tools.ietf.org/html/rfc2183.
|
13
|
+
#
|
14
|
+
# This class is abstract.
|
15
|
+
#
|
16
|
+
class CompositeMedia < Media
|
17
|
+
|
18
|
+
class Body
|
19
|
+
#
|
20
|
+
# Create new composite body.
|
21
|
+
#
|
22
|
+
def initialize boundary
|
23
|
+
@boundary = boundary
|
24
|
+
@body = Array.new
|
25
|
+
end
|
26
|
+
|
27
|
+
#
|
28
|
+
# Format the CompositeMedia object as a MIME message.
|
29
|
+
#
|
30
|
+
def to_s
|
31
|
+
all_entities = @body.join("\r\n--#{@boundary}\r\n")
|
32
|
+
"--#{@boundary}\r\n#{all_entities}\r\n--#{@boundary}--\r\n"
|
33
|
+
end
|
34
|
+
|
35
|
+
#
|
36
|
+
# Add +entity+ to the composite body.
|
37
|
+
#
|
38
|
+
def add entity
|
39
|
+
@body.push(entity)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
attr_reader :boundary
|
45
|
+
|
46
|
+
def initialize content_type
|
47
|
+
AbstractClassError.no_instantiation(self, CompositeMedia)
|
48
|
+
@boundary = "Boundary_#{ID.generate_id}" # delimits body entities
|
49
|
+
super(Body.new(boundary), content_type, 'boundary' => boundary)
|
50
|
+
end
|
51
|
+
|
52
|
+
#
|
53
|
+
# Add a Media +entity+ to the message.
|
54
|
+
#
|
55
|
+
# The entity will be added to the main body of the message with no
|
56
|
+
# disposition specified. Presentation of the entity will be dictated by
|
57
|
+
# the display user agent.
|
58
|
+
#
|
59
|
+
# === Text and HTML Multipart/Alternative message
|
60
|
+
#
|
61
|
+
# A display user agent may only be capable of displaying plain text. If so,
|
62
|
+
# it will choose to display the Text/Plain entity. However, if it is capable
|
63
|
+
# of displaying HTML, it may choose to display the Text/HTML version.
|
64
|
+
#
|
65
|
+
# msg = MIME::Multipart::Alternative.new
|
66
|
+
# msg.add(MIME::Text.new('plain text'))
|
67
|
+
# msg.add(MIME::Text.new('<html>html text</html>', 'html'))
|
68
|
+
#
|
69
|
+
# The order in which the entities are added is significant. Add the simplest
|
70
|
+
# representations first.
|
71
|
+
#
|
72
|
+
def add entity
|
73
|
+
raise Error.new('can only add Media objects') unless entity.is_a? Media
|
74
|
+
@body.add(entity)
|
75
|
+
end
|
76
|
+
|
77
|
+
#
|
78
|
+
# Attach a Media +entity+ to the message.
|
79
|
+
#
|
80
|
+
# The entity will be presented as separate from the main body of the
|
81
|
+
# message. Thus, display of the entity will not be automatic, but contingent
|
82
|
+
# upon some further action of the user. For example, the display user agent
|
83
|
+
# may present an icon representation of the entity, which the user can
|
84
|
+
# select to view or save the entity.
|
85
|
+
#
|
86
|
+
# === Attachment with filename and size parameters:
|
87
|
+
#
|
88
|
+
# f = File.open('file.txt')
|
89
|
+
# file = MIME::Text.new(f.read)
|
90
|
+
# text = MIME::Text.new('See the attached file.')
|
91
|
+
#
|
92
|
+
# msg = MIME::Multipart::Mixed.new
|
93
|
+
# msg.inline(text)
|
94
|
+
# msg.attach(file, 'filename' => f.path, 'size' => f.size)
|
95
|
+
#
|
96
|
+
def attach entity, params = {}
|
97
|
+
entity.set_disposition('attachment', params)
|
98
|
+
add(entity)
|
99
|
+
end
|
100
|
+
|
101
|
+
#
|
102
|
+
# Inline a Media +entity+ in the message.
|
103
|
+
#
|
104
|
+
# The entity will be embedded within the main body of the message. Thus,
|
105
|
+
# display of the entity will be automatic upon display of the message.
|
106
|
+
# Inline entities should be added in the order in which they occur within
|
107
|
+
# the message.
|
108
|
+
#
|
109
|
+
# === Message with two embedded images:
|
110
|
+
#
|
111
|
+
# msg = MIME::Multipart::Mixed.new
|
112
|
+
# msg.inline(MIME::Image.new(File.read('screenshot1.png'), 'png'))
|
113
|
+
# msg.inline(MIME::Image.new(File.read('screenshot2.png'), 'png'))
|
114
|
+
# msg.description = 'My screenshots'
|
115
|
+
#
|
116
|
+
def inline entity, params = {}
|
117
|
+
entity.set_disposition('inline', params)
|
118
|
+
add(entity)
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
122
|
+
|
123
|
+
#
|
124
|
+
# Message is intended to encapsulate another message. In particular, the
|
125
|
+
# <em>message/rfc822</em> content type is used to encapsulate RFC 822
|
126
|
+
# messages.
|
127
|
+
#
|
128
|
+
# TODO Implement
|
129
|
+
#
|
130
|
+
class Message < CompositeMedia
|
131
|
+
end
|
132
|
+
|
133
|
+
#
|
134
|
+
# The abstract base class for all multipart message subtypes. The entities of
|
135
|
+
# a multipart message are delimited by a unique boundary.
|
136
|
+
#
|
137
|
+
class Multipart < CompositeMedia
|
138
|
+
def initialize media_subtype
|
139
|
+
AbstractClassError.no_instantiation(self, Multipart)
|
140
|
+
super("multipart/#{media_subtype}")
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
#
|
145
|
+
# The Alternative subtype indicates that each contained entity is an
|
146
|
+
# alternatively formatted version of the same content. The most complex
|
147
|
+
# version should be added to the message first, i.e. it will be sequentially
|
148
|
+
# last in the message.
|
149
|
+
#
|
150
|
+
class Multipart::Alternative < Multipart
|
151
|
+
|
152
|
+
#
|
153
|
+
# Returns a Multipart::Alternative object with a content type of
|
154
|
+
# multipart/alternative.
|
155
|
+
#
|
156
|
+
def initialize
|
157
|
+
super('alternative')
|
158
|
+
end
|
159
|
+
|
160
|
+
end
|
161
|
+
|
162
|
+
#
|
163
|
+
# The FormData subtype expresses values for HTML form data submissions.
|
164
|
+
# ---
|
165
|
+
# RFCs consulted during implementation:
|
166
|
+
#
|
167
|
+
# * RFC-1867 Form-based File Upload in HTML
|
168
|
+
# * RFC-2388 Returning Values from Forms: multipart/form-data
|
169
|
+
#
|
170
|
+
class Multipart::FormData < Multipart
|
171
|
+
|
172
|
+
#
|
173
|
+
# Returns a Multipart::FormData object with a content type of
|
174
|
+
# multipart/form-data.
|
175
|
+
#
|
176
|
+
def initialize
|
177
|
+
super('form-data')
|
178
|
+
end
|
179
|
+
|
180
|
+
#
|
181
|
+
# Add the Media object, +entity+, to the FormData object. +name+ is
|
182
|
+
# typically an HTML input tag variable name. If the input tag is of type
|
183
|
+
# _file_, then +filename+ must be specified to indicate a file upload.
|
184
|
+
#
|
185
|
+
def add entity, name, filename = nil
|
186
|
+
entity.set_disposition('form-data', 'name' => name, 'filename' => filename)
|
187
|
+
super(entity)
|
188
|
+
end
|
189
|
+
|
190
|
+
end
|
191
|
+
|
192
|
+
#
|
193
|
+
# The Mixed subtype aggregates contextually independent entities.
|
194
|
+
#
|
195
|
+
class Multipart::Mixed < Multipart
|
196
|
+
|
197
|
+
#
|
198
|
+
# Returns a Multipart::Mixed object with a content type of
|
199
|
+
# multipart/mixed.
|
200
|
+
#
|
201
|
+
def initialize
|
202
|
+
super('mixed')
|
203
|
+
end
|
204
|
+
|
205
|
+
end
|
206
|
+
|
207
|
+
#
|
208
|
+
# The Related subtype aggregates multiple related entities. The message
|
209
|
+
# consists of a root (the first entity) which references subsequent inline
|
210
|
+
# entities. Message entities should be referenced by their Content-ID header.
|
211
|
+
# The syntax of a reference is unspecified and is instead dictated by the
|
212
|
+
# encoding or protocol used in the entity.
|
213
|
+
# ---
|
214
|
+
# RFC consulted during implementation:
|
215
|
+
#
|
216
|
+
# * RFC-2387 The MIME Multipart/Related Content-type
|
217
|
+
#
|
218
|
+
class Multipart::Related < Multipart
|
219
|
+
|
220
|
+
#
|
221
|
+
# Returns a Multipart::Related object with a content type of
|
222
|
+
# multipart/related.
|
223
|
+
#
|
224
|
+
def initialize
|
225
|
+
super('related')
|
226
|
+
end
|
227
|
+
|
228
|
+
end
|
229
|
+
|
230
|
+
end
|
@@ -41,15 +41,15 @@ module MIME::ContentFormats
|
|
41
41
|
# Encode plain +text+ into flowed format, reducing long lines to +max+
|
42
42
|
# characters or less using soft line breaks (i.e., SPACE+CRLF).
|
43
43
|
#
|
44
|
-
#
|
45
|
-
# and 72 characters
|
44
|
+
# According to the RFC, the +max+ flowed line length is 79 characters. Line
|
45
|
+
# lengths of 66 and 72 characters are common.
|
46
46
|
#
|
47
47
|
# The features of RFC 2646, such as line quoting and space-stuffing,
|
48
48
|
# are not implemented.
|
49
49
|
#
|
50
50
|
def self.encode(text, max = MAX_FLOWED_LINE)
|
51
|
-
if max >
|
52
|
-
raise ArgumentError,
|
51
|
+
if max > MAX_FLOWED_LINE
|
52
|
+
raise ArgumentError, "flowed lines must be #{MAX_FLOWED_LINE} characters or less"
|
53
53
|
end
|
54
54
|
|
55
55
|
out = []
|
@@ -83,7 +83,7 @@ module MIME::ContentFormats
|
|
83
83
|
if word.size < max
|
84
84
|
line << word + char
|
85
85
|
else
|
86
|
-
word.scan(/.{1,#{
|
86
|
+
word.scan(/.{1,#{MIME::MAX_LINE_LENGTH}}/) {|s| out << s }
|
87
87
|
end
|
88
88
|
end
|
89
89
|
word.clear
|
@@ -98,7 +98,7 @@ module MIME::ContentFormats
|
|
98
98
|
out << line + word
|
99
99
|
else
|
100
100
|
out << line unless line.empty?
|
101
|
-
word.scan(/.{1,#{
|
101
|
+
word.scan(/.{1,#{MIME::MAX_LINE_LENGTH}}/) {|s| out << s }
|
102
102
|
end
|
103
103
|
elsif ! line.empty?
|
104
104
|
out << line
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module MIME
|
2
|
+
|
3
|
+
#
|
4
|
+
# Discrete media must be handled by non-MIME mechanisms; they are opaque to
|
5
|
+
# MIME processors. Therefore, the body of a DiscreteMedia object does not need
|
6
|
+
# further MIME processing.
|
7
|
+
#
|
8
|
+
# This class is abstract.
|
9
|
+
#
|
10
|
+
class DiscreteMedia < Media
|
11
|
+
def initialize(content, content_type, content_params)
|
12
|
+
AbstractClassError.no_instantiation(self, DiscreteMedia)
|
13
|
+
super
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
#
|
18
|
+
# Application is intended for discrete data that is to be processed by some
|
19
|
+
# type of application program. The body contains information which must be
|
20
|
+
# processed by an application before it is viewable or usable by a user.
|
21
|
+
#
|
22
|
+
# Application is the catch all class. If your content cannot be identified as
|
23
|
+
# another DiscreteMedia, then it is application media.
|
24
|
+
#
|
25
|
+
class Application < DiscreteMedia
|
26
|
+
def initialize(body, subtype = 'octet-stream', params = {})
|
27
|
+
super(body, "application/#{subtype}", params)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
#
|
32
|
+
# Audio is intended for discrete audio content. The +subtype+ indicates the
|
33
|
+
# specific audio format, such as *mpeg* or *midi*.
|
34
|
+
#
|
35
|
+
class Audio < DiscreteMedia
|
36
|
+
def initialize(body, subtype = 'basic', params = {})
|
37
|
+
super(body, "audio/#{subtype}", params)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
#
|
42
|
+
# Image is intented for discrete image content. The +subtype+ indicates the
|
43
|
+
# specific image format, such as *jpeg* or *gif*.
|
44
|
+
#
|
45
|
+
class Image < DiscreteMedia
|
46
|
+
def initialize(body, subtype = 'jpeg', params = {})
|
47
|
+
super(body, "image/#{subtype}", params)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
#
|
52
|
+
# Text is intended for content which is principally textual in form. The
|
53
|
+
# +subtype+ indicates the specific text type, such as *plain* or *html*.
|
54
|
+
#
|
55
|
+
class Text < DiscreteMedia
|
56
|
+
def initialize(body, subtype = 'plain', params = {})
|
57
|
+
super(body, "text/#{subtype}", params)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
#
|
62
|
+
# Video is intended for discrete video content. The content +subtype+
|
63
|
+
# indicates the specific video format. The RFC describes video media as
|
64
|
+
# content that contains a time-varying-picture image, possibly with color and
|
65
|
+
# coordinated sound.
|
66
|
+
#
|
67
|
+
class Video < DiscreteMedia
|
68
|
+
def initialize(body, subtype = 'mpeg', params = {})
|
69
|
+
super(body, "video/#{subtype}", params)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
@@ -1,16 +1,20 @@
|
|
1
1
|
module MIME
|
2
2
|
|
3
3
|
#
|
4
|
-
# Module used only for initializing derived
|
4
|
+
# Module used only for initializing derived DiscreteMedia objects.
|
5
5
|
#
|
6
6
|
module DiscreteMediaFactory
|
7
7
|
|
8
|
+
module DispositionParameters
|
9
|
+
attr_accessor :path, :size
|
10
|
+
end
|
11
|
+
|
8
12
|
class << self
|
9
13
|
|
10
14
|
include ContentTypes
|
11
15
|
|
12
16
|
#
|
13
|
-
# Creates a corresponding
|
17
|
+
# Creates a corresponding DiscreteMedia subclass object for the given
|
14
18
|
# +file+ based on +file+'s filename extension. +file+ can be a file path
|
15
19
|
# or File object.
|
16
20
|
#
|
@@ -22,14 +26,16 @@ module MIME
|
|
22
26
|
# +path+ method is utilized by other methods in the MIME library,
|
23
27
|
# therefore, eliminating redundant and explicit filename assignments.
|
24
28
|
#
|
25
|
-
#
|
29
|
+
# === Comparison Example
|
26
30
|
#
|
27
|
-
#
|
28
|
-
#
|
31
|
+
# file1 = '/tmp/file1.txt'
|
32
|
+
# file2 = '/tmp/file2.txt'
|
33
|
+
# entity1 = Text.new(File.read(file1))
|
34
|
+
# entity2 = DiscreteMediaFactory.create(file2)
|
29
35
|
#
|
30
36
|
# mixed_msg = Multipart::Mixed.new
|
31
|
-
# mixed_msg.
|
32
|
-
# mixed_msg.
|
37
|
+
# mixed_msg.attach(entity1, 'filename' => file1)
|
38
|
+
# mixed_msg.attach(entity2) # filename automatically added
|
33
39
|
#
|
34
40
|
def create file, content_type = nil
|
35
41
|
if file.is_a? File
|
@@ -49,15 +55,15 @@ module MIME
|
|
49
55
|
|
50
56
|
media_obj =
|
51
57
|
case type
|
52
|
-
when 'application';
|
53
|
-
when 'audio' ;
|
54
|
-
when 'image' ;
|
55
|
-
when 'text' ;
|
56
|
-
when 'video' ;
|
58
|
+
when 'application'; Application.new(cntnt, subtype)
|
59
|
+
when 'audio' ; Audio.new(cntnt, subtype)
|
60
|
+
when 'image' ; Image.new(cntnt, subtype)
|
61
|
+
when 'text' ; Text.new(cntnt, subtype)
|
62
|
+
when 'video' ; Video.new(cntnt, subtype)
|
57
63
|
else raise UnknownContentError, "invalid content type: #{ctype}"
|
58
64
|
end
|
59
65
|
|
60
|
-
|
66
|
+
media_obj.extend(DispositionParameters)
|
61
67
|
media_obj.path = fname
|
62
68
|
media_obj
|
63
69
|
end
|
data/lib/mime/header.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
module MIME
|
2
|
+
|
3
|
+
#
|
4
|
+
# Header section for Internet and MIME messages.
|
5
|
+
#
|
6
|
+
class Header
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@headers = Hash.new
|
10
|
+
end
|
11
|
+
|
12
|
+
#
|
13
|
+
# Convert all headers to their string equivalents and join them using the
|
14
|
+
# RFC 2822 CRLF line separator.
|
15
|
+
#--
|
16
|
+
# TODO fold lines to 78 chars.
|
17
|
+
# word.scan(/(.,?){1,78}/) OR word.split
|
18
|
+
#
|
19
|
+
def to_s
|
20
|
+
@headers.to_a.map {|kv| kv.join(": ")}.join("\r\n")
|
21
|
+
end
|
22
|
+
|
23
|
+
#
|
24
|
+
# Get header value associated with +name+.
|
25
|
+
#
|
26
|
+
def get name
|
27
|
+
_, value = @headers.find {|k,v| name.downcase == k.downcase }
|
28
|
+
value
|
29
|
+
end
|
30
|
+
|
31
|
+
#
|
32
|
+
# Set header +name+ to +value+. If a header of the same name exists it will
|
33
|
+
# be overwritten. Header names are _case-insensitive_.
|
34
|
+
#
|
35
|
+
def set name, value
|
36
|
+
delete(name)
|
37
|
+
@headers.store(name, value)
|
38
|
+
end
|
39
|
+
|
40
|
+
#
|
41
|
+
# Delete header associated with +name+.
|
42
|
+
#
|
43
|
+
def delete name
|
44
|
+
@headers.delete_if {|k| name.downcase == k.downcase }
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
@@ -4,8 +4,18 @@ module MIME
|
|
4
4
|
#
|
5
5
|
# The RFC 2822 Internet message header fields.
|
6
6
|
#
|
7
|
+
# Mailbox fields #to, #from, #cc, #bcc, and #reply_to may be a single email
|
8
|
+
# address, an array of email addresses, or a hash of _email_ => _name_
|
9
|
+
# pairs. When using a hash, set _name_ to +nil+ to omit email display name.
|
10
|
+
# The #sender field is a special case and contain only a single mailbox.
|
11
|
+
#
|
7
12
|
module Internet
|
8
13
|
|
14
|
+
# Internet message character specifications (RFC 5322)
|
15
|
+
ATOM = /[[:alnum:]!#\$%&'*+\/=?^_`{|}~-]/
|
16
|
+
DOT_ATOM = /^#{ATOM}+(#{ATOM}|\.)*$/
|
17
|
+
SPECIALS = /[()<>\[\]:;@\,."]/
|
18
|
+
|
9
19
|
attr_reader(
|
10
20
|
# Required Headers
|
11
21
|
:to,
|
@@ -15,76 +25,163 @@ module MIME
|
|
15
25
|
# Optional Headers
|
16
26
|
:cc,
|
17
27
|
:bcc,
|
28
|
+
:sender,
|
18
29
|
:reply_to,
|
19
30
|
:message_id,
|
31
|
+
:in_reply_to,
|
32
|
+
:references,
|
20
33
|
:comments,
|
21
34
|
:keywords,
|
22
35
|
:subject
|
23
36
|
)
|
24
37
|
|
38
|
+
#
|
39
|
+
# Origination date at which the creator of the message indicated that the
|
40
|
+
# message was complete and ready to enter the mail delivery system.
|
41
|
+
#
|
25
42
|
def date= date
|
26
43
|
@date = date
|
27
|
-
headers.
|
44
|
+
headers.set('Date', date.rfc2822)
|
28
45
|
end
|
29
46
|
|
30
|
-
|
31
|
-
|
32
|
-
|
47
|
+
#
|
48
|
+
# Person(s) or system(s) responsible for writing the message.
|
49
|
+
#
|
50
|
+
def from= mailbox
|
51
|
+
@from = mailbox
|
52
|
+
headers.set('From', stringify_mailbox(mailbox))
|
53
|
+
end
|
54
|
+
|
55
|
+
#
|
56
|
+
# Mailbox of the agent responsible for actual transmission of the message.
|
57
|
+
# Sender field is required if the From field contains multiple mailboxes.
|
58
|
+
#
|
59
|
+
# === Example scenario
|
60
|
+
# If a secretary were to send a message for another person, the mailbox of
|
61
|
+
# the secretary would appear in the Sender field and the mailbox of the
|
62
|
+
# actual author would appear in the From field.
|
63
|
+
#
|
64
|
+
def sender= mailbox
|
65
|
+
if (mailbox.is_a?(Hash) || mailbox.is_a?(Array)) && mailbox.size != 1
|
66
|
+
raise ArgumentError, '"Sender" must be a single mailbox specification'
|
67
|
+
end
|
68
|
+
@sender = mailbox
|
69
|
+
headers.set('Sender', stringify_mailbox(mailbox))
|
33
70
|
end
|
34
71
|
|
35
|
-
|
36
|
-
|
37
|
-
|
72
|
+
#
|
73
|
+
# Mailbox(es) of the primary recipient(s).
|
74
|
+
#
|
75
|
+
def to= mailbox
|
76
|
+
@to = mailbox
|
77
|
+
headers.set('To', stringify_mailbox(mailbox))
|
38
78
|
end
|
39
79
|
|
40
|
-
|
41
|
-
|
42
|
-
|
80
|
+
#
|
81
|
+
# Mailbox(es) of others who are to receive the message, though the content
|
82
|
+
# of the message may not be directed at them; "Carbon Copy."
|
83
|
+
#
|
84
|
+
def cc= mailbox
|
85
|
+
@cc = mailbox
|
86
|
+
headers.set('Cc', stringify_mailbox(mailbox))
|
43
87
|
end
|
44
88
|
|
45
|
-
|
46
|
-
|
47
|
-
|
89
|
+
#
|
90
|
+
# Mailbox(es) of recipients of the message whose addresses are not to be
|
91
|
+
# revealed to other recipients of the message; "Blind Carbon Copy."
|
92
|
+
#
|
93
|
+
def bcc= mailbox
|
94
|
+
@bcc = mailbox
|
95
|
+
headers.set('Bcc', stringify_mailbox(mailbox))
|
48
96
|
end
|
49
97
|
|
50
|
-
|
51
|
-
|
52
|
-
|
98
|
+
#
|
99
|
+
# Mailbox(es) to which the author suggests that replies be sent.
|
100
|
+
#
|
101
|
+
def reply_to= mailbox
|
102
|
+
@reply_to = mailbox
|
103
|
+
headers.set('Reply-To', stringify_mailbox(mailbox))
|
53
104
|
end
|
54
105
|
|
106
|
+
#
|
107
|
+
# Globally unique identifier of the message.
|
55
108
|
#
|
56
109
|
# The message +id+ must contain an embedded "@" symbol. An example +id+
|
57
110
|
# might be <em>some-unique-id@domain.com</em>.
|
58
111
|
#
|
59
112
|
def message_id= id
|
60
|
-
@message_id =
|
61
|
-
headers.
|
113
|
+
@message_id = id
|
114
|
+
headers.set('Message-ID', "<#{id}>")
|
115
|
+
end
|
116
|
+
|
117
|
+
#
|
118
|
+
# The +id+ of the message to which this message is a reply.
|
119
|
+
#--
|
120
|
+
# TODO fully implement and test
|
121
|
+
#
|
122
|
+
def in_reply_to= id
|
123
|
+
@in_reply_to = id
|
124
|
+
headers.set('In-Reply-To', "<#{id}>")
|
125
|
+
end
|
126
|
+
|
127
|
+
#
|
128
|
+
# The +id+ used to identify a "thread" of conversation.
|
129
|
+
#--
|
130
|
+
# TODO fully implement and test
|
131
|
+
#
|
132
|
+
def references= id
|
133
|
+
@references = id
|
134
|
+
headers.set('References', "<#{id}>")
|
62
135
|
end
|
63
136
|
|
137
|
+
#
|
138
|
+
# Additional comments about the message content.
|
139
|
+
#
|
64
140
|
def comments= comments
|
65
141
|
@comments = comments
|
66
|
-
headers.
|
142
|
+
headers.set('Comments', comments)
|
67
143
|
end
|
68
144
|
|
145
|
+
#
|
146
|
+
# Comma-separated list of important words and phrases that might be useful
|
147
|
+
# for the recipient.
|
148
|
+
#
|
69
149
|
def keywords= keywords
|
70
150
|
@keywords = keywords
|
71
|
-
headers.
|
151
|
+
headers.set('Keywords', keywords)
|
72
152
|
end
|
73
153
|
|
154
|
+
#
|
155
|
+
# The message topic.
|
156
|
+
#
|
74
157
|
def subject= subject
|
75
158
|
@subject = subject
|
76
|
-
headers.
|
159
|
+
headers.set('Subject', subject)
|
77
160
|
end
|
78
161
|
|
79
162
|
|
80
163
|
private
|
81
164
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
165
|
+
def stringify_mailbox mailbox
|
166
|
+
case mailbox
|
167
|
+
when Hash
|
168
|
+
mailbox.map do |email, name|
|
169
|
+
if name
|
170
|
+
if name =~ SPECIALS
|
171
|
+
name.gsub!('"', '\"')
|
172
|
+
%["#{name}" <#{email}>]
|
173
|
+
else
|
174
|
+
%[#{name} <#{email}>]
|
175
|
+
end
|
176
|
+
else
|
177
|
+
email
|
178
|
+
end
|
179
|
+
end.join(', ')
|
180
|
+
when Array
|
181
|
+
mailbox.join(', ')
|
182
|
+
else
|
183
|
+
return mailbox
|
184
|
+
end
|
88
185
|
end
|
89
186
|
|
90
187
|
end
|