sequoia_openpgp/packet/
userid.rs

1use std::borrow::Cow;
2use std::fmt;
3use std::str;
4use std::hash::{Hash, Hasher};
5use std::cmp::Ordering;
6use std::sync::OnceLock;
7
8#[cfg(test)]
9use quickcheck::{Arbitrary, Gen};
10
11use regex::Regex;
12
13use crate::Result;
14use crate::packet;
15use crate::Packet;
16use crate::Error;
17use crate::policy::HashAlgoSecurity;
18
19/// A conventionally parsed UserID.
20#[derive(Clone, Debug)]
21pub struct ConventionallyParsedUserID {
22    userid: String,
23
24    name: Option<(usize, usize)>,
25    comment: Option<(usize, usize)>,
26    email: Option<(usize, usize)>,
27    uri: Option<(usize, usize)>,
28}
29assert_send_and_sync!(ConventionallyParsedUserID);
30
31impl ConventionallyParsedUserID {
32    /// Parses the userid according to the usual conventions.
33    pub fn new<S>(userid: S) -> Result<Self>
34        where S: Into<String>
35    {
36        Ok(Self::parse(userid.into())?)
37    }
38
39    /// Returns the User ID's name component, if any.
40    pub fn name(&self) -> Option<&str> {
41        self.name.map(|(s, e)| &self.userid[s..e])
42    }
43
44    /// Returns the User ID's comment field, if any.
45    pub fn comment(&self) -> Option<&str> {
46        self.comment.map(|(s, e)| &self.userid[s..e])
47    }
48
49    /// Returns the User ID's email component, if any.
50    pub fn email(&self) -> Option<&str> {
51        self.email.map(|(s, e)| &self.userid[s..e])
52    }
53
54    /// Returns the User ID's URI component, if any.
55    ///
56    /// Note: the URI is returned as is; dot segments are not removed,
57    /// escape sequences are not unescaped, etc.
58    pub fn uri(&self) -> Option<&str> {
59        self.uri.map(|(s, e)| &self.userid[s..e])
60    }
61
62    fn parse(userid: String) -> Result<Self> {
63        fn user_id_parser() -> &'static Regex {
64            use std::sync::OnceLock;
65            static USER_ID_PARSER: OnceLock<Regex> = OnceLock::new();
66            USER_ID_PARSER.get_or_init(|| {
67                // Whitespace.
68                let ws_bare = " ";
69                let ws = format!("[{}]", ws_bare);
70                let optional_ws = format!("(?:{}*)", ws);
71
72                // Specials minus ( and ).
73                let comment_specials_bare = r#"<>\[\]:;@\\,.""#;
74                let _comment_specials
75                    = format!("[{}]", comment_specials_bare);
76
77                let atext_specials_bare = r#"()\[\]:;@\\,.""#;
78                let _atext_specials =
79                    format!("[{}]", atext_specials_bare);
80
81                // "Text"
82                let atext_bare
83                    = "-A-Za-z0-9!#$%&'*+/=?^_`{|}~\u{80}-\u{10ffff}";
84                let atext = format!("[{}]", atext_bare);
85
86                // An atext with dots and the added restriction that
87                // it may not start or end with a dot.
88                let dot_atom_text
89                    = format!(r"(?:{}+(?:\.{}+)*)", atext, atext);
90
91
92                let name_char_start
93                    = format!("[{}{}]",
94                              atext_bare, atext_specials_bare);
95                let name_char_rest
96                    = format!("[{}{}{}]",
97                              atext_bare, atext_specials_bare, ws_bare);
98                // We need to minimize the match as otherwise we
99                // swallow any comment.
100                let name
101                    = format!("(?:{}{}*?)", name_char_start, name_char_rest);
102
103                let comment_char
104                    = format!("[{}{}{}]",
105                              atext_bare, comment_specials_bare, ws_bare);
106
107                let comment = |prefix| {
108                    format!(r#"(?:\({}(?P<{}_comment>{}*?){}\))"#,
109                            optional_ws, prefix, comment_char, optional_ws)
110                };
111
112                let addr_spec
113                    = format!("(?:{}@{})", dot_atom_text, dot_atom_text);
114
115                let uri = |prefix| {
116                    // The regex suggested from the RFC:
117                    //
118                    // ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?
119                    //   ^schema         ^authority ^path      ^query     ^fragment
120                    //
121                    // Since only the path component is required, and
122                    // the path matches everything but the '?' and '#'
123                    // characters, this regular expression will match
124                    // almost any string.
125                    //
126                    // This regular expression is good for picking
127                    // apart strings that are known to be URIs.  But,
128                    // we want to detect URIs and distinguish them
129                    // from things that are almost certainly not URIs,
130                    // like email addresses.
131                    //
132                    // As such, we require the URI to have a
133                    // well-formed schema, and the schema must be
134                    // followed by a non-empty component.  Further, we
135                    // restrict the alphabet to approximately what the
136                    // grammar permits.
137
138                    // Looking at the productions for the schema,
139                    // authority, path, query, and fragment
140                    // components, we can distil the following useful
141                    // alphabets (the symbols are drawn from the
142                    // following pct-encoded, unreserved, gen-delims,
143                    // sub-delims, pchar, and IP-literal productions):
144                    let symbols = "-{}0-9._~%!$&'()*+,;=:@\\[\\]";
145                    let ascii_alpha = "a-zA-Z";
146                    let utf8_alpha = "a-zA-Z\u{80}-\u{10ffff}";
147
148                    // We strictly match the schema production:
149                    //
150                    // scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
151                    let schema
152                        = format!("(?:[{}][-+.{}0-9]*:)",
153                                  ascii_alpha, ascii_alpha);
154
155                    // The symbols that can occur in a fragment are a
156                    // superset of those that can occur in a query and
157                    // its delimiters.  Likewise, the symbols that can
158                    // occur in a query are a superset of those that
159                    // can occur in a path and its delimiters.  The
160                    // symbols that can occur in a path are *almost* a
161                    // subset of those that can occur in an authority:
162                    // '[' and ']' can occur in an authority component
163                    // (via the IP-literal production, e.g.,
164                    // '[2001:db8::7]'), but not in a path.  But, URI
165                    // parsers appear to accept '[' and ']' as part of
166                    // a path.  So, we accept them too.
167                    //
168                    // Given this, a fragment matches all components
169                    // and everything that precedes it.  Since we
170                    // don't need to distinguish the individual parts
171                    // here, matching what follows the schema in a URI
172                    // is straightforward:
173                    let rest = format!("(?:[{}{}/\\?#]+)",
174                                       symbols, utf8_alpha);
175
176                    format!("(?P<{}_uri>{}{})",
177                            prefix, schema, rest)
178                };
179
180                let raw_addr_spec
181                    = format!("(?P<raw_addr_spec>{})", addr_spec);
182
183                let raw_uri = format!("(?:{})", uri("raw"));
184
185                // whitespace is ignored.  It is allowed (but not
186                // required) at the start and between components, but
187                // it is not allowed after the closing '>'.  space is
188                // not allowed.
189                let wrapped_addr_spec
190                    = format!("{}(?P<wrapped_addr_spec_name>{})?{}\
191                               (?:{})?{}\
192                               <(?P<wrapped_addr_spec>{})>",
193                              optional_ws, name, optional_ws,
194                              comment("wrapped_addr_spec"), optional_ws,
195                              addr_spec);
196
197                let wrapped_uri
198                    = format!("{}(?P<wrapped_uri_name>{})?{}\
199                               (?:{})?{}\
200                               <(?:{})>",
201                              optional_ws, name, optional_ws,
202                              comment("wrapped_uri"), optional_ws,
203                              uri("wrapped"));
204
205                let bare_name
206                    = format!("{}(?P<bare_name>{}){}\
207                               (?:{})?{}",
208                              optional_ws, name, optional_ws,
209                              comment("bare"), optional_ws);
210
211                // Note: bare-name has to come after addr-spec-raw as
212                // prefer addr-spec-raw to bare-name when the match is
213                // ambiguous.
214                let pgp_uid_convention
215                    = format!("^(?:{}|{}|{}|{}|{})$",
216                              raw_addr_spec, raw_uri,
217                              wrapped_addr_spec, wrapped_uri,
218                              bare_name);
219
220                Regex::new(&pgp_uid_convention).unwrap()
221            })
222        }
223
224        // The regex is anchored at the start and at the end so we
225        // have either 0 or 1 matches.
226        if let Some(cap) = user_id_parser().captures_iter(&userid).next() {
227            let to_range = |m: regex::Match| (m.start(), m.end());
228
229            // We need to figure out which branch matched.  Match on a
230            // required capture for each branch.
231
232            if let Some(email) = cap.name("raw_addr_spec") {
233                // raw-addr-spec
234                let email = Some(to_range(email));
235
236                Ok(ConventionallyParsedUserID {
237                    userid,
238                    name: None,
239                    comment: None,
240                    email,
241                    uri: None,
242                })
243            } else if let Some(uri) = cap.name("raw_uri") {
244                // raw-uri
245                let uri = Some(to_range(uri));
246
247                Ok(ConventionallyParsedUserID {
248                    userid,
249                    name: None,
250                    comment: None,
251                    email: None,
252                    uri,
253                })
254            } else if let Some(email) = cap.name("wrapped_addr_spec") {
255                // wrapped-addr-spec
256                let name = cap.name("wrapped_addr_spec_name").map(to_range);
257                let comment = cap.name("wrapped_addr_spec_comment").map(to_range);
258                let email = Some(to_range(email));
259
260                Ok(ConventionallyParsedUserID {
261                    userid,
262                    name,
263                    comment,
264                    email,
265                    uri: None,
266                })
267            } else if let Some(uri) = cap.name("wrapped_uri") {
268                // uri-wrapped
269                let name = cap.name("wrapped_uri_name").map(to_range);
270                let comment = cap.name("wrapped_uri_comment").map(to_range);
271                let uri = Some(to_range(uri));
272
273                Ok(ConventionallyParsedUserID {
274                    userid,
275                    name,
276                    comment,
277                    email: None,
278                    uri,
279                })
280            } else if let Some(name) = cap.name("bare_name") {
281                // name-bare
282                let name = to_range(name);
283                let comment = cap.name("bare_comment").map(to_range);
284
285                Ok(ConventionallyParsedUserID {
286                    userid,
287                    name: Some(name),
288                    comment,
289                    email: None,
290                    uri: None,
291                })
292            } else {
293                panic!("Unexpected result");
294            }
295        } else {
296            Err(Error::InvalidArgument(
297                "Failed to parse UserID".into()).into())
298        }
299    }
300}
301
302/// Holds a UserID packet.
303///
304/// The standard imposes no structure on UserIDs, but suggests to
305/// follow [RFC 2822].  See [Section 5.11 of RFC 9580] for details.
306/// In practice though, implementations do not follow [RFC 2822], or
307/// do not even help their users in producing well-formed User IDs.
308/// Experience has shown that parsing User IDs using [RFC 2822] does
309/// not work, so we are taking a more pragmatic approach and define
310/// what we call *Conventional User IDs*.
311///
312///   [RFC 2822]: https://tools.ietf.org/html/rfc2822
313///   [Section 5.11 of RFC 9580]: https://www.rfc-editor.org/rfc/rfc9580.html#section-5.11
314///
315/// Using this definition, we provide methods to extract the [name],
316/// [comment], [email address], or [URI] from `UserID` packets.
317/// Furthermore, we provide a way to [canonicalize the email address]
318/// found in a `UserID` packet.  We provide [two] [constructors] that
319/// create well-formed User IDs from email address, and optional name
320/// and comment.
321///
322///   [name]: UserID::name()
323///   [comment]: UserID::comment()
324///   [email address]: UserID::email()
325///   [URI]: UserID::uri()
326///   [canonicalize the email address]: UserID::email_normalized()
327///   [two]: UserID::from_address()
328///   [constructors]: UserID::from_unchecked_address()
329///
330/// # Conventional User IDs
331///
332/// Informally, conventional User IDs are of the form:
333///
334///   - `First Last (Comment) <name@example.org>`
335///   - `First Last <name@example.org>`
336///   - `First Last`
337///   - `name@example.org <name@example.org>`
338///   - `<name@example.org>`
339///   - `name@example.org`
340///
341///   - `Name (Comment) <scheme://hostname/path>`
342///   - `Name (Comment) <mailto:user@example.org>`
343///   - `Name <scheme://hostname/path>`
344///   - `<scheme://hostname/path>`
345///   - `scheme://hostname/path`
346///
347/// Names consist of UTF-8 non-control characters and may include
348/// punctuation.  For instance, the following names are valid:
349///
350///   - `Acme Industries, Inc.`
351///   - `Michael O'Brian`
352///   - `Smith, John`
353///   - `e.e. cummings`
354///
355/// (Note: according to [RFC 2822] and its successors, all of these
356/// would need to be quoted.  Conventionally, no implementation quotes
357/// names.)
358///
359/// Conventional User IDs are UTF-8.  [RFC 2822] only covers US-ASCII
360/// and allows character set switching using [RFC 2047].  For example,
361/// an [RFC 2822] parser would parse:
362///
363///    - <code>Bj=?utf-8?q?=C3=B6?=rn Bj=?utf-8?q?=C3=B6?=rnson</code>
364///
365///   [RFC 2047]: https://tools.ietf.org/html/rfc2047
366///
367/// "Björn Björnson".  Nobody uses this in practice, and, as such,
368/// this extension is not supported by this parser.
369///
370/// Comments can include any UTF-8 text except parentheses.  Thus, the
371/// following is not a valid comment even though the parentheses are
372/// balanced:
373///
374///   - `(foo (bar))`
375///
376/// URIs
377/// ----
378///
379/// The URI parser recognizes URIs using a regular expression similar
380/// to the one recommended in [RFC 3986] with the following extensions
381/// and restrictions:
382///
383///   - UTF-8 characters are in the range `\u{80}-\u{10ffff}` are
384///     allowed wherever percent-encoded characters are allowed (i.e.,
385///     everywhere but the schema).
386///
387///   - The scheme component and its trailing `:` are required.
388///
389///   - The URI must have an authority component (`//domain`) or a
390///     path component (`/path/to/resource`).
391///
392///   - Although the RFC does not allow it, in practice, the `[` and
393///     `]` characters are allowed wherever percent-encoded characters
394///     are allowed (i.e., everywhere but the schema).
395///
396/// URIs are neither normalized nor interpreted.  For instance, dot
397/// segments are not removed, escape sequences are not decoded, etc.
398///
399/// Note: the recommended regular expression is less strict than the
400/// grammar.  For instance, a percent encoded character must consist
401/// of three characters: the percent character followed by two hex
402/// digits.  The parser that we use does not enforce this either.
403///
404///   [RFC 3986]: https://tools.ietf.org/html/rfc3986
405///
406/// Formal Grammar
407/// --------------
408///
409/// Formally, the following grammar is used to decompose a User ID:
410///
411/// ```text
412///   WS                 = 0x20 (space character)
413///
414///   comment-specials   = "<" / ">" /   ; RFC 2822 specials - "(" and ")"
415///                        "[" / "]" /
416///                        ":" / ";" /
417///                        "@" / "\" /
418///                        "," / "." /
419///                        DQUOTE
420///
421///   atext-specials     = "(" / ")" /   ; RFC 2822 specials - "<" and ">".
422///                        "[" / "]" /
423///                        ":" / ";" /
424///                        "@" / "\" /
425///                        "," / "." /
426///                        DQUOTE
427///
428///   atext              = ALPHA / DIGIT /   ; Any character except controls,
429///                        "!" / "#" /       ;  SP, and specials.
430///                        "$" / "%" /       ;  Used for atoms
431///                        "&" / "'" /
432///                        "*" / "+" /
433///                        "-" / "/" /
434///                        "=" / "?" /
435///                        "^" / "_" /
436///                        "`" / "{" /
437///                        "|" / "}" /
438///                        "~" /
439///                        \u{80}-\u{10ffff} ; Non-ascii, non-control UTF-8
440///
441///   dot_atom_text      = 1*atext *("." *atext)
442///
443///   name-char-start    = atext / atext-specials
444///
445///   name-char-rest     = atext / atext-specials / WS
446///
447///   name               = name-char-start *name-char-rest
448///
449///   comment-char       = atext / comment-specials / WS
450///
451///   comment-content    = *comment-char
452///
453///   comment            = "(" *WS comment-content *WS ")"
454///
455///   addr-spec          = dot-atom-text "@" dot-atom-text
456///
457///   uri                = See [RFC 3986] and the note on URIs above.
458///
459///   pgp-uid-convention = addr-spec /
460///                        uri /
461///                        *WS [name] *WS [comment] *WS "<" addr-spec ">" /
462///                        *WS [name] *WS [comment] *WS "<" uri ">" /
463///                        *WS name *WS [comment] *WS
464/// ```
465pub struct UserID {
466    /// CTB packet header fields.
467    pub(crate) common: packet::Common,
468    /// The user id.
469    ///
470    /// According to [Section 5.11 of RFC 9580], the text is by convention UTF-8 encoded
471    /// and in "mail name-addr" form, i.e., "Name (Comment)
472    /// <email@example.com>".
473    ///
474    ///   [Section 5.11 of RFC 9580]: https://www.rfc-editor.org/rfc/rfc9580.html#section-5.11
475    ///
476    /// Use `UserID::default()` to get a UserID with a default settings.
477    value: Cow<'static, [u8]>,
478
479    hash_algo_security: OnceLock<HashAlgoSecurity>,
480
481    parsed: OnceLock<ConventionallyParsedUserID>,
482}
483assert_send_and_sync!(UserID);
484
485impl UserID {
486    /// Returns a User ID.
487    ///
488    /// This is equivalent to using `UserID::from`, but the function
489    /// is constant, and the slice must have a static lifetime.
490    ///
491    /// # Examples
492    ///
493    /// ```
494    /// use sequoia_openpgp::packet::UserID;
495    ///
496    /// const TRUST_ROOT_USERID: UserID
497    ///     = UserID::from_static_bytes(b"Local Trust Root");
498    /// ```
499    pub const fn from_static_bytes(u: &'static [u8]) -> Self {
500        UserID {
501            common: packet::Common::new(),
502            hash_algo_security: OnceLock::new(),
503            value: Cow::Borrowed(u),
504            parsed: OnceLock::new(),
505        }
506    }
507}
508
509impl From<Vec<u8>> for UserID {
510    fn from(u: Vec<u8>) -> Self {
511        UserID {
512            common: Default::default(),
513            hash_algo_security: Default::default(),
514            value: Cow::Owned(u),
515            parsed: Default::default(),
516        }
517    }
518}
519
520impl From<&[u8]> for UserID {
521    fn from(u: &[u8]) -> Self {
522        u.to_vec().into()
523    }
524}
525
526impl<'a> From<&'a str> for UserID {
527    fn from(u: &'a str) -> Self {
528        u.as_bytes().into()
529    }
530}
531
532impl From<String> for UserID {
533    fn from(u: String) -> Self {
534        u.into_bytes().into()
535    }
536}
537
538impl<'a> From<Cow<'a, str>> for UserID {
539    fn from(u: Cow<'a, str>) -> Self {
540        match u {
541            Cow::Owned(u) => u.into(),
542            Cow::Borrowed(u) => u.into(),
543        }
544    }
545}
546
547impl fmt::Display for UserID {
548    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
549        let userid = String::from_utf8_lossy(&self.value[..]);
550        write!(f, "{}", userid)
551    }
552}
553
554impl fmt::Debug for UserID {
555    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
556        let userid = String::from_utf8_lossy(&self.value[..]);
557
558        f.debug_struct("UserID")
559            .field("value", &userid)
560            .finish()
561    }
562}
563
564impl PartialEq for UserID {
565    fn eq(&self, other: &UserID) -> bool {
566        self.common == other.common
567            && self.value == other.value
568    }
569}
570
571impl Eq for UserID {
572}
573
574impl PartialOrd for UserID {
575    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
576        Some(self.cmp(other))
577    }
578}
579
580impl Ord for UserID {
581    fn cmp(&self, other: &Self) -> Ordering {
582        self.common.cmp(&other.common).then_with(
583            || self.value.cmp(&other.value))
584    }
585}
586
587impl Hash for UserID {
588    fn hash<H: Hasher>(&self, state: &mut H) {
589        // We hash only the data; the cache does not implement hash.
590        self.common.hash(state);
591        self.value.hash(state);
592    }
593}
594
595impl Clone for UserID {
596    fn clone(&self) -> Self {
597        UserID {
598            common: self.common.clone(),
599            hash_algo_security: self.hash_algo_security.clone(),
600            value: self.value.clone(),
601            parsed: if let Some(p) = self.parsed.get() {
602                p.clone().into()
603            } else {
604                OnceLock::new()
605            },
606        }
607    }
608}
609
610impl UserID {
611    fn assemble(name: Option<&str>, comment: Option<&str>,
612                address: &str, check_address: bool)
613        -> Result<Self>
614    {
615        let mut value = String::with_capacity(64);
616
617        // Make sure the individual components are valid.
618        if let Some(ref name) = name {
619            match ConventionallyParsedUserID::new(name.to_string()) {
620                Err(err) =>
621                    return Err(err.context(format!(
622                        "Validating name ({:?})",
623                        name))),
624                Ok(p) => {
625                    if !(p.name().is_some()
626                         && p.comment().is_none()
627                         && p.email().is_none()) {
628                        return Err(Error::InvalidArgument(
629                            format!("Invalid name ({:?})", name)).into());
630                    }
631                }
632            }
633
634            value.push_str(name);
635        }
636
637        if let Some(ref comment) = comment {
638            match ConventionallyParsedUserID::new(
639                format!("x ({})", comment))
640            {
641                Err(err) =>
642                    return Err(err.context(format!(
643                        "Validating comment ({:?})",
644                        comment))),
645                Ok(p) => {
646                    if !(p.name().is_some()
647                         && p.comment().is_some()
648                         && p.email().is_none()) {
649                    return Err(Error::InvalidArgument(
650                        format!("Invalid comment ({:?})", comment)).into());
651                    }
652                }
653            }
654
655            if !value.is_empty() {
656                value.push(' ');
657            }
658            value.push('(');
659            value.push_str(comment);
660            value.push(')');
661        }
662
663        if check_address {
664            match ConventionallyParsedUserID::new(
665                format!("<{}>", address))
666            {
667                Err(err) =>
668                    return Err(err.context(format!(
669                        "Validating address ({:?})",
670                        address))),
671                Ok(p) => {
672                    if !(p.name().is_none()
673                         && p.comment().is_none()
674                         && p.email().is_some()) {
675                        return Err(Error::InvalidArgument(
676                            format!("Invalid address address ({:?})", address))
677                                .into());
678                    }
679                }
680            }
681        }
682
683        if !value.is_empty() {
684            value.push(' ');
685        }
686        value.push('<');
687        value.push_str(address);
688        value.push('>');
689
690        if check_address {
691            // Make sure the combined thing is valid.
692            match ConventionallyParsedUserID::new(value.clone())
693            {
694                Err(err) =>
695                    return Err(err.context(format!(
696                        "Validating User ID ({:?})",
697                        value))),
698                Ok(p) => {
699                    if !(p.name().is_none() == name.is_none()
700                         && p.comment().is_none() == comment.is_none()
701                         && p.email().is_some()) {
702                        return Err(Error::InvalidArgument(
703                            format!("Invalid User ID ({:?})", value)).into());
704                    }
705                }
706            }
707        }
708
709        Ok(UserID::from(value))
710    }
711
712    /// The security requirements of the hash algorithm for
713    /// self-signatures.
714    ///
715    /// A cryptographic hash algorithm usually has [three security
716    /// properties]: pre-image resistance, second pre-image
717    /// resistance, and collision resistance.  If an attacker can
718    /// influence the signed data, then the hash algorithm needs to
719    /// have both second pre-image resistance, and collision
720    /// resistance.  If not, second pre-image resistance is
721    /// sufficient.
722    ///
723    ///   [three security properties]: https://en.wikipedia.org/wiki/Cryptographic_hash_function#Properties
724    ///
725    /// In general, an attacker may be able to influence third-party
726    /// signatures.  But direct key signatures, and binding signatures
727    /// are only over data fully determined by signer.  And, an
728    /// attacker's control over self signatures over User IDs is
729    /// limited due to their structure.
730    ///
731    /// In the case of self signatures over User IDs, an attacker may
732    /// be able to control the content of the User ID packet.
733    /// However, unlike an image, there is no easy way to hide large
734    /// amounts of arbitrary data (e.g., the 512 bytes needed by the
735    /// [SHA-1 is a Shambles] attack) from the user.  Further, normal
736    /// User IDs are short and encoded using UTF-8.
737    ///
738    ///   [SHA-1 is a Shambles]: https://sha-mbles.github.io/
739    ///
740    /// These observations can be used to extend the life of a hash
741    /// algorithm after its collision resistance has been partially
742    /// compromised, but not completely broken.  Specifically for the
743    /// case of User IDs, we relax the requirement for strong
744    /// collision resistance for self signatures over User IDs if:
745    ///
746    ///   - The User ID is at most 96 bytes long,
747    ///   - It contains valid UTF-8, and
748    ///   - It doesn't contain a UTF-8 control character (this includes
749    ///     the NUL byte).
750    ///
751    ///
752    /// For more details, please refer to the documentation for
753    /// [HashAlgoSecurity].
754    ///
755    ///   [HashAlgoSecurity]: crate::policy::HashAlgoSecurity
756    pub fn hash_algo_security(&self) -> HashAlgoSecurity {
757        self.hash_algo_security
758            .get_or_init(|| {
759                UserID::determine_hash_algo_security(&self.value)
760            })
761            .clone()
762    }
763
764    // See documentation for hash_algo_security.
765    fn determine_hash_algo_security(u: &[u8]) -> HashAlgoSecurity {
766        // SHA-1 has 64 byte (512-bit) blocks.  A block and a half (96
767        // bytes) is more than enough for all but malicious users.
768        if u.len() > 96 {
769            return HashAlgoSecurity::CollisionResistance;
770        }
771
772        // Check that the User ID is valid UTF-8.
773        match str::from_utf8(u) {
774            Ok(s) => {
775                // And doesn't contain control characters.
776                if s.chars().any(char::is_control) {
777                    return HashAlgoSecurity::CollisionResistance;
778                }
779            }
780            Err(_err) => {
781                return HashAlgoSecurity::CollisionResistance;
782            }
783        }
784
785        HashAlgoSecurity::SecondPreImageResistance
786    }
787
788    /// Constructs a User ID.
789    ///
790    /// This does a basic check and any necessary escaping to form a
791    /// [conventional User ID].
792    ///
793    /// Only the address is required.  If a comment is supplied, then
794    /// a name is also required.
795    ///
796    /// If you already have a User ID value, then you can just
797    /// use `UserID::from()`.
798    ///
799    ///   [conventional User ID]: #conventional-user-ids
800    ///
801    /// ```
802    /// # fn main() -> sequoia_openpgp::Result<()> {
803    /// # use sequoia_openpgp as openpgp;
804    /// # use openpgp::packet::UserID;
805    /// assert_eq!(UserID::from_address(
806    ///                "John Smith",
807    ///                None,
808    ///                "boat@example.org")?.value(),
809    ///            &b"John Smith <boat@example.org>"[..]);
810    ///
811    /// assert_eq!(UserID::from_address(
812    ///                "John Smith",
813    ///                "Who is Advok?",
814    ///                "boat@example.org")?.value(),
815    ///            &b"John Smith (Who is Advok?) <boat@example.org>"[..]);
816    /// # Ok(()) }
817    /// ```
818    pub fn from_address<'a, N, C, E>(name: N, comment: C, email: E)
819        -> Result<Self>
820    where
821        N: Into<Option<&'a str>>,
822        C: Into<Option<&'a str>>,
823        E: AsRef<str>,
824    {
825        Self::assemble(name.into().as_ref().map(|s| s.as_ref()),
826                       comment.into().as_ref().map(|s| s.as_ref()),
827                       email.as_ref(),
828                       true)
829    }
830
831    /// Constructs a User ID.
832    ///
833    /// This does a basic check and any necessary escaping to form a
834    /// [conventional User ID] modulo the address, which is not
835    /// checked.
836    ///
837    /// This is useful when you want to specify a URI instead of an
838    /// email address.
839    ///
840    /// If you already have a User ID value, then you can just
841    /// use `UserID::from()`.
842    ///
843    ///   [conventional User ID]: #conventional-user-ids
844    ///
845    /// ```
846    /// # fn main() -> sequoia_openpgp::Result<()> {
847    /// # use sequoia_openpgp as openpgp;
848    /// # use openpgp::packet::UserID;
849    /// assert_eq!(UserID::from_unchecked_address(
850    ///                "NAS",
851    ///                None, "ssh://host.example.org")?.value(),
852    ///            &b"NAS <ssh://host.example.org>"[..]);
853    /// # Ok(()) }
854    /// ```
855    pub fn from_unchecked_address<'a, N, C, E>(name: N, comment: C, address: E)
856        -> Result<Self>
857    where
858        N: Into<Option<&'a str>>,
859        C: Into<Option<&'a str>>,
860        E: AsRef<str>,
861    {
862        Self::assemble(name.into().as_ref().map(|s| s.as_ref()),
863                       comment.into().as_ref().map(|s| s.as_ref()),
864                       address.as_ref(),
865                       false)
866    }
867
868    /// Gets the user ID packet's value.
869    ///
870    /// This returns the raw, uninterpreted value.  See
871    /// [`UserID::name`], [`UserID::email`],
872    /// [`UserID::email_normalized`], [`UserID::uri`], and
873    /// [`UserID::comment`] for how to extract parts of [conventional
874    /// User ID]s.
875    ///
876    ///   [`UserID::name`]: UserID::name()
877    ///   [`UserID::email`]: UserID::email()
878    ///   [`UserID::email_normalized`]: UserID::email_normalized()
879    ///   [`UserID::uri`]: UserID::uri()
880    ///   [`UserID::comment`]: UserID::comment()
881    ///   [conventional User ID]: #conventional-user-ids
882    pub fn value(&self) -> &[u8] {
883        &self.value
884    }
885
886    fn do_parse(&self) -> Result<&ConventionallyParsedUserID> {
887        if let Some(p) = self.parsed.get() {
888            return Ok(p);
889        }
890
891        let s = str::from_utf8(&self.value)?;
892        let p = ConventionallyParsedUserID::parse(s.to_string())?;
893        let _lost_race = self.parsed.set(p.clone());
894        Ok(self.parsed.get().expect("just set"))
895    }
896
897    /// Parses the User ID according to de facto conventions, and
898    /// returns the name component, if any.
899    ///
900    /// See [conventional User ID] for more information.
901    ///
902    ///   [conventional User ID]: #conventional-user-ids
903    pub fn name(&self) -> Result<Option<&str>> {
904        Ok(self.do_parse()?.name())
905    }
906
907    /// Parses the User ID according to de facto conventions, and
908    /// returns the comment field, if any.
909    ///
910    /// See [conventional User ID] for more information.
911    ///
912    ///   [conventional User ID]: #conventional-user-ids
913    pub fn comment(&self) -> Result<Option<&str>> {
914        Ok(self.do_parse()?.comment())
915    }
916
917    /// Parses the User ID according to de facto conventions, and
918    /// returns the email address, if any.
919    ///
920    /// See [conventional User ID] for more information.
921    ///
922    ///   [conventional User ID]: #conventional-user-ids
923    pub fn email(&self) -> Result<Option<&str>> {
924        Ok(self.do_parse()?.email())
925    }
926
927    /// Parses the User ID according to de facto conventions, and
928    /// returns the URI, if any.
929    ///
930    /// See [conventional User ID] for more information.
931    ///
932    ///   [conventional User ID]: #conventional-user-ids
933    pub fn uri(&self) -> Result<Option<&str>> {
934        Ok(self.do_parse()?.uri())
935    }
936
937    /// Returns a normalized version of the UserID's email address.
938    ///
939    /// Normalized email addresses are primarily needed when email
940    /// addresses are compared.
941    ///
942    /// Note: normalized email addresses are still valid email
943    /// addresses.
944    ///
945    /// This function normalizes an email address by doing [puny-code
946    /// normalization] on the domain, and lowercasing the local part in
947    /// the so-called [empty locale].
948    ///
949    /// Note: this normalization procedure is the same as the
950    /// normalization procedure recommended by [Autocrypt].
951    ///
952    ///   [puny-code normalization]: https://tools.ietf.org/html/rfc5891.html#section-4.4
953    ///   [empty locale]: https://www.w3.org/International/wiki/Case_folding
954    ///   [Autocrypt]: https://autocrypt.org/level1.html#e-mail-address-canonicalization
955    pub fn email_normalized(&self) -> Result<Option<String>> {
956        match self.email()? {
957            None => Ok(None),
958            Some(address) => {
959                let mut iter = address.split('@');
960                let localpart = iter.next().expect("Invalid email address");
961                let domain = iter.next().expect("Invalid email address");
962                assert!(iter.next().is_none(), "Invalid email address");
963
964                // Normalize Unicode in domains.
965                let domain = idna::domain_to_ascii(domain)
966                    .map_err(|e| anyhow::Error::from(e)
967                             .context("punycode conversion failed"))?;
968
969                // Join.
970                let address = format!("{}@{}", localpart, domain);
971
972                // Convert to lowercase without tailoring, i.e. without taking
973                // any locale into account.  See:
974                //
975                //  - https://www.w3.org/International/wiki/Case_folding
976                //  - https://doc.rust-lang.org/std/primitive.str.html#method.to_lowercase
977                //  - http://www.unicode.org/versions/Unicode7.0.0/ch03.pdf#G33992
978                let address = address.to_lowercase();
979
980                Ok(Some(address))
981            }
982        }
983    }
984}
985
986impl From<UserID> for Packet {
987    fn from(s: UserID) -> Self {
988        Packet::UserID(s)
989    }
990}
991
992#[cfg(test)]
993impl Arbitrary for UserID {
994    fn arbitrary(g: &mut Gen) -> Self {
995        Vec::<u8>::arbitrary(g).into()
996    }
997}
998
999#[cfg(test)]
1000mod tests {
1001    use super::*;
1002    use crate::parse::Parse;
1003    use crate::serialize::MarshalInto;
1004
1005    quickcheck! {
1006        fn roundtrip(p: UserID) -> bool {
1007            let q = UserID::from_bytes(&p.to_vec().unwrap()).unwrap();
1008            assert_eq!(p, q);
1009            true
1010        }
1011    }
1012
1013    #[test]
1014    fn decompose() {
1015        tracer!(true, "decompose", 0);
1016
1017        fn c(userid: &str,
1018             name: Option<&str>, comment: Option<&str>,
1019             email: Option<&str>, uri: Option<&str>)
1020            -> bool
1021        {
1022            assert!(email.is_none() || uri.is_none());
1023            t!("userid: {}, name: {:?}, comment: {:?}, email: {:?}, uri: {:?}",
1024               userid, name, comment, email, uri);
1025
1026            match ConventionallyParsedUserID::new(userid) {
1027                Ok(puid) => {
1028                    let good = puid.name() == name
1029                        && puid.comment() == comment
1030                        && puid.email() == email
1031                        && puid.uri() == uri;
1032
1033                    if ! good {
1034                        t!("userid: {}", userid);
1035                        t!(" -> {:?}", puid);
1036                        t!("  {:?} {}= {:?}",
1037                           puid.name(),
1038                           if puid.name() == name { "=" } else { "!" },
1039                           name);
1040                        t!("  {:?} {}= {:?}",
1041                           puid.comment(),
1042                           if puid.comment() == comment { "=" } else { "!" },
1043                           comment);
1044                        t!("  {:?} {}= {:?}",
1045                           puid.email(),
1046                           if puid.email() == email { "=" } else { "!" },
1047                           email);
1048                        t!("  {:?} {}= {:?}",
1049                           puid.uri(),
1050                           if puid.uri() == uri { "=" } else { "!" },
1051                           uri);
1052
1053                        t!(" -> BAD PARSE");
1054                    }
1055                    good
1056                }
1057                Err(err) => {
1058                    t!("userid: {} -> PARSE ERROR: {:?}", userid, err);
1059                    false
1060                }
1061            }
1062        }
1063
1064        let mut g = true;
1065
1066        // Conventional User IDs:
1067        g &= c("First Last (Comment) <name@example.org>",
1068          Some("First Last"), Some("Comment"), Some("name@example.org"), None);
1069        g &= c("First Last <name@example.org>",
1070          Some("First Last"), None, Some("name@example.org"), None);
1071        g &= c("First Last", Some("First Last"), None, None, None);
1072        g &= c("name@example.org <name@example.org>",
1073          Some("name@example.org"), None, Some("name@example.org"), None);
1074        g &= c("<name@example.org>",
1075          None, None, Some("name@example.org"), None);
1076        g &= c("name@example.org",
1077          None, None, Some("name@example.org"), None);
1078
1079        // Examples from dkg's mail:
1080        g &= c("Björn Björnson <bjoern@example.net>",
1081          Some("Björn Björnson"), None, Some("bjoern@example.net"), None);
1082        // We explicitly don't support RFC 2047 so the following is
1083        // correctly not escaped.
1084        g &= c("Bj=?utf-8?q?=C3=B6?=rn Bj=?utf-8?q?=C3=B6?=rnson \
1085           <bjoern@example.net>",
1086          Some("Bj=?utf-8?q?=C3=B6?=rn Bj=?utf-8?q?=C3=B6?=rnson"),
1087          None, Some("bjoern@example.net"), None);
1088        g &= c("Acme Industries, Inc. <info@acme.example>",
1089          Some("Acme Industries, Inc."), None, Some("info@acme.example"), None);
1090        g &= c("Michael O'Brian <obrian@example.biz>",
1091          Some("Michael O'Brian"), None, Some("obrian@example.biz"), None);
1092        g &= c("Smith, John <jsmith@example.com>",
1093          Some("Smith, John"), None, Some("jsmith@example.com"), None);
1094        g &= c("mariag@example.org",
1095          None, None, Some("mariag@example.org"), None);
1096        g &= c("joe@example.net <joe@example.net>",
1097          Some("joe@example.net"), None, Some("joe@example.net"), None);
1098        g &= c("иван.сергеев@пример.рф",
1099          None, None, Some("иван.сергеев@пример.рф"), None);
1100        g &= c("Dörte@Sörensen.example.com",
1101          None, None, Some("Dörte@Sörensen.example.com"), None);
1102
1103        // Some craziness.
1104
1105        g &= c("Vorname Nachname, Dr.",
1106               Some("Vorname Nachname, Dr."), None, None, None);
1107        g &= c("Vorname Nachname, Dr. <dr@example.org>",
1108               Some("Vorname Nachname, Dr."), None, Some("dr@example.org"), None);
1109
1110        // Only the last comment counts as a comment.  The rest if
1111        // part of the name.
1112        g &= c("Foo (Bar) (Baz)",
1113          Some("Foo (Bar)"), Some("Baz"), None, None);
1114        // The same with extra whitespace.
1115        g &= c("Foo  (Bar)  (Baz)",
1116          Some("Foo  (Bar)"), Some("Baz"), None, None);
1117        g &= c("Foo  (Bar  (Baz)",
1118          Some("Foo  (Bar"), Some("Baz"), None, None);
1119
1120        // Make sure whitespace is stripped.
1121        g &= c("  Name   Last   (   some  comment )   <name@example.org>",
1122               Some("Name   Last"), Some("some  comment"),
1123               Some("name@example.org"), None);
1124
1125        // Make sure an email is a comment is recognized as a comment.
1126        g &= c(" Name Last (email@example.org)",
1127               Some("Name Last"), Some("email@example.org"), None, None);
1128
1129        // Quoting in the local part of the email address is not
1130        // allowed, but it is recognized as a name.  That's fine.
1131        g &= c("\"user\"@example.org",
1132               Some("\"user\"@example.org"), None, None, None);
1133        // Even unbalanced quotes.
1134        g &= c("\"user@example.org",
1135               Some("\"user@example.org"), None, None, None);
1136
1137        g &= c("Henry Ford (CEO) <henry@ford.com>",
1138               Some("Henry Ford"), Some("CEO"), Some("henry@ford.com"), None);
1139
1140        g &= c("Thomas \"Tomakin\" (DHC) <thomas@clh.co.uk>",
1141               Some("Thomas \"Tomakin\""), Some("DHC"),
1142               Some("thomas@clh.co.uk"), None);
1143
1144        g &= c("Aldous L. Huxley <huxley@old-world.org>",
1145               Some("Aldous L. Huxley"), None,
1146               Some("huxley@old-world.org"), None);
1147
1148
1149        // Some URIs.
1150
1151        // Examples from https://tools.ietf.org/html/rfc3986#section-1.1.2
1152        g &= c("<ftp://ftp.is.co.za/rfc/rfc1808.txt>",
1153               None, None,
1154               None, Some("ftp://ftp.is.co.za/rfc/rfc1808.txt"));
1155
1156        g &= c("<http://www.ietf.org/rfc/rfc2396.txt>",
1157               None, None,
1158               None, Some("http://www.ietf.org/rfc/rfc2396.txt"));
1159
1160        g &= c("<ldap://[2001:db8::7]/c=GB?objectClass?one>",
1161               None, None,
1162               None, Some("ldap://[2001:db8::7]/c=GB?objectClass?one"));
1163
1164        g &= c("<mailto:John.Doe@example.com>",
1165               None, None,
1166               None, Some("mailto:John.Doe@example.com"));
1167
1168        g &= c("<news:comp.infosystems.www.servers.unix>",
1169               None, None,
1170               None, Some("news:comp.infosystems.www.servers.unix"));
1171
1172        g &= c("<tel:+1-816-555-1212>",
1173               None, None,
1174               None, Some("tel:+1-816-555-1212"));
1175
1176        g &= c("<telnet://192.0.2.16:80/>",
1177               None, None,
1178               None, Some("telnet://192.0.2.16:80/"));
1179
1180        g &= c("<urn:oasis:names:specification:docbook:dtd:xml:4.1.2>",
1181               None, None,
1182               None, Some("urn:oasis:names:specification:docbook:dtd:xml:4.1.2"));
1183
1184
1185
1186        g &= c("Foo's ssh server <ssh://hostname>",
1187               Some("Foo's ssh server"), None,
1188               None, Some("ssh://hostname"));
1189
1190        g &= c("Foo (ssh server) <ssh://hostname>",
1191               Some("Foo"), Some("ssh server"),
1192               None, Some("ssh://hostname"));
1193
1194        g &= c("<ssh://hostname>",
1195               None, None,
1196               None, Some("ssh://hostname"));
1197
1198        g &= c("Warez <ftp://127.0.0.1>",
1199               Some("Warez"), None,
1200               None, Some("ftp://127.0.0.1"));
1201
1202        g &= c("ssh://hostname",
1203               None, None,
1204               None, Some("ssh://hostname"));
1205
1206        g &= c("ssh:hostname",
1207               None, None,
1208               None, Some("ssh:hostname"));
1209
1210        g &= c("Frank Füber <ssh://ïntérnätïònál.eu>",
1211               Some("Frank Füber"), None,
1212               None, Some("ssh://ïntérnätïònál.eu"));
1213
1214        g &= c("ssh://ïntérnätïònál.eu",
1215               None, None,
1216               None, Some("ssh://ïntérnätïònál.eu"));
1217
1218        g &= c("<foo://domain.org>",
1219               None, None,
1220               None, Some("foo://domain.org"));
1221
1222        g &= c("<foo-bar://domain.org>",
1223               None, None,
1224               None, Some("foo-bar://domain.org"));
1225
1226        g &= c("<foo+bar://domain.org>",
1227               None, None,
1228               None, Some("foo+bar://domain.org"));
1229
1230        g &= c("<foo.bar://domain.org>",
1231               None, None,
1232               None, Some("foo.bar://domain.org"));
1233
1234        g &= c("<foo.bar://domain.org#anchor?query>",
1235               None, None,
1236               None, Some("foo.bar://domain.org#anchor?query"));
1237
1238        // Is it an email address or a URI?  It should show up as a URI.
1239        g &= c("<foo://user:password@domain.org>",
1240               None, None,
1241               None, Some("foo://user:password@domain.org"));
1242
1243        // Ports...
1244        g &= c("<foo://domain.org:348>",
1245               None, None,
1246               None, Some("foo://domain.org:348"));
1247
1248        g &= c("<foo://domain.org:348/>",
1249               None, None,
1250               None, Some("foo://domain.org:348/"));
1251
1252        // Some test vectors from
1253        // https://github.com/cweb/iri-tests/blob/master/iris.txt
1254        g &= c("<http://[:]>", None, None, None, Some("http://[:]"));
1255        g &= c("<http://2001:db8::1>", None, None, None, Some("http://2001:db8::1"));
1256        g &= c("<http://[www.google.com]/>", None, None, None, Some("http://[www.google.com]/"));
1257        g &= c("<http:////////user:@google.com:99?foo>", None, None, None, Some("http:////////user:@google.com:99?foo"));
1258        g &= c("<http:path>", None, None, None, Some("http:path"));
1259        g &= c("<http:/path>", None, None, None, Some("http:/path"));
1260        g &= c("<http:host>", None, None, None, Some("http:host"));
1261        g &= c("<http://user:pass@foo:21/bar;par?b#c>", None, None, None,
1262               Some("http://user:pass@foo:21/bar;par?b#c"));
1263        g &= c("<http:foo.com>", None, None, None, Some("http:foo.com"));
1264        g &= c("<http://f:/c>", None, None, None, Some("http://f:/c"));
1265        g &= c("<http://f:0/c>", None, None, None, Some("http://f:0/c"));
1266        g &= c("<http://f:00000000000000/c>", None, None, None, Some("http://f:00000000000000/c"));
1267        g &= c("<http://f:&#x000A;/c>", None, None, None, Some("http://f:&#x000A;/c"));
1268        g &= c("<http://f:fifty-two/c>", None, None, None, Some("http://f:fifty-two/c"));
1269        g &= c("<foo://>", None, None, None, Some("foo://"));
1270        g &= c("<http://a:b@c:29/d>", None, None, None, Some("http://a:b@c:29/d"));
1271        g &= c("<http::@c:29>", None, None, None, Some("http::@c:29"));
1272        g &= c("<http://&amp;a:foo(b]c@d:2/>", None, None, None, Some("http://&amp;a:foo(b]c@d:2/"));
1273        g &= c("<http://iris.test.ing/re&#x301;sume&#x301;/re&#x301;sume&#x301;.html>", None, None, None, Some("http://iris.test.ing/re&#x301;sume&#x301;/re&#x301;sume&#x301;.html"));
1274        g &= c("<http://google.com/foo[bar]>", None, None, None, Some("http://google.com/foo[bar]"));
1275
1276        if !g {
1277            panic!("Parse error");
1278        }
1279    }
1280
1281    #[test]
1282    fn compose() {
1283        tracer!(true, "compose", 0);
1284
1285        fn c(userid: &str,
1286             name: Option<&str>, comment: Option<&str>,
1287             email: Option<&str>, uri: Option<&str>)
1288        {
1289            assert!(email.xor(uri).is_some());
1290            t!("userid: {}, name: {:?}, comment: {:?}, email: {:?}, uri: {:?}",
1291               userid, name, comment, email, uri);
1292
1293            if let Some(email) = email {
1294                let uid = UserID::from_address(name, comment, email).unwrap();
1295                assert_eq!(userid, String::from_utf8_lossy(uid.value()));
1296            }
1297
1298            if let Some(uri) = uri {
1299                let uid =
1300                    UserID::from_unchecked_address(name, comment, uri).unwrap();
1301                assert_eq!(userid, String::from_utf8_lossy(uid.value()));
1302            }
1303        }
1304
1305        // Conventional User IDs:
1306        c("First Last (Comment) <name@example.org>",
1307          Some("First Last"), Some("Comment"), Some("name@example.org"), None);
1308        c("First Last <name@example.org>",
1309          Some("First Last"), None, Some("name@example.org"), None);
1310        c("<name@example.org>",
1311          None, None, Some("name@example.org"), None);
1312    }
1313
1314    // Make sure we can't parse non-conventional User IDs.
1315    #[test]
1316    fn decompose_non_conventional() {
1317        // Empty string is not allowed.
1318        assert!(ConventionallyParsedUserID::new("").is_err());
1319        // Likewise, only whitespace.
1320        assert!(ConventionallyParsedUserID::new(" ").is_err());
1321        assert!(ConventionallyParsedUserID::new("   ").is_err());
1322
1323        // Double dots are not allowed.
1324        assert!(ConventionallyParsedUserID::new(
1325            "<a..b@example.org>").is_err());
1326        // Nor are dots at the start or end of the local part.
1327        assert!(ConventionallyParsedUserID::new(
1328            "<dr.@example.org>").is_err());
1329        assert!(ConventionallyParsedUserID::new(
1330            "<.drb@example.org>").is_err());
1331
1332        assert!(ConventionallyParsedUserID::new(
1333            "<hallo> <hello@example.org>").is_err());
1334        assert!(ConventionallyParsedUserID::new(
1335            "<hallo <hello@example.org>").is_err());
1336        assert!(ConventionallyParsedUserID::new(
1337            "hallo> <hello@example.org>").is_err());
1338
1339        // No @.
1340        assert!(ConventionallyParsedUserID::new(
1341            "foo <example.org>").is_err());
1342        // Two @s.
1343        assert!(ConventionallyParsedUserID::new(
1344            "Huxley <huxley@@old-world.org>").is_err());
1345
1346        // Unfortunately, the following is accepted as a name:
1347        //
1348        // assert!(ConventionallyParsedUserID::new(
1349        //     "huxley@@old-world.org").is_err());
1350
1351        // No local part.
1352        assert!(ConventionallyParsedUserID::new(
1353            "foo <@example.org>").is_err());
1354
1355        // No leading/ending dot in the email address.
1356        assert!(ConventionallyParsedUserID::new(
1357            "<huxley@.old-world.org>").is_err());
1358        assert!(ConventionallyParsedUserID::new(
1359            "<huxley@old-world.org.>").is_err());
1360
1361        // Unfortunately, the following are recognized as names:
1362        //
1363        // assert!(ConventionallyParsedUserID::new(
1364        //     "huxley@.old-world.org").is_err());
1365        // assert!(ConventionallyParsedUserID::new(
1366        //     "huxley@old-world.org.").is_err());
1367
1368        // Need something in the local part.
1369        assert!(ConventionallyParsedUserID::new(
1370            "<@old-world.org>").is_err());
1371
1372        // Unfortunately, the following is recognized as a name:
1373        //
1374        // assert!(ConventionallyParsedUserID::new(
1375        //     "@old-world.org").is_err());
1376
1377
1378        // URI schemas must be ASCII.
1379        assert!(ConventionallyParsedUserID::new(
1380            "<über://domain.org>").is_err());
1381
1382        // Whitespace is not allowed.
1383        assert!(ConventionallyParsedUserID::new(
1384            "<http://some domain.org>").is_err());
1385    }
1386
1387    #[test]
1388    fn email_normalized() {
1389        fn c(value: &str, expected: &str) {
1390            let u = UserID::from(value);
1391            let got = u.email_normalized().unwrap().unwrap();
1392            assert_eq!(expected, got);
1393        }
1394
1395        c("Henry Ford (CEO) <henry@ford.com>", "henry@ford.com");
1396        c("Henry Ford (CEO) <Henry@Ford.com>", "henry@ford.com");
1397        c("Henry Ford (CEO) <Henry@Ford.com>", "henry@ford.com");
1398        c("hans@bücher.tld", "hans@xn--bcher-kva.tld");
1399        c("hANS@bücher.tld", "hans@xn--bcher-kva.tld");
1400    }
1401
1402    #[test]
1403    fn from_address() {
1404        assert_eq!(UserID::from_address(None, None, "foo@bar.com")
1405                       .unwrap().value(),
1406                   b"<foo@bar.com>");
1407        assert!(UserID::from_address(None, None, "foo@@bar.com").is_err());
1408        assert_eq!(UserID::from_address("Foo Q. Bar", None, "foo@bar.com")
1409                      .unwrap().value(),
1410                   b"Foo Q. Bar <foo@bar.com>");
1411    }
1412
1413    #[test]
1414    fn hash_algo_security() {
1415        // Acceptable.
1416        assert_eq!(UserID::from("Alice Lovelace <alice@lovelace.org>")
1417                   .hash_algo_security(),
1418                   HashAlgoSecurity::SecondPreImageResistance);
1419
1420        // Embedded NUL.
1421        assert_eq!(UserID::from(&b"Alice Lovelace <alice@lovelace.org>\0"[..])
1422                   .hash_algo_security(),
1423                   HashAlgoSecurity::CollisionResistance);
1424        assert_eq!(
1425            UserID::from(
1426                &b"Alice Lovelace <alice@lovelace.org>\0Hidden!"[..])
1427                .hash_algo_security(),
1428            HashAlgoSecurity::CollisionResistance);
1429
1430        // Long strings.
1431        assert_eq!(
1432            UserID::from(String::from_utf8(vec![b'a'; 90]).unwrap())
1433                .hash_algo_security(),
1434            HashAlgoSecurity::SecondPreImageResistance);
1435        assert_eq!(
1436            UserID::from(String::from_utf8(vec![b'a'; 100]).unwrap())
1437                .hash_algo_security(),
1438            HashAlgoSecurity::CollisionResistance);
1439    }
1440}