mime 0.3.0 → 0.4.0
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.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
|