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:
/c>", None, None, None, Some("http://f:
/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://&a:foo(b]c@d:2/>", None, None, None, Some("http://&a:foo(b]c@d:2/"));
1273 g &= c("<http://iris.test.ing/résumé/résumé.html>", None, None, None, Some("http://iris.test.ing/résumé/résumé.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}