1# = uri/mailto.rb
2#
3# Author:: Akira Yamada <akira@ruby-lang.org>
4# License:: You can redistribute it and/or modify it under the same term as Ruby.
5# Revision:: $Id: mailto.rb 34360 2012-01-23 08:12:52Z naruse $
6#
7# See URI for general documentation
8#
9
10require 'uri/generic'
11
12module URI
13
14  #
15  # RFC2368, The mailto URL scheme
16  #
17  class MailTo < Generic
18    include REGEXP
19
20    # A Default port of nil for URI::MailTo
21    DEFAULT_PORT = nil
22
23    # An Array of the available components for URI::MailTo
24    COMPONENT = [ :scheme, :to, :headers ].freeze
25
26    # :stopdoc:
27    #  "hname" and "hvalue" are encodings of an RFC 822 header name and
28    #  value, respectively. As with "to", all URL reserved characters must
29    #  be encoded.
30    #
31    #  "#mailbox" is as specified in RFC 822 [RFC822]. This means that it
32    #  consists of zero or more comma-separated mail addresses, possibly
33    #  including "phrase" and "comment" components. Note that all URL
34    #  reserved characters in "to" must be encoded: in particular,
35    #  parentheses, commas, and the percent sign ("%"), which commonly occur
36    #  in the "mailbox" syntax.
37    #
38    #  Within mailto URLs, the characters "?", "=", "&" are reserved.
39
40    # hname      =  *urlc
41    # hvalue     =  *urlc
42    # header     =  hname "=" hvalue
43    HEADER_PATTERN = "(?:[^?=&]*=[^?=&]*)".freeze
44    HEADER_REGEXP  = Regexp.new(HEADER_PATTERN).freeze
45    # headers    =  "?" header *( "&" header )
46    # to         =  #mailbox
47    # mailtoURL  =  "mailto:" [ to ] [ headers ]
48    MAILBOX_PATTERN = "(?:#{PATTERN::ESCAPED}|[^(),%?=&])".freeze
49    MAILTO_REGEXP = Regexp.new(" # :nodoc:
50      \\A
51      (#{MAILBOX_PATTERN}*?)                          (?# 1: to)
52      (?:
53        \\?
54        (#{HEADER_PATTERN}(?:\\&#{HEADER_PATTERN})*)  (?# 2: headers)
55      )?
56      (?:
57        \\#
58        (#{PATTERN::FRAGMENT})                        (?# 3: fragment)
59      )?
60      \\z
61    ", Regexp::EXTENDED).freeze
62    # :startdoc:
63
64    #
65    # == Description
66    #
67    # Creates a new URI::MailTo object from components, with syntax checking.
68    #
69    # Components can be provided as an Array or Hash. If an Array is used,
70    # the components must be supplied as [to, headers].
71    #
72    # If a Hash is used, the keys are the component names preceded by colons.
73    #
74    # The headers can be supplied as a pre-encoded string, such as
75    # "subject=subscribe&cc=address", or as an Array of Arrays like
76    # [['subject', 'subscribe'], ['cc', 'address']]
77    #
78    # Examples:
79    #
80    #    require 'uri'
81    #
82    #    m1 = URI::MailTo.build(['joe@example.com', 'subject=Ruby'])
83    #    puts m1.to_s  ->  mailto:joe@example.com?subject=Ruby
84    #
85    #    m2 = URI::MailTo.build(['john@example.com', [['Subject', 'Ruby'], ['Cc', 'jack@example.com']]])
86    #    puts m2.to_s  ->  mailto:john@example.com?Subject=Ruby&Cc=jack@example.com
87    #
88    #    m3 = URI::MailTo.build({:to => 'listman@example.com', :headers => [['subject', 'subscribe']]})
89    #    puts m3.to_s  ->  mailto:listman@example.com?subject=subscribe
90    #
91    def self.build(args)
92      tmp = Util::make_components_hash(self, args)
93
94      if tmp[:to]
95        tmp[:opaque] = tmp[:to]
96      else
97        tmp[:opaque] = ''
98      end
99
100      if tmp[:headers]
101        tmp[:opaque] << '?'
102
103        if tmp[:headers].kind_of?(Array)
104          tmp[:opaque] << tmp[:headers].collect { |x|
105            if x.kind_of?(Array)
106              x[0] + '=' + x[1..-1].join
107            else
108              x.to_s
109            end
110          }.join('&')
111
112        elsif tmp[:headers].kind_of?(Hash)
113          tmp[:opaque] << tmp[:headers].collect { |h,v|
114            h + '=' + v
115          }.join('&')
116
117        else
118          tmp[:opaque] << tmp[:headers].to_s
119        end
120      end
121
122      return super(tmp)
123    end
124
125    #
126    # == Description
127    #
128    # Creates a new URI::MailTo object from generic URL components with
129    # no syntax checking.
130    #
131    # This method is usually called from URI::parse, which checks
132    # the validity of each component.
133    #
134    def initialize(*arg)
135      super(*arg)
136
137      @to = nil
138      @headers = []
139
140      if MAILTO_REGEXP =~ @opaque
141        if arg[-1]
142          self.to = $1
143          self.headers = $2
144        else
145          set_to($1)
146          set_headers($2)
147        end
148
149      else
150        raise InvalidComponentError,
151          "unrecognised opaque part for mailtoURL: #{@opaque}"
152      end
153    end
154
155    # The primary e-mail address of the URL, as a String
156    attr_reader :to
157
158    # E-mail headers set by the URL, as an Array of Arrays
159    attr_reader :headers
160
161    # check the to +v+ component against either
162    # * URI::Parser Regexp for :OPAQUE
163    # * MAILBOX_PATTERN
164    def check_to(v)
165      return true unless v
166      return true if v.size == 0
167
168      if parser.regexp[:OPAQUE] !~ v || /\A#{MAILBOX_PATTERN}*\z/o !~ v
169        raise InvalidComponentError,
170          "bad component(expected opaque component): #{v}"
171      end
172
173      return true
174    end
175    private :check_to
176
177    # private setter for to +v+
178    def set_to(v)
179      @to = v
180    end
181    protected :set_to
182
183    # setter for to +v+
184    def to=(v)
185      check_to(v)
186      set_to(v)
187      v
188    end
189
190    # check the headers +v+ component against either
191    # * URI::Parser Regexp for :OPAQUE
192    # * HEADER_PATTERN
193    def check_headers(v)
194      return true unless v
195      return true if v.size == 0
196
197      if parser.regexp[:OPAQUE] !~ v ||
198          /\A(#{HEADER_PATTERN}(?:\&#{HEADER_PATTERN})*)\z/o !~ v
199        raise InvalidComponentError,
200          "bad component(expected opaque component): #{v}"
201      end
202
203      return true
204    end
205    private :check_headers
206
207    # private setter for headers +v+
208    def set_headers(v)
209      @headers = []
210      if v
211        v.scan(HEADER_REGEXP) do |x|
212          @headers << x.split(/=/o, 2)
213        end
214      end
215    end
216    protected :set_headers
217
218    # setter for headers +v+
219    def headers=(v)
220      check_headers(v)
221      set_headers(v)
222      v
223    end
224
225    # Constructs String from URI
226    def to_s
227      @scheme + ':' +
228        if @to
229          @to
230        else
231          ''
232        end +
233        if @headers.size > 0
234          '?' + @headers.collect{|x| x.join('=')}.join('&')
235        else
236          ''
237        end +
238        if @fragment
239          '#' + @fragment
240        else
241          ''
242        end
243    end
244
245    # Returns the RFC822 e-mail text equivalent of the URL, as a String.
246    #
247    # Example:
248    #
249    #   require 'uri'
250    #
251    #   uri = URI.parse("mailto:ruby-list@ruby-lang.org?Subject=subscribe&cc=myaddr")
252    #   uri.to_mailtext
253    #   # => "To: ruby-list@ruby-lang.org\nSubject: subscribe\nCc: myaddr\n\n\n"
254    #
255    def to_mailtext
256      to = parser.unescape(@to)
257      head = ''
258      body = ''
259      @headers.each do |x|
260        case x[0]
261        when 'body'
262          body = parser.unescape(x[1])
263        when 'to'
264          to << ', ' + parser.unescape(x[1])
265        else
266          head << parser.unescape(x[0]).capitalize + ': ' +
267            parser.unescape(x[1])  + "\n"
268        end
269      end
270
271      return "To: #{to}
272#{head}
273#{body}
274"
275    end
276    alias to_rfc822text to_mailtext
277  end
278
279  @@schemes['MAILTO'] = MailTo
280end
281