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