smail-mime 0.0.6

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.
@@ -0,0 +1,192 @@
1
+ #require 'jcode'
2
+ require 'iconv'
3
+
4
+ # Extensions to the String library for encoding and decoding of MIME data.
5
+ class String
6
+
7
+ # Returns true if the string consists entirely of whitespace.
8
+ # (The empty string will return false.)
9
+ def is_space?
10
+ return Regexp.new('\A\s+\Z', Regexp::MULTILINE).match(self) != nil
11
+ end
12
+
13
+ # Returns true if the string contains only valid ASCII characters
14
+ # (i.e. nothing over ASCII 127).
15
+ def is_ascii?
16
+ self.length == self.tr("\200-\377", '').length
17
+ end
18
+
19
+ # Returns this string encoded as base64 as defined in RFC2045, section 6.8.
20
+ def encode_base64
21
+ [self].pack("m*")
22
+ end
23
+
24
+ # Performs encode_base64 in place, and returns the string.
25
+ def encode_base64!
26
+ self.replace(self.encode_base64)
27
+ end
28
+
29
+ # Returns this string decoded from base64 as defined in RFC2045, section 6.8.
30
+ def decode_base64
31
+ #self.unpack("m*").first
32
+ # This should be the above line but due to a bug in the ruby base64 decoder
33
+ # it will only decode base64 where the lines are in multiples of 4, this is
34
+ # contrary to RFC2045 which says that all characters other than the 65 used
35
+ # are to be ignored. Currently we remove all the other characters but it
36
+ # might be better to use it's advice to only remove line breaks and white
37
+ # space
38
+ self.tr("^A-Za-z0-9+/=", "").unpack("m*").first
39
+ end
40
+
41
+ # Performs decode_base64 in place, and returns the string.
42
+ def decode_base64!
43
+ self.replace(self.decode_base64)
44
+ end
45
+
46
+ # Returns this string encoded as quoted-printable as defined in RFC2045, section 6.7.
47
+ def encode_quoted_printable
48
+ result = [self].pack("M*")
49
+ # Ruby's quoted printable encoding uses soft line breaks to buffer spaces
50
+ # at the end of lines, rather than encoding them with =20. We fix this.
51
+ result.gsub!(/( +)=\n\n/) { "=20" * $1.length + "\n" }
52
+ # Ruby's quoted printable encode puts a soft line break on the end of any
53
+ # string that doesn't already end in a hard line break, so we have to
54
+ # clean it up.
55
+ result.gsub!(/=\n\Z/, '')
56
+ result
57
+ end
58
+
59
+ # Performs encode_quoted_printable in place, and returns the string.
60
+ def encode_quoted_printable!
61
+ self.replace(self.encode_quoted_printable)
62
+ end
63
+
64
+ # Returns this string decoded from quoted-printable as defined in RFC2045, section 6.7.
65
+ def decode_quoted_printable
66
+ self.unpack("M*").first
67
+ end
68
+
69
+ # Performs decode_quoted_printable in place, and returns the string.
70
+ def decode_quoted_printable!
71
+ self.replace(self.decode_quoted_printable)
72
+ end
73
+
74
+ # Guesses whether this string is encoded in base64 or quoted-printable.
75
+ #
76
+ # Returns either :base64 or :quoted_printable
77
+ def guess_mime_encoding
78
+ # Grab the first line and have a guess?
79
+ # A multiple of 4 and no characters that aren't in base64 ?
80
+ # Need to allow for = at end of base64 string
81
+ squashed = self.tr("\r\n\s", '').strip.sub(/=*\Z/, '')
82
+ if squashed.length.remainder(4) == 0 && squashed.count("^A-Za-z0-9+/") == 0
83
+ :base64
84
+ else
85
+ :quoted_printable
86
+ end
87
+ # or should we just try both and see what works?
88
+ end
89
+
90
+ # Returns the MIME encoding that is likely to produce the shortest
91
+ # encoded string, either :none, :base64, or :quoted_printable.
92
+ def best_mime_encoding
93
+ if self.is_ascii?
94
+ :none
95
+ elsif self.length > (self.mb_chars.length * 1.1)
96
+ :base64
97
+ else
98
+ :quoted_printable
99
+ end
100
+ end
101
+
102
+ # Decodes this string according to method, where method is
103
+ # :base64, :quoted_printable, or :none.
104
+ #
105
+ # If method is not supplied or is nil, guess_mime_encoding is used to
106
+ # try to pick an appropriate method.
107
+ #
108
+ # Method can also be a string: 'q', 'quoted-printable', 'b', or 'base64'
109
+ # This lets you pass in methods directly from Content-Transfer-Encoding
110
+ # headers, or from RFC2047 words. Matching is case-insensitive.
111
+ def decode_mime(method = nil)
112
+ method ||= guess_mime_encoding
113
+ method = method.downcase if method.kind_of?(String)
114
+ case method
115
+ when :none
116
+ self
117
+ when :base64, 'b', 'base64'
118
+ self.decode_base64
119
+ when :quoted_printable, 'q', 'quoted-printable'
120
+ self.decode_quoted_printable
121
+ else
122
+ raise ArgumentError, "Bad MIME encoding"
123
+ end
124
+ end
125
+
126
+ # Performs decode_mime in place, and returns the string.
127
+ def decode_mime!(method = nil)
128
+ self.replace(self.decode_mime(method))
129
+ end
130
+
131
+ # Converts this string to to_charset from from_charset using Iconv.
132
+ #
133
+ # Because there are cases where charsets are encoded incorrectly on the 'net
134
+ # we also allow for them and attempt to fix them up here. If conversion
135
+ # ultimately fails we remove all characters 0x80 and above, replacing them
136
+ # with ! symbols and effectively making it a US-ASCII (and therefore UTF-8)
137
+ # string.
138
+ def iconv(to_charset, from_charset)
139
+ failed = false
140
+ begin
141
+ converted = Iconv.new(to_charset, from_charset).iconv(self)
142
+ rescue Iconv::IllegalSequence
143
+ case from_charset.downcase
144
+ when 'us-ascii'
145
+ # Some mailers do not send a charset when it should be CP1252,
146
+ # the default Windows Latin charset
147
+ begin
148
+ converted = Iconv.new(to_charset, 'cp1252').iconv(self)
149
+ rescue Iconv::IllegalSequence
150
+ failed = true
151
+ end
152
+ when 'ks_c_5601-1987'
153
+ # Microsoft products erroneously use this for what should be CP949
154
+ # see http://tagunov.tripod.com/cjk.html
155
+ begin
156
+ converted = Iconv.new(to_charset, 'cp949').iconv(self)
157
+ rescue Iconv::IllegalSequence, Iconv::InvalidCharacter
158
+ failed = true
159
+ end
160
+ else
161
+ failed = true
162
+ end
163
+ rescue Iconv::InvalidCharacter
164
+ if self =~ /\n$/
165
+ # Some messages can come in with a superfluous new line on the end,
166
+ # which screws up the encoding. (ISO-2022-JP for example.)
167
+ begin
168
+ converted = Iconv.new(to_charset, 'iso-2022-jp').iconv(self.chomp) + "\n"
169
+ rescue Iconv::InvalidCharacter
170
+ converted = self.tr("\200-\377", "\041")
171
+ end
172
+ else
173
+ converted = self.tr("\200-\377", "\041")
174
+ end
175
+ end
176
+
177
+ if failed
178
+ begin
179
+ converted = Iconv.new(to_charset + '//IGNORE', from_charset).iconv(self)
180
+ rescue Iconv::InvalidCharacter
181
+ converted = self.tr("\200-\377", "\041")
182
+ end
183
+ end
184
+
185
+ converted
186
+ end
187
+
188
+ def iconv!(to_charset, from_charset)
189
+ self.replace(self.iconv(to_charset, from_charset))
190
+ end
191
+
192
+ end
@@ -0,0 +1,189 @@
1
+ class SMail #:nodoc:
2
+ class MIME < SMail
3
+ class ContentField
4
+ class << self
5
+ end
6
+
7
+ attr_accessor :type_raw # The raw type as parsed from the message.
8
+ attr_reader :params
9
+
10
+ def initialize(text = nil)
11
+ @params = Params.new
12
+ unless text.nil?
13
+ (@type_raw, params_raw) = SMail::MIME.decode_content_field(text)
14
+ @params.replace(params_raw)
15
+ end
16
+ end
17
+
18
+ # Returns the type as a lower case string
19
+ def type
20
+ @type_raw.nil? ? @type_raw : @type_raw.downcase
21
+ end
22
+
23
+ def type=(text)
24
+ @type_raw = text
25
+ end
26
+
27
+ # Returns the Content Field as a string suitable for inclusion in an email header.
28
+ def to_s
29
+ "#{@type_raw}#{self.params.to_s}"
30
+ end
31
+
32
+ class Params < Hash
33
+ class << self
34
+ # FIXME: These two should probably be moved
35
+ def needs_quoting?(text)
36
+ false # FIXME
37
+ end
38
+
39
+ def quote(text)
40
+ "\"#{text}\"" # FIXME
41
+ end
42
+ end
43
+
44
+ def to_s
45
+ if self.empty?
46
+ return ""
47
+ else
48
+ pairs = []
49
+ self.each do |key, value|
50
+ pair = key + '='
51
+ if SMail::MIME::ContentField::Params.needs_quoting?(value)
52
+ pair << SMail::MIME::ContentField::Params.quote(value)
53
+ else
54
+ pair << value
55
+ end
56
+ pairs << pair
57
+ end
58
+ end
59
+ '; ' + pairs.join('; ')
60
+ end
61
+ end
62
+ end # ContentField
63
+
64
+ # An object representing a Content-Disposition header as specified in RFC2183.
65
+ class ContentDisposition < ContentField
66
+
67
+ # Is this an inline part??
68
+ def inline?
69
+ type == 'inline'
70
+ end
71
+
72
+ # Is this an attachment part?
73
+ def attachment?
74
+ type == "attachment"
75
+ end
76
+
77
+ # Returns the filename if specified
78
+ def filename
79
+ self.params['filename']
80
+ end
81
+
82
+ # Returns the creation date if available or nil.
83
+ def creation_date
84
+ self.params['creation-date'] # FIXME: parse as a date?
85
+ end
86
+
87
+ # Returns the modification date if available or nil.
88
+ def modification_date
89
+ self.params['modification-date'] # FIXME: parse as a date?
90
+ end
91
+
92
+ # Returns the read date if available or nil.
93
+ def read_date
94
+ self.params['read-date'] # FIXME: parse as a date?
95
+ end
96
+
97
+ # Returns the size if available or nil.
98
+ def size
99
+ self.params['size']
100
+ end
101
+
102
+ # FIXME: add all the other parameters specified in RFC2183, also add setters
103
+
104
+ # Returns the Content-Disposition as a string suitable for inclusion in an email
105
+ # header. If no disposition type is specified it will default to a disposition
106
+ # type of 'attachment'.
107
+ def to_s
108
+ "#{self.type_raw || 'attachment'}#{self.params.to_s}"
109
+ end
110
+
111
+ end # ContentDisposition
112
+
113
+ class ContentType < ContentField
114
+
115
+ attr_accessor :media_type_raw, :media_subtype_raw
116
+
117
+ def initialize(text = nil)
118
+ super(text)
119
+ self.type = @type_raw
120
+ end
121
+
122
+ # Returns the media type
123
+ def media_type
124
+ @media_type_raw.nil? ? @media_type_raw : @media_type_raw.downcase
125
+ end
126
+
127
+ # Set the media type
128
+ def media_type=(text)
129
+ @media_type_raw = text
130
+ end
131
+
132
+ # Returns the media subtype
133
+ def media_subtype
134
+ @media_subtype.nil? ? @media_subtype_raw : @media_subtype_raw.downcase
135
+ end
136
+
137
+ # Set the media subtype
138
+ def media_subtype=(text)
139
+ @media_subtype_raw = text
140
+ end
141
+
142
+ # Returns the media 'type/subtype' as a lower case string.
143
+ def type
144
+ # Default to 'text/plain'
145
+ if @media_type_raw.nil? or @media_subtype_raw.nil?
146
+ 'text/plain'
147
+ else
148
+ "#{@media_type_raw}/#{@media_subtype_raw}".downcase
149
+ end
150
+ end
151
+
152
+ # Set the media 'type/subtype' together
153
+ def type=(text = nil)
154
+ unless text.nil?
155
+ @type_raw = text # keep this inherited accessor in sync
156
+ (@media_type_raw, @media_subtype_raw) = text.split('/', 2)
157
+ end
158
+ end
159
+
160
+ # Is this a composite media type as specified in section 5.1 of RFC 2045.
161
+ #
162
+ # Note extension tokens are permitted to be composite but will always be seen
163
+ # as discrete by this code.
164
+ def composite?
165
+ media_type == 'message' or media_type == 'multipart'
166
+ end
167
+
168
+ # Is this a discrete media type as specified in section 5.1 of RFC 2045.
169
+ #
170
+ # Note extension tokens are permitted to be composite but will always be seen
171
+ # as discrete by this code.
172
+ def discrete?
173
+ !composite?
174
+ end
175
+
176
+ # Returns the full Content-Type header as a string suitable for inclusion in an
177
+ # email header. If either of the media type or subtype are not specified it will
178
+ # default to 'text/plain; charset=us-ascii'.
179
+ def to_s
180
+ # Default to 'text/plain; charset=us-ascii'
181
+ if @media_type_raw.nil? or @media_subtype_raw.nil?
182
+ 'text/plain; charset=us-ascii'
183
+ else
184
+ "#{@media_type_raw}/#{@media_subtype_raw}#{self.params.to_s}"
185
+ end
186
+ end
187
+ end # ContentType
188
+ end
189
+ end
@@ -0,0 +1,13 @@
1
+ class SMail #:nodoc:
2
+ class MIME < SMail
3
+ # We inherit from DateTime in order to make its to_s method return an RFC2822
4
+ # compliant date string.
5
+ class Date < DateTime
6
+ # Return an RFC2822 compliant date string suitable for use in the Date header.
7
+ def to_s
8
+ # This should meet RFC2822 requirements
9
+ self.strftime('%a, %e %b %Y %H:%M:%S %z').gsub(/\s+/, ' ')
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,190 @@
1
+ require 'strscan'
2
+
3
+ class SMail #:nodoc:
4
+ class MIME < SMail
5
+
6
+ PATTERN_RFC2047_FIELD = '(.*?)(=\?(?:[^?]+)\?(?:.)\?(?:[^?]*)\?=)(.*)'
7
+
8
+ class << self
9
+ # Parses a Content-* header and returns the bits.
10
+ #
11
+ # Given the contents of a header field such as
12
+ #
13
+ # text/plain; charset=US-ASCII
14
+ #
15
+ # this will return:
16
+ #
17
+ # [ 'text', 'plain', { 'charset' => 'US-ASCII' } ]
18
+ #
19
+ # The type and the keys of the hash are all converted to lower case.
20
+ #
21
+ # This parses Content-Type and Content-Disposition headers according to
22
+ # the description of the Content-Type header in section 5.1 of RFC2045.
23
+ #
24
+ # It also handles continuations and character sets in parameter values
25
+ # as described in sections 3 and 4 of RFC2231.
26
+ def decode_content_field(text)
27
+ s = StringScanner.new(text)
28
+
29
+ type = s.scan(/[^;]*/)
30
+ s.skip(/;\s*/)
31
+
32
+ params = {}
33
+ charsets = {}
34
+ while key = s.scan(/[^=]+/)
35
+ s.skip(/=/)
36
+ if s.skip(/"/)
37
+ # Deal with quoted parameters.
38
+ value = s.scan(/(\\.|[^"])*/)
39
+ s.skip(/"/)
40
+ value.gsub!(/\\(.)/, '\1')
41
+ else
42
+ value = s.scan(/[^;\s]+/)
43
+ end
44
+
45
+ is_encoded = false
46
+ if key =~ /^(.*)\*$/
47
+ key = $1
48
+ is_encoded = true
49
+ end
50
+
51
+ is_continued = false
52
+ if key =~ /^(.*)\*[0-9]+$/
53
+ key = $1
54
+ is_continued = true
55
+ end
56
+
57
+ if is_encoded
58
+ # Deal with character sets and languages.
59
+ if value =~ /^([^']*)'([^']*)'(.*)$/
60
+ charsets[key] = ($1 or 'US-ASCII')
61
+ value = $3
62
+ end
63
+ value.gsub!(/%([[:xdigit:]]{2})/) { $1.hex.chr }
64
+ value.iconv!(charsets[key], 'UTF-8')
65
+ end
66
+
67
+ if is_continued and params[key]
68
+ # Deal with parameter continuations.
69
+ params[key] << value
70
+ else
71
+ params[key] = value
72
+ end
73
+
74
+ s.skip(/\s*;?\s*/) # skip any whitespace before and after a semicolon
75
+ end
76
+
77
+ # Some mail clients (I'm looking at you Becky!) don't use RFC2231 parameter
78
+ # value character set information but instead encode the parameters as
79
+ # RFC2047 fields, so lets cycle through them and try to decode, this should
80
+ # not do any harm if they don't have encoded fields
81
+ params.each_key {|key|
82
+ params[key] = self.decode_field(params[key])
83
+ }
84
+
85
+ [type, params]
86
+ end
87
+
88
+ # Decodes any RFC2047 words in a string and returns the string as UTF-8.
89
+ # Uses our iconv to deal with common encoding problems
90
+ def decode_field(text)
91
+ return nil if text.nil?
92
+ result = ''
93
+ while text =~ Regexp.new(PATTERN_RFC2047_FIELD, Regexp::MULTILINE)
94
+ prefix, encoded, text = $1, $2, $3
95
+ result << prefix unless prefix =~ Regexp.new('\A\s*\Z', Regexp::MULTILINE)
96
+ result << decode_word(encoded)
97
+ end
98
+ result << text
99
+ result
100
+ end
101
+
102
+ # Decodes an RFC2047 word to a UTF-8 string.
103
+ # Uses our iconv to deal with common encoding problems
104
+ def decode_word(text)
105
+ return text unless text =~ /=\?([^?]+)\?(.)\?([^?]*)\?=/
106
+
107
+ charset, method, encoded_string = $1, $2, $3
108
+
109
+ # Strip out the RFC2231 language specification if there is one.
110
+ charset = $1 if charset =~ /^([^\*]+)\*?(.*)$/
111
+
112
+ # Quoted-printable in RFC2047 substitutes spaces with underscores.
113
+ encoded_string.tr!('_', ' ') if method.downcase == 'q'
114
+
115
+ encoded_string.decode_mime(method).iconv('utf-8', charset)
116
+ end
117
+
118
+ # Takes the given UTF-8 string, converts it the given character set, and
119
+ # encodes it as an RFC2047 style field.
120
+ #
121
+ # All arguments after text are optional. If a method is not supplied,
122
+ # the String.best_mime_encoding method is used to pick one. The charset
123
+ # defaults to UTF-8, and the line length to 66 characters.
124
+ def encode_field(text, method = nil, charset = 'UTF-8', line_length = 66)
125
+ return '' if text.nil?
126
+ method ||= text.best_mime_encoding
127
+ method = method.downcase if method.kind_of?(String)
128
+ case method
129
+ when :none
130
+ text
131
+ when :base64, 'b', 'base64'
132
+ encode_base64_field(text, charset, line_length)
133
+ when :quoted_printable, 'q', 'quoted-printable'
134
+ encode_quoted_printable_field(text, charset, line_length)
135
+ else
136
+ raise ArgumentError, "Bad MIME encoding"
137
+ end
138
+ end
139
+
140
+ def encode_quoted_printable_field(text, charset = 'UTF-8', line_length = 66) #:nodoc:
141
+ charset.upcase!
142
+ encoded_line_length = line_length - (charset.length + 7)
143
+
144
+ iconv = Iconv.new(charset, 'UTF-8')
145
+ encoded_text = ''
146
+ word = ''
147
+ text.each_char do |char|
148
+ char = iconv.iconv(char)
149
+ # RFC2047 has its own ideas about quoted-printable encoding.
150
+ char.encode_quoted_printable!
151
+ char = case char
152
+ when "_": "=5F"
153
+ when " ": "_"
154
+ when "?": "=3F"
155
+ when "\t": "=09"
156
+ else char
157
+ end
158
+ if word.length + char.length > encoded_line_length
159
+ encoded_text << "=?#{charset}?Q?#{word}?=\n "
160
+ word = ''
161
+ end
162
+ word << char
163
+ end
164
+ encoded_text << "=?#{charset}?Q?#{word}?="
165
+ encoded_text
166
+ end
167
+
168
+ def encode_base64_field(text, charset = 'UTF-8', line_length = 66) #:nodoc:
169
+ charset.upcase!
170
+ unencoded_line_length = (line_length - (charset.length + 7)) / 4 * 3
171
+
172
+ iconv = Iconv.new(charset, 'UTF-8')
173
+ encoded_text = ''
174
+ word = ''
175
+ text.each_char do |char|
176
+ char = iconv.iconv(char)
177
+ if word.length + char.length > unencoded_line_length
178
+ encoded_text << "=?#{charset}?B?#{word.encode_base64.chomp}?=\n "
179
+ word = ''
180
+ end
181
+ word << char
182
+ end
183
+ encoded_text << "=?#{charset}?B?#{word.encode_base64.chomp}?="
184
+ encoded_text
185
+ end
186
+
187
+ end # self
188
+ end
189
+
190
+ end
@@ -0,0 +1,257 @@
1
+ class SMail
2
+ class MIME < SMail
3
+ class << self
4
+ end
5
+
6
+ attr_accessor :parts, :preamble, :epilogue
7
+ attr_reader :content_type, :boundary
8
+
9
+ def initialize(text = '')
10
+ super(text)
11
+ self.content_type = self.header('content-type')
12
+ fill_parts
13
+ end
14
+
15
+ # Returns the size of the message in bytes.
16
+ def size
17
+ self.to_s.length
18
+ end
19
+
20
+ # Sets the content type
21
+ def content_type=(content_type)
22
+ case content_type
23
+ when SMail::MIME::ContentType
24
+ @content_type = content_type
25
+ when String
26
+ self.content_type = SMail::MIME::ContentType.new(content_type)
27
+ when nil
28
+ self.content_type = SMail::MIME::ContentType.new
29
+ else
30
+ raise ArgumentError
31
+ end
32
+ end
33
+
34
+ # Is this a multipart message
35
+ def multipart?
36
+ @content_type.composite?
37
+ end
38
+
39
+ # Returns the MIME-Version as a string (unlikely to be anything but '1.0')
40
+ def version
41
+ self.header('mime-version') || '1.0'
42
+ end
43
+
44
+ # Returns the subject in UTF-8
45
+ def subject
46
+ SMail::MIME.decode_field(subject_raw)
47
+ end
48
+
49
+ # Sets the subject, performs any necessary encoding
50
+ def subject=(text)
51
+ self.subject_raw = SMail::MIME.encode_field(text)
52
+ end
53
+
54
+ # Returns the raw potentially MIME encoded subject
55
+ def subject_raw
56
+ self.header('subject')
57
+ end
58
+
59
+ # Set the subject directly, any necessary MIME encoding is up to the caller
60
+ def subject_raw=(text)
61
+ self.header_set('subject', text)
62
+ end
63
+
64
+ # Returns the date from the Date header as a DateTime object.
65
+ def date
66
+ date = self.header('date')
67
+ return nil unless date
68
+ SMail::MIME::Date.parse(date)
69
+ #(year, month, day, hour, minute, second, timezone, weekday) = ParseDate.parsedate(date)
70
+ #Time.gm(second, minute, hour, day, month, year, weekday, nil, nil, timezone)
71
+ end
72
+
73
+
74
+ # Returns the raw body of the email including all parts
75
+ alias body_raw body
76
+
77
+ # Returns the body decoded and converted to UTF-8 if necessary, if this is is a
78
+ # multipart message this is not what you suspect
79
+ def body
80
+ if self.multipart? # what if it is message/rfc822 ?
81
+ @preamble
82
+ else
83
+ # decode
84
+ case self.header('content-transfer-encoding')
85
+ when 'quoted-printable'
86
+ body = @body.decode_quoted_printable
87
+ when 'base64'
88
+ body = @body.decode_base64
89
+ else
90
+ # matches nil when there is no header or an unrecognised encoding
91
+ body = @body
92
+ end
93
+
94
+ # convert to UTF-8 if text
95
+ if self.content_type.media_type == 'text'
96
+ charset = self.content_type.params['charset'] || 'us-ascii'
97
+ body.iconv!('utf-8', charset)
98
+ end
99
+
100
+ body
101
+ end
102
+ end
103
+
104
+ # Returns a string description of the MIME structure of this message.
105
+ #
106
+ # This is useful for debugging and testing. The returned string is
107
+ # formatted as shown in the following example:
108
+ # multipart/mixed
109
+ # multipart/alternative
110
+ # text/plain
111
+ # multipart/related
112
+ # text/html
113
+ # image/gif
114
+ # application/octet-stream
115
+ def describe_mime_structure(depth = 0)
116
+ result = (' '*depth) + self.content_type.type + "\n"
117
+ if self.multipart?
118
+ self.parts.each do |part|
119
+ result << part.describe_mime_structure(depth+1)
120
+ end
121
+ end
122
+ result.chomp! if depth == 0
123
+ result
124
+ end
125
+
126
+ # Pulls out any body parts matching the given MIME types and puts them
127
+ # into an array.
128
+ #
129
+ # This is useful for pulling out parts in the appropriate order for
130
+ # rendering. For example calling:
131
+ # message.flatten_body('text/plain', /^application\/.*$)
132
+ # should return all the text parts and attached files in the order in
133
+ # which they appear in the original message.
134
+ #
135
+ # The various multipart subtypes are handled sensibly. For example,
136
+ # for multipart/alternative messages, the best matching part (i.e. the
137
+ # last part consisting entirely of the given types) is used.
138
+ def flatten_body(*types)
139
+ types = types.flatten
140
+ if self.multipart?
141
+ case self.content_type.type
142
+ when 'multipart/alternative'
143
+ part = self.parts.reverse.find {|part| part.consists_of_mime_types?(types) }
144
+ part ? part.flatten_body(types) : []
145
+ when 'multipart/mixed', 'multipart/related'
146
+ # FIXME: For multipart/related, this should look for a start parameter and try that first.
147
+ parts = self.parts.collect {|part| part.flatten_body(types) }
148
+ parts.flatten
149
+ when 'multipart/signed'
150
+ self.parts.first.flatten_body(types)
151
+ when 'multipart/appledouble'
152
+ self.parts[1].flatten_body(types)
153
+ else
154
+ # FIXME: should we also have an entry for message/rfc822 etc.
155
+ []
156
+ end
157
+ else
158
+ self.consists_of_mime_types?(types) ? [self] : []
159
+ end
160
+ end
161
+
162
+ # Returns true if the message consists entirely of the given mime types.
163
+ #
164
+ # For single part messages this is simple: the Content-Type of the
165
+ # message must by one of the supplied types.
166
+ #
167
+ # For multipart messages it gets a bit more complicated. We try to
168
+ # make sure that the message can be entirely decomposed into
169
+ # just the supplied types.
170
+ #
171
+ # The rules are as follows:
172
+ # [multipart/alternative]
173
+ # At least one sub-part must consist of the given types.
174
+ # [multipart/mixed]
175
+ # All sub-parts must consist of the given types.
176
+ # [multipart/related]
177
+ # The root part (usually the first part) must consist of the
178
+ # given types.
179
+ # [multipart/signed]
180
+ # The first part must consist of the given types.
181
+ # [multipart/appledouble]
182
+ # The second part must consist of the given types. (See RFC 1740.)
183
+ def consists_of_mime_types?(*types)
184
+ types = types.flatten
185
+ type = self.content_type.type
186
+
187
+ if self.multipart?
188
+ case type
189
+ when 'multipart/alternative'
190
+ self.parts.any? {|part| part.consists_of_mime_types?(types) }
191
+ when 'multipart/mixed'
192
+ self.parts.all? {|part| part.consists_of_mime_types?(types) }
193
+ when 'multipart/related'
194
+ # FIXME: This should look for a start parameter and try that first.
195
+ self.parts.first.consists_of_mime_types?(types)
196
+ when 'multipart/signed'
197
+ self.parts.first.consists_of_mime_types?(types)
198
+ when 'multipart/appledouble'
199
+ self.parts[1].consists_of_mime_types?(types)
200
+ when 'message/rfc822', 'message/rfc2822'
201
+ self.parts.first.consists_of_mime_types?(types)
202
+ else
203
+ false
204
+ end
205
+ else
206
+ types.any? {|t| t === type }
207
+ end
208
+ end
209
+
210
+
211
+
212
+ private
213
+
214
+ def fill_parts
215
+ if self.content_type.discrete?
216
+ parts_single_part
217
+ else
218
+ parts_multipart
219
+ end
220
+ end
221
+
222
+ def parts_single_part
223
+ @parts = []
224
+ end
225
+
226
+ def parts_multipart
227
+ @parts = []
228
+ @boundary = self.content_type.params['boundary']
229
+
230
+ if self.content_type.type == 'message/rfc822' or self.content_type.type == 'message/rfc2822'
231
+ @parts << SMail::MIME.new(@body)
232
+ return @parts
233
+ end
234
+
235
+ return parts_single_part unless @boundary
236
+
237
+ #alias body_raw body # FIXME: does this work?
238
+
239
+ epilogue_re = Regexp.new("^--#{Regexp.escape(@boundary)}--\s*\r?$", Regexp::MULTILINE)
240
+ (body, @epilogue) = @body.split(epilogue_re, 2)
241
+ @epilogue.lstrip! unless @epilogue.nil?
242
+
243
+ bits_re = Regexp.new("^--#{Regexp.escape(@boundary)}\s*\r?$", Regexp::MULTILINE)
244
+ bits = body.split(bits_re)
245
+
246
+ @preamble = bits.shift # FIXME is this OK? or better to see a header in the first line?
247
+
248
+ bits.each do |bit|
249
+ bit.lstrip!
250
+ @parts << SMail::MIME.new(bit)
251
+ end
252
+
253
+ @parts
254
+ end
255
+
256
+ end
257
+ end
@@ -0,0 +1,11 @@
1
+ class SMail
2
+ class MIME
3
+ module VERSION #:nodoc:
4
+ MAJOR = 0
5
+ MINOR = 0
6
+ TINY = 5
7
+
8
+ STRING = [MAJOR, MINOR, TINY].join('.')
9
+ end
10
+ end
11
+ end
data/lib/smail/mime.rb ADDED
@@ -0,0 +1,4 @@
1
+ require 'smail'
2
+ require 'active_support'
3
+
4
+ Dir[File.join(File.dirname(__FILE__), 'mime/**/*.rb')].sort.each { |lib| require lib }
metadata ADDED
@@ -0,0 +1,90 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: smail-mime
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.6
5
+ platform: ruby
6
+ authors:
7
+ - Matthew Walker
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-11-17 00:00:00 +11:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: smail
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 0.0.5
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: activesupport
27
+ type: :runtime
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 2.0.0
34
+ version:
35
+ - !ruby/object:Gem::Dependency
36
+ name: rspec
37
+ type: :development
38
+ version_requirement:
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: 1.0.5
44
+ version:
45
+ description:
46
+ email: matthew@walker.wattle.id.au
47
+ executables: []
48
+
49
+ extensions: []
50
+
51
+ extra_rdoc_files: []
52
+
53
+ files:
54
+ - lib/smail/mime.rb
55
+ - lib/smail/mime/coding_extensions.rb
56
+ - lib/smail/mime/content_fields.rb
57
+ - lib/smail/mime/date.rb
58
+ - lib/smail/mime/header.rb
59
+ - lib/smail/mime/mime.rb
60
+ - lib/smail/mime/version.rb
61
+ has_rdoc: true
62
+ homepage: http://github.com/mwalker/smail-mime
63
+ licenses: []
64
+
65
+ post_install_message:
66
+ rdoc_options: []
67
+
68
+ require_paths:
69
+ - lib
70
+ required_ruby_version: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: "0"
75
+ version:
76
+ required_rubygems_version: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ version: "0"
81
+ version:
82
+ requirements: []
83
+
84
+ rubyforge_project:
85
+ rubygems_version: 1.3.5
86
+ signing_key:
87
+ specification_version: 3
88
+ summary: A simple MIME email parser
89
+ test_files: []
90
+