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