Botan 3.13.0
Crypto and TLS for C&
email.cpp
Go to the documentation of this file.
1/*
2* (C) 2026 Jack Lloyd
3*
4* Botan is released under the Simplified BSD License (see license.txt)
5*/
6
7#include <botan/email.h>
8
9#include <botan/internal/charset.h>
10#include <botan/internal/fmt.h>
11
12namespace Botan {
13
14namespace {
15
16std::optional<std::pair<std::string, DNSName>> parse_email_address(std::string_view addr) {
17 /*
18 * RFC 5322 atext: ALPHA / DIGIT plus "!#$%&'*+-/=?^_`{|}~" (plus ".")
19 *
20 * Anything outside this set in the local part requires quoting,
21 * which we deliberately don't accept.
22 */
23 constexpr auto is_atext_or_dot = CharacterValidityTable::alpha_numeric_plus(".!#$%&'*+-/=?^_`{|}~");
24
25 if(addr.empty() || !is_valid_utf8(addr)) {
26 return {};
27 }
28
29 const auto at = addr.find('@');
30
31 // Must be one (and only one) @ sign
32 if(at == std::string_view::npos || addr.find('@', at + 1) != std::string_view::npos) {
33 return {};
34 }
35
36 // Split at the @ sign and verify both halves are non-empty
37 const std::string_view local = addr.substr(0, at);
38 const std::string_view domain = addr.substr(at + 1);
39
40 if(local.empty() || domain.empty()) {
41 return {};
42 }
43
44 // RFC 3696 section 3:
45 //
46 // period (".") may also appear [in an email address local-part],
47 // but may not be used to start or end the local part, nor may two
48 // or more consecutive periods appear.
49
50 // TODO(C++23): use std::string::contains
51 if(local.starts_with('.') || local.ends_with('.') || local.find("..") != std::string_view::npos) {
52 return {};
53 }
54
55 // RFC 5322 dot-atom. This intentionally omits support for quoting
56 for(const char c : local) {
57 // Here we accept high bit for UTF-8 for SmtpUtf8Mailbox
58 if(!is_atext_or_dot(c) && static_cast<uint8_t>(c) < 0x80) {
59 return {};
60 }
61 }
62
63 auto parsed_domain = DNSName::from_string(domain);
64 if(!parsed_domain.has_value()) {
65 return {};
66 }
67
68 return std::make_pair(std::string(local), parsed_domain.value());
69}
70
71} // namespace
72
73std::string EmailAddress::to_string() const {
74 return fmt("{}@{}", m_local_part, m_domain.to_string());
75}
76
77//static
78std::optional<EmailAddress> EmailAddress::from_string(std::string_view addr) {
79 auto parsed = parse_email_address(addr);
80
81 if(parsed) {
82 // Verify the local-part is all ASCII
83 for(const char c : parsed->first) {
84 if(static_cast<uint8_t>(c) >= 0x80) {
85 return {};
86 }
87 }
88 return EmailAddress(std::move(parsed->first), std::move(parsed->second));
89 } else {
90 return {};
91 }
92}
93
94std::string SmtpUtf8Mailbox::to_string() const {
95 return fmt("{}@{}", m_local_part, m_domain.to_string());
96}
97
98//static
99std::optional<SmtpUtf8Mailbox> SmtpUtf8Mailbox::from_string(std::string_view addr) {
100 auto parsed = parse_email_address(addr);
101
102 if(parsed) {
103 /*
104 * RFC 9598 Section 3
105 * SmtpUTF8Mailbox subjectAltName MUST NOT be used unless the Local-part
106 * of the email address contains non-ASCII characters. When the Local-
107 * part is ASCII, rfc822Name subjectAltName MUST be used instead of
108 * SmtpUTF8Mailbox.
109 *
110 * We do not currently enforce this on the decoding side.
111 */
112 return SmtpUtf8Mailbox(std::move(parsed->first), std::move(parsed->second));
113 }
114
115 return {};
116}
117
118} // namespace Botan
static constexpr CharacterValidityTable alpha_numeric_plus(std::string_view extras)
Definition charset.h:114
static std::optional< DNSName > from_string(std::string_view name)
Definition dns_name.cpp:136
std::string to_string() const
Definition email.cpp:73
static std::optional< EmailAddress > from_string(std::string_view addr)
Definition email.cpp:78
static std::optional< SmtpUtf8Mailbox > from_string(std::string_view addr)
Definition email.cpp:99
std::string to_string() const
Definition email.cpp:94
std::string fmt(std::string_view format, const T &... args)
Definition fmt.h:53
bool is_valid_utf8(std::string_view utf8)
Definition charset.cpp:108