├── AUTHORS ├── NEWS ├── .gitignore ├── test ├── input-clean │ ├── 01-simple │ ├── 02-multipart-alternative │ ├── 03-multipart-mixed │ ├── 04-nested-multipart │ └── 05-nested-message-rfc822 ├── parse-reproduce.cpp ├── describe.cpp ├── load-save.cpp ├── meson.build ├── build-highlevel.cpp ├── build-lowlevel.cpp ├── headers.cpp └── multipart.cpp ├── meson.build ├── src ├── meson.build ├── quoted-printable.hpp ├── charset.hpp ├── base64.hpp ├── quoted-printable.cpp ├── charset.cpp ├── base64.cpp ├── string_view.hpp ├── mimesis.hpp └── mimesis.cpp ├── README.md └── LICENSE /AUTHORS: -------------------------------------------------------------------------------- 1 | Guus Sliepen 2 | -------------------------------------------------------------------------------- /NEWS: -------------------------------------------------------------------------------- 1 | Initial commit August 30 2017 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .* 3 | !.gitignore 4 | core 5 | core.* 6 | vgcore.* 7 | /build* 8 | -------------------------------------------------------------------------------- /test/input-clean/01-simple: -------------------------------------------------------------------------------- 1 | From: Some One 2 | To: Someone Else 3 | Subject: Test 4 | 5 | Hello! 6 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project('Mimesis', 'cpp', 2 | version: '0.1', 3 | license: 'LGPL3+', 4 | ) 5 | 6 | cpp = meson.get_compiler('cpp') 7 | 8 | subdir('src') 9 | subdir('test') 10 | -------------------------------------------------------------------------------- /src/meson.build: -------------------------------------------------------------------------------- 1 | libmimesis = library('mimesis', 2 | 'base64.cpp', 3 | 'charset.cpp', 4 | 'mimesis.cpp', 5 | 'quoted-printable.cpp', 6 | install: true 7 | ) 8 | 9 | incdir = include_directories('.') 10 | install_headers('mimesis.hpp', subdir: '') 11 | 12 | pkg = import('pkgconfig') 13 | pkg.generate(libmimesis, subdirs: '', description: 'C++ library for RFC2822 message parsing and creation') 14 | -------------------------------------------------------------------------------- /test/input-clean/02-multipart-alternative: -------------------------------------------------------------------------------- 1 | From: Some One 2 | To: Someone Else 3 | Subject: Test 4 | MIME-Version: 1.0 5 | Content-Type: multipart/alternative; boundary=zxnrbl 6 | 7 | This is the preamble. 8 | --zxnrbl 9 | Content-Type: text/plain 10 | 11 | Hello! 12 | --zxnrbl 13 | Content-Type: text/html 14 | 15 |

Hello!

16 | --zxnrbl-- 17 | This is the epilogue. 18 | -------------------------------------------------------------------------------- /test/input-clean/03-multipart-mixed: -------------------------------------------------------------------------------- 1 | From: Some One 2 | To: Someone Else 3 | Subject: Test 4 | MIME-Version: 1.0 5 | Content-Type: multipart/mixed; boundary=zxnrbl 6 | 7 | This is the preamble. 8 | --zxnrbl 9 | Content-Type: text/plain 10 | 11 | Hello! 12 | --zxnrbl 13 | Content-Type: text/plain 14 | Content-Disposition: attachment; filename="attachment.txt" 15 | 16 | This is the attachment. 17 | --zxnrbl-- 18 | This is the epilogue. 19 | -------------------------------------------------------------------------------- /test/input-clean/04-nested-multipart: -------------------------------------------------------------------------------- 1 | From: Some One 2 | To: Someone Else 3 | Subject: Test 4 | MIME-Version: 1.0 5 | Content-Type: multipart/mixed; boundary=zxnrbl 6 | 7 | This is the preamble. 8 | --zxnrbl 9 | Content-Type: multipart/alternative; boundary=xyzzy 10 | 11 | This is the nested preamble. 12 | --xyzzy 13 | Content-Type: text/plain 14 | 15 | Hello! 16 | --xyzzy 17 | Content-Type: text/html 18 | 19 |

Hello!

20 | --xyzzy-- 21 | This is the nested epilogue. 22 | --zxnrbl 23 | Content-Type: text/plain 24 | Content-Disposition: attachment; filename="attachment.txt" 25 | 26 | This is the attachment. 27 | --zxnrbl-- 28 | This is the epilogue. 29 | -------------------------------------------------------------------------------- /test/input-clean/05-nested-message-rfc822: -------------------------------------------------------------------------------- 1 | From: Some One 2 | To: Someone Else 3 | Subject: Test 4 | MIME-Version: 1.0 5 | Content-Type: message/rfc822 6 | 7 | From: Some One 8 | To: Someone Else 9 | Subject: Nested test 10 | MIME-Version: 1.0 11 | Content-Type: multipart/mixed; boundary=zxnrbl 12 | 13 | This is the preamble. 14 | --zxnrbl 15 | Content-Type: multipart/alternative; boundary=xyzzy 16 | 17 | This is the nested preamble. 18 | --xyzzy 19 | Content-Type: text/plain 20 | 21 | Hello! 22 | --xyzzy 23 | Content-Type: text/html 24 | 25 |

Hello!

26 | --xyzzy-- 27 | This is the nested epilogue. 28 | --zxnrbl 29 | Content-Type: text/plain 30 | Content-Disposition: attachment; filename="attachment.txt" 31 | 32 | This is the attachment. 33 | --zxnrbl-- 34 | This is the epilogue. 35 | -------------------------------------------------------------------------------- /src/quoted-printable.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | /* Mimesis -- a library for parsing and creating RFC2822 messages 4 | Copyright © 2017 Guus Sliepen 5 | 6 | Mimesis is free software; you can redistribute it and/or modify it under the 7 | terms of the GNU Lesser General Public License as published by the Free 8 | Software Foundation, either version 3 of the License, or (at your option) 9 | any later version. 10 | 11 | This program is distributed in the hope that it will be useful, but WITHOUT 12 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 14 | more details. 15 | 16 | You should have received a copy of the GNU Lesser General Public License 17 | along with this program. If not, see . 18 | */ 19 | 20 | #include 21 | 22 | #include "string_view.hpp" 23 | 24 | std::string quoted_printable_decode(std::string_view in); 25 | -------------------------------------------------------------------------------- /src/charset.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | /* Mimesis -- a library for parsing and creating RFC2822 messages 4 | Copyright © 2017 Guus Sliepen 5 | 6 | Mimesis is free software; you can redistribute it and/or modify it under the 7 | terms of the GNU Lesser General Public License as published by the Free 8 | Software Foundation, either version 3 of the License, or (at your option) 9 | any later version. 10 | 11 | This program is distributed in the hope that it will be useful, but WITHOUT 12 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 14 | more details. 15 | 16 | You should have received a copy of the GNU Lesser General Public License 17 | along with this program. If not, see . 18 | */ 19 | 20 | #include 21 | 22 | #include "string_view.hpp" 23 | 24 | std::string charset_decode(const std::string &charset, std::string_view text); 25 | -------------------------------------------------------------------------------- /src/base64.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | /* Mimesis -- a library for parsing and creating RFC2822 messages 4 | Copyright © 2017 Guus Sliepen 5 | 6 | Mimesis is free software; you can redistribute it and/or modify it under the 7 | terms of the GNU Lesser General Public License as published by the Free 8 | Software Foundation, either version 3 of the License, or (at your option) 9 | any later version. 10 | 11 | This program is distributed in the hope that it will be useful, but WITHOUT 12 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 14 | more details. 15 | 16 | You should have received a copy of the GNU Lesser General Public License 17 | along with this program. If not, see . 18 | */ 19 | 20 | #include 21 | #include "string_view.hpp" 22 | 23 | std::string base64_encode(std::string_view in); 24 | std::string base64_decode(std::string_view in); 25 | -------------------------------------------------------------------------------- /test/parse-reproduce.cpp: -------------------------------------------------------------------------------- 1 | /* This test reads each input file in a string, 2 | * converts the string to a Mimesis::Part, 3 | * then converts the Mimesis::Part to a string. 4 | * The test succeeds if the input and output strings are identical. 5 | */ 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | 13 | using namespace std; 14 | 15 | static bool parse_reproduce(istream &in, ostream &out) { 16 | stringstream sin; 17 | stringstream sout; 18 | sin << in.rdbuf(); 19 | 20 | Mimesis::Part msg; 21 | try { 22 | msg.load(sin); 23 | } catch(runtime_error &e) { 24 | return false; 25 | } 26 | msg.save(sout); 27 | out << sout.rdbuf(); 28 | 29 | return sin.str() == sout.str(); 30 | } 31 | 32 | int main(int argc, char *argv[]) { 33 | if (argc <= 1) { 34 | return parse_reproduce(cin, cout) ? 0 : 1; 35 | } 36 | 37 | for (int i = 1; i < argc; i++) { 38 | ifstream in(argv[i]); 39 | if (!parse_reproduce(in, cout)) 40 | return 1; 41 | 42 | } 43 | 44 | return 0; 45 | } 46 | -------------------------------------------------------------------------------- /test/describe.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include 6 | 7 | using namespace std; 8 | 9 | static string indent(int level) { 10 | return string(level, '\t'); 11 | } 12 | 13 | static void describe_part(Mimesis::Part &part, int level, ostream &out) { 14 | string content_type = part.get_header("Content-Type"); 15 | string from = part.get_header("From"); 16 | 17 | if (!from.empty()) 18 | out << indent(level) << "From: " << from << "\n"; 19 | 20 | out << indent(level) << content_type << "\n"; 21 | 22 | if (part.is_multipart()) 23 | for (auto &child: part.get_parts()) 24 | describe_part(child, level + 1, out); 25 | } 26 | 27 | static bool describe(istream &in, ostream &out) { 28 | Mimesis::Part msg; 29 | try { 30 | msg.load(in); 31 | } catch (runtime_error &e) { 32 | out << "Invalid message\n"; 33 | return false; 34 | } 35 | describe_part(msg, 0, out); 36 | return true; 37 | } 38 | 39 | int main(int argc, char *argv[]) { 40 | if (argc <= 1) { 41 | return describe(cin, cout) ? 0 : 1; 42 | } 43 | 44 | for (int i = 1; i < argc; i++) { 45 | ifstream in(argv[i]); 46 | if (!describe(in, cout)) 47 | return 1; 48 | 49 | } 50 | 51 | return 0; 52 | } 53 | -------------------------------------------------------------------------------- /test/load-save.cpp: -------------------------------------------------------------------------------- 1 | /* This tests saving and loading messages, 2 | * via files, streams and strings. 3 | */ 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | 13 | using namespace std; 14 | 15 | static bool load_save(const Mimesis::Message &msg) { 16 | // Save to and load from file 17 | { 18 | msg.save("load-save.tmp"); 19 | Mimesis::Message msg2; 20 | msg2.load("load-save.tmp"); 21 | assert(msg2 == msg); 22 | } 23 | 24 | // Save to and load from string 25 | { 26 | string str = msg.to_string(); 27 | Mimesis::Message msg2; 28 | msg2.from_string(str); 29 | assert(msg2 == msg); 30 | } 31 | 32 | // Save to and load from stream using operators 33 | { 34 | stringstream ss; 35 | ss << msg; 36 | Mimesis::Message msg2; 37 | ss >> msg2; 38 | assert(msg2 == msg); 39 | } 40 | 41 | return true; 42 | } 43 | 44 | 45 | int main(int argc, char *argv[]) { 46 | if (argc <= 1) { 47 | Mimesis::Message msg; 48 | msg.load(cin); 49 | return load_save(msg) ? 0 : 1; 50 | } 51 | 52 | for (int i = 1; i < argc; i++) { 53 | Mimesis::Message msg; 54 | msg.load(argv[i]); 55 | if (!load_save(msg)) 56 | return 1; 57 | 58 | } 59 | 60 | return 0; 61 | } 62 | -------------------------------------------------------------------------------- /test/meson.build: -------------------------------------------------------------------------------- 1 | binaries = [ 2 | 'parse-reproduce', 3 | 'describe', 4 | 'build-lowlevel', 5 | 'build-highlevel', 6 | 'headers', 7 | 'multipart', 8 | 'load-save', 9 | ] 10 | 11 | input_clean = [ 12 | 'input-clean/01-simple', 13 | 'input-clean/02-multipart-alternative', 14 | 'input-clean/03-multipart-mixed', 15 | 'input-clean/04-nested-multipart', 16 | 'input-clean/05-nested-message-rfc822', 17 | ] 18 | 19 | incdir = include_directories('../src') 20 | 21 | test('parse-reproduce', executable('parse-reproduce', 'parse-reproduce.cpp', link_with: libmimesis, include_directories: incdir), args: files(input_clean)) 22 | test('describe', executable('describe', 'describe.cpp', link_with: libmimesis, include_directories: incdir), args: files(input_clean)) 23 | test('build-highlevel', executable('build-highlevel', 'build-highlevel.cpp', link_with: libmimesis, include_directories: incdir)) 24 | test('build-lowlevel', executable('build-lowlevel', 'build-lowlevel.cpp', link_with: libmimesis, include_directories: incdir)) 25 | test('headers', executable('headers', 'headers.cpp', link_with: libmimesis, include_directories: incdir)) 26 | test('multipart', executable('multipart', 'multipart.cpp', link_with: libmimesis, include_directories: incdir)) 27 | test('load-save', executable('load-save', 'load-save.cpp', link_with: libmimesis, include_directories: incdir), args: files(input_clean)) 28 | -------------------------------------------------------------------------------- /src/quoted-printable.cpp: -------------------------------------------------------------------------------- 1 | /* Mimesis -- a library for parsing and creating RFC2822 messages 2 | Copyright © 2017 Guus Sliepen 3 | 4 | Mimesis is free software; you can redistribute it and/or modify it under the 5 | terms of the GNU Lesser General Public License as published by the Free 6 | Software Foundation, either version 3 of the License, or (at your option) 7 | any later version. 8 | 9 | This program is distributed in the hope that it will be useful, but WITHOUT 10 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 11 | FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 12 | more details. 13 | 14 | You should have received a copy of the GNU Lesser General Public License 15 | along with this program. If not, see . 16 | */ 17 | 18 | #include "quoted-printable.hpp" 19 | 20 | using namespace std; 21 | 22 | string quoted_printable_decode(string_view in) { 23 | string out; 24 | out.reserve(in.size()); 25 | 26 | int decode = 0; 27 | uint8_t val = 0; 28 | 29 | for (auto &&c: in) { 30 | if (decode) { 31 | if (c >= '0' && c <= '9') { 32 | val <<= 4; 33 | val |= c - '0'; 34 | decode--; 35 | } else if (c >= 'A' && c <= 'F') { 36 | val <<= 4; 37 | val |= 10 + (c - 'A'); 38 | decode--; 39 | } else { 40 | decode = 0; 41 | continue; 42 | } 43 | 44 | if (decode == 0) 45 | out.push_back(static_cast(val)); 46 | } else { 47 | if (c == '=') 48 | decode = 2; 49 | else 50 | out.push_back(c); 51 | } 52 | } 53 | 54 | return out; 55 | } 56 | -------------------------------------------------------------------------------- /test/build-highlevel.cpp: -------------------------------------------------------------------------------- 1 | /* This tests building messages using the high-level functions. */ 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | using namespace std; 9 | 10 | int main() { 11 | // 01-simple 12 | Mimesis::Message msg; 13 | msg["From"] = "Some One "; 14 | msg["To"] = "Someone Else "; 15 | msg["Subject"] = "Test"; 16 | msg.set_plain("Hello!\r\n"); 17 | msg.save("01-simple"); 18 | 19 | // 02-multipart-alternative 20 | msg.clear(); 21 | msg["From"] = "Some One "; 22 | msg["To"] = "Someone Else "; 23 | msg["Subject"] = "Test"; 24 | msg.set_plain("Hello!\r\n"); 25 | msg.set_html("

Hello!

\r\n"); 26 | msg.save("02-multipart-alternative"); 27 | 28 | // 03-multipart-mixed 29 | msg.clear(); 30 | msg["From"] = "Some One "; 31 | msg["To"] = "Someone Else "; 32 | msg["Subject"] = "Test"; 33 | msg.set_plain("Hello!\r\n"); 34 | msg.attach("This is the attachment.\r\n", "text/plain"); 35 | msg.save("03-multipart-mixed"); 36 | 37 | // 04-nested-multipart 38 | msg.clear(); 39 | msg["From"] = "Some One "; 40 | msg["To"] = "Someone Else "; 41 | msg["Subject"] = "Test"; 42 | msg.set_plain("Hello!\r\n"); 43 | msg.set_html("

Hello!

\r\n"); 44 | msg.attach("This is the attachment.\r\n", "text/plain"); 45 | msg.save("04-nested-multipart"); 46 | 47 | // 05-nested-message-rfc822 48 | Mimesis::Part outer; 49 | outer["From"] = "Some One "; 50 | outer["To"] = "Someone Else "; 51 | outer["Subject"] = "Test"; 52 | msg["Subject"] = "Nested test"; 53 | outer.attach(msg); 54 | outer.save("05-nested-message-rfc822"); 55 | } 56 | 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | About Mimesis 2 | ------------- 3 | 4 | Mimesis is a C++ library for RFC2822 message parsing and creation. It was born 5 | out of a frustration with existing email libraries for C++. The goals of 6 | Mimesis are: 7 | 8 | - Equally good at parsing and building RFC2822 messages. 9 | - Easy API without unnecessary abstraction layers. 10 | - Make good use of C++11 features. 11 | 12 | In particular, applications that parse/build emails want to treat them just 13 | like a user would treat emails in a mail user agent (MUA). Users don't see 14 | emails as a tree of MIME structures; instead they see them as a collection of 15 | headers, bodies and attachments. Users also don't see details such as MIME 16 | types and content encodings, MUAs handle those details automatically. 17 | 18 | Building and installing 19 | ----------------------- 20 | 21 | Mimesis uses the Meson build system. Ensure you have this installed. Use your 22 | operating system's package manager if available, for example on Debian and its 23 | derivatives, use `sudo apt install meson`. If that is not possible, download 24 | and install Meson from https://mesonbuild.com/. 25 | 26 | To build Mimesis, run these commands in the root directory of the source code: 27 | 28 | meson build 29 | ninja -C build 30 | 31 | To run the test suite, run: 32 | 33 | ninja -C build test 34 | 35 | To install the binaries on your system, run: 36 | 37 | ninja -C build install 38 | 39 | The latter command might ask for a root password if it detects the installation 40 | directory is not writable by the current user. 41 | 42 | By default, a debug version will be built, and the resulting libraries and 43 | header files will be install inside `/usr/local/`. If you want to create a 44 | release build and install it in `/usr/`, use the following commands: 45 | 46 | meson --buildtype=release --prefix=/usr build-release 47 | ninja -C build-release install 48 | 49 | Distribution packagers that want to override all compiler flags should use 50 | `--buildtype=plain`. The `DESTDIR` environment variable is supported by Meson. 51 | -------------------------------------------------------------------------------- /src/charset.cpp: -------------------------------------------------------------------------------- 1 | /* Mimesis -- a library for parsing and creating RFC2822 messages 2 | Copyright © 2017 Guus Sliepen 3 | 4 | Mimesis is free software; you can redistribute it and/or modify it under the 5 | terms of the GNU Lesser General Public License as published by the Free 6 | Software Foundation, either version 3 of the License, or (at your option) 7 | any later version. 8 | 9 | This program is distributed in the hope that it will be useful, but WITHOUT 10 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 11 | FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 12 | more details. 13 | 14 | You should have received a copy of the GNU Lesser General Public License 15 | along with this program. If not, see . 16 | */ 17 | 18 | #include "charset.hpp" 19 | 20 | #include 21 | #include 22 | 23 | using namespace std; 24 | 25 | struct iconv_state { 26 | iconv_t cd; 27 | 28 | iconv_state(const char *tocode, const char *fromcode) { 29 | cd = iconv_open(tocode, fromcode); 30 | if (cd == (iconv_t)-1) 31 | throw runtime_error("Unsupported character set"); 32 | } 33 | 34 | ~iconv_state() { 35 | iconv_close(cd); 36 | } 37 | 38 | size_t convert(char **inbuf, size_t *inbytesleft, char **outbuf, size_t *outbytesleft) { 39 | return ::iconv(cd, inbuf, inbytesleft, outbuf, outbytesleft); 40 | } 41 | }; 42 | 43 | string charset_decode(const string &charset, string_view in) { 44 | iconv_state cd("utf-8", charset.c_str()); 45 | 46 | string out; 47 | out.reserve((in.size() * 102) / 100); 48 | 49 | char *inbuf = const_cast(in.data()); 50 | size_t inbytesleft = in.size(); 51 | 52 | char buf[1024]; 53 | char *outbuf; 54 | size_t outbytesleft; 55 | 56 | while (inbytesleft) { 57 | outbuf = buf; 58 | outbytesleft = sizeof outbuf; 59 | size_t result = cd.convert(&inbuf, &inbytesleft, &outbuf, &outbytesleft); 60 | if (result == (size_t)-1) { 61 | if (errno != E2BIG) 62 | throw runtime_error("Character set conversion error"); 63 | } 64 | out.append(buf, outbuf - buf); 65 | } 66 | 67 | return out; 68 | } 69 | -------------------------------------------------------------------------------- /src/base64.cpp: -------------------------------------------------------------------------------- 1 | /* Mimesis -- a library for parsing and creating RFC2822 messages 2 | Copyright © 2017 Guus Sliepen 3 | 4 | Mimesis is free software; you can redistribute it and/or modify it under the 5 | terms of the GNU Lesser General Public License as published by the Free 6 | Software Foundation, either version 3 of the License, or (at your option) 7 | any later version. 8 | 9 | This program is distributed in the hope that it will be useful, but WITHOUT 10 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 11 | FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 12 | more details. 13 | 14 | You should have received a copy of the GNU Lesser General Public License 15 | along with this program. If not, see . 16 | */ 17 | 18 | #include "base64.hpp" 19 | 20 | using namespace std; 21 | 22 | static const string base64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; 23 | static const signed char base64_inverse[256] = { 24 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 25 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 26 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, 27 | 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, 28 | -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 29 | 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, 30 | -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 31 | 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, 32 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 33 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 34 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 35 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 36 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 37 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 38 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 39 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 40 | }; 41 | 42 | string base64_encode(string_view in) { 43 | string out; 44 | size_t outlen = ((in.size() + 2) / 3) * 4; 45 | out.reserve(outlen); 46 | 47 | size_t i; 48 | const uint8_t *uin = (const uint8_t *)in.data(); 49 | 50 | for (i = 0; i < (in.size() / 3) * 3; i += 3) { 51 | out.push_back(base64[ (uin[i + 0] >> 2)]); 52 | out.push_back(base64[(uin[i + 0] << 4 & 63) | (uin[i + 1] >> 4)]); 53 | out.push_back(base64[(uin[i + 1] << 2 & 63) | (uin[i + 2] >> 6)]); 54 | out.push_back(base64[(uin[i + 2] << 0 & 63) ]); 55 | } 56 | 57 | while (i++ < in.size()) 58 | out.push_back('='); 59 | 60 | return out; 61 | } 62 | 63 | string base64_decode(string_view in) { 64 | string out; 65 | size_t estimate = (in.size() / 4) * 3; 66 | out.reserve(estimate); 67 | 68 | int i = 0; 69 | uint32_t triplet = 0; 70 | 71 | for(uint8_t c: in) { 72 | auto d = base64_inverse[c]; 73 | if (d == -1) { 74 | if (c == '=') 75 | break; 76 | else 77 | continue; 78 | } 79 | 80 | triplet <<= 6; 81 | triplet |= d; 82 | 83 | if((i & 3) == 3) { 84 | out.push_back(static_cast(triplet >> 16)); 85 | out.push_back(static_cast(triplet >> 8)); 86 | out.push_back(static_cast(triplet)); 87 | } 88 | 89 | i++; 90 | } 91 | 92 | if((i & 3) == 3) { 93 | out.push_back(static_cast(triplet >> 10)); 94 | out.push_back(static_cast(triplet >> 2)); 95 | } else if((i & 3) == 2) { 96 | out.push_back(static_cast(triplet >> 4)); 97 | } 98 | 99 | return out; 100 | } 101 | -------------------------------------------------------------------------------- /test/build-lowlevel.cpp: -------------------------------------------------------------------------------- 1 | /* This tests building messages using the low-level functions. */ 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | using namespace std; 9 | 10 | int main() { 11 | // 01-simple 12 | Mimesis::Message msg; 13 | msg["From"] = "Some One "; 14 | msg["To"] = "Someone Else "; 15 | msg["Subject"] = "Test"; 16 | msg.set_body("Hello!\r\n"); 17 | msg.save("01-simple"); 18 | 19 | // 02-multipart-alternative 20 | msg.clear(); 21 | msg["From"] = "Some One "; 22 | msg["To"] = "Someone Else "; 23 | msg["Subject"] = "Test"; 24 | msg.make_multipart("alternative", "zxnrbl"); 25 | msg.set_preamble("This is the preamble.\r\n"); 26 | { 27 | Mimesis::Part plain_hello; 28 | plain_hello["Content-Type"] = "text/plain"; 29 | plain_hello.set_body("Hello!\r\n"); 30 | msg.append_part(plain_hello); 31 | } 32 | { 33 | Mimesis::Part html_hello; 34 | html_hello["Content-Type"] = "text/html"; 35 | html_hello.set_body("

Hello!

\r\n"); 36 | msg.append_part(html_hello); 37 | } 38 | msg.set_epilogue("This is the epilogue.\r\n"); 39 | msg.save("02-multipart-alternative"); 40 | 41 | // 03-multipart-mixed 42 | msg.clear(); 43 | msg["From"] = "Some One "; 44 | msg["To"] = "Someone Else "; 45 | msg["Subject"] = "Test"; 46 | msg.make_multipart("mixed", "zxnrbl"); 47 | msg.set_preamble("This is the preamble.\r\n"); 48 | { 49 | Mimesis::Part plain_hello; 50 | plain_hello["Content-Type"] = "text/plain"; 51 | plain_hello.set_body("Hello!\r\n"); 52 | msg.append_part(plain_hello); 53 | } 54 | { 55 | Mimesis::Part plain_attachment; 56 | plain_attachment["Content-Type"] = "text/plain"; 57 | plain_attachment["Content-Disposition"] = "attachment; filename=\"attachment.txt\""; 58 | plain_attachment.set_body("This is the attachment.\r\n"); 59 | msg.append_part(plain_attachment); 60 | } 61 | msg.set_epilogue("This is the epilogue.\r\n"); 62 | msg.save("03-multipart-mixed"); 63 | 64 | // 04-nested-multipart 65 | msg.clear(); 66 | msg["From"] = "Some One "; 67 | msg["To"] = "Someone Else "; 68 | msg["Subject"] = "Test"; 69 | msg.make_multipart("mixed", "zxnrbl"); 70 | msg.set_preamble("This is the preamble.\r\n"); 71 | { 72 | Mimesis::Part nested; 73 | nested.make_multipart("alternative", "xyzzy"); 74 | nested.set_preamble("This is the nested preamble.\r\n"); 75 | { 76 | Mimesis::Part plain_hello; 77 | plain_hello["Content-Type"] = "text/plain"; 78 | plain_hello.set_body("Hello!\r\n"); 79 | nested.append_part(plain_hello); 80 | } 81 | { 82 | Mimesis::Part html_hello; 83 | html_hello["Content-Type"] = "text/html"; 84 | html_hello.set_body("

Hello!

\r\n"); 85 | nested.append_part(html_hello); 86 | } 87 | nested.set_epilogue("This is the nested epilogue.\r\n"); 88 | msg.append_part(nested); 89 | } 90 | { 91 | Mimesis::Part plain_attachment; 92 | plain_attachment["Content-Type"] = "text/plain"; 93 | plain_attachment["Content-Disposition"] = "attachment; filename=\"attachment.txt\""; 94 | plain_attachment.set_body("This is the attachment.\r\n"); 95 | msg.append_part(plain_attachment); 96 | } 97 | msg.set_epilogue("This is the epilogue.\r\n"); 98 | msg.save("04-nested-multipart"); 99 | 100 | // 05-nested-message-rfc822 101 | Mimesis::Part outer; 102 | outer["From"] = "Some One "; 103 | outer["To"] = "Someone Else "; 104 | outer["Subject"] = "Test"; 105 | outer["MIME-Version"] = "1.0"; 106 | outer["Content-Type"] = "message/rfc822"; 107 | msg["Subject"] = "Nested test"; 108 | outer.set_body(msg.to_string()); 109 | outer.save("05-nested-message-rfc822"); 110 | } 111 | 112 | -------------------------------------------------------------------------------- /test/headers.cpp: -------------------------------------------------------------------------------- 1 | /* This tests manipulating message headers. */ 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | using namespace std; 10 | 11 | int main() { 12 | Mimesis::Part part; 13 | 14 | assert(part.get_headers().empty()); 15 | 16 | // Set using [] 17 | part["foo"] = "bar"; 18 | assert(part["foo"] == "bar"); 19 | assert(part.get_header("foo") == "bar"); 20 | assert(part.get_header_value("foo") == "bar"); 21 | assert(part.get_header_parameter("foo", "").empty()); 22 | assert(part.get_header_parameter("foo", "foo").empty()); 23 | assert(part.to_string() == "foo: bar\r\n\r\n"); 24 | 25 | // Set using set_header 26 | part.set_header("baz", "quux"); 27 | assert(part["baz"] == "quux"); 28 | assert(part.get_header("baz") == "quux"); 29 | assert(part.get_header_value("baz") == "quux"); 30 | assert(part.to_string() == "foo: bar\r\nbaz: quux\r\n\r\n"); 31 | 32 | // Change existing 33 | part.set_header("foo", "bar2"); 34 | assert(part["foo"] == "bar2"); 35 | assert(part.get_header("foo") == "bar2"); 36 | assert(part.get_header_value("foo") == "bar2"); 37 | 38 | // Set parameter on existing 39 | part.set_header_parameter("baz", "parameter", "value"); 40 | assert(part["baz"] == "quux; parameter=value"); 41 | assert(part.get_header("baz") == "quux; parameter=value"); 42 | assert(part.get_header_value("baz") == "quux"); 43 | assert(part.get_header_parameter("baz", "parameter") == "value"); 44 | 45 | // Add parameter on existing 46 | part.set_header_parameter("baz", "parameter", "value2"); 47 | assert(part.get_header("baz") == "quux; parameter=value2"); 48 | assert(part.get_header_value("baz") == "quux"); 49 | assert(part.get_header_parameter("baz", "parameter") == "value2"); 50 | 51 | // Change parameter on existing 52 | part.set_header_parameter("baz", "foo", "bar"); 53 | assert(part.get_header("baz") == "quux; parameter=value2; foo=bar"); 54 | assert(part.get_header_value("baz") == "quux"); 55 | assert(part.get_header_parameter("baz", "parameter") == "value2"); 56 | assert(part.get_header_parameter("baz", "foo") == "bar"); 57 | 58 | // Set parameter on empty 59 | part.set_header_parameter("aap", "noot", "mies"); 60 | assert(part.get_header("aap") == "; noot=mies"); 61 | assert(part.get_header_value("aap").empty()); 62 | assert(part.get_header_parameter("aap", "noot") == "mies"); 63 | 64 | // Change value but not parameters 65 | part.set_header_value("baz", "quux2"); 66 | assert(part.get_header("baz") == "quux2; parameter=value2; foo=bar"); 67 | 68 | part.set_header_value("a", "b"); 69 | assert(part.get_header("a") == "b"); 70 | 71 | // Erase header 72 | part.erase_header("a"); 73 | assert(part.get_header("a").empty()); 74 | 75 | // Appback header 76 | part.append_header("insert", "back"); 77 | assert(part.get_header("insert") == "back"); 78 | 79 | // Prepback header 80 | part.prepend_header("insert", "front"); 81 | assert(part.get_header("insert") == "front"); 82 | 83 | // Array access 84 | auto headers = part.get_headers(); 85 | assert(headers.size() == 5); 86 | for (auto &header: headers) { 87 | assert(!header.first.empty()); 88 | assert(!header.second.empty()); 89 | } 90 | assert(headers.front().second == "front"); 91 | assert(headers[1].first == "foo"); 92 | assert(headers[2].first == "baz"); 93 | assert(headers[3].first == "aap"); 94 | assert(headers[3].second == "; noot=mies"); 95 | assert(headers.back().second == "back"); 96 | 97 | // Delete single header 98 | part.set_header("foo", {}); 99 | assert(part.get_header("foo").empty()); 100 | 101 | // Intermediate result 102 | assert(part.to_string() == 103 | "insert: front\r\n" 104 | "baz: quux2; parameter=value2; foo=bar\r\n" 105 | "aap: ; noot=mies\r\n" 106 | "insert: back\r\n" 107 | "\r\n"); 108 | 109 | // Erase multiple headers 110 | part.erase_header("insert"); 111 | assert(part.get_header("insert").empty()); 112 | 113 | // Intermediate result 114 | assert(part.to_string() == 115 | "baz: quux2; parameter=value2; foo=bar\r\n" 116 | "aap: ; noot=mies\r\n" 117 | "\r\n"); 118 | 119 | // Const header access 120 | const auto &cpart = part; 121 | assert(cpart.get_headers().size() >= 2); 122 | assert(cpart.get_header("baz") == "quux2; parameter=value2; foo=bar"); 123 | assert(cpart["aap"] == "; noot=mies"); 124 | assert(cpart["insert"].empty()); 125 | 126 | // Delete headers 127 | part.clear_headers(); 128 | assert(part.get_headers().empty()); 129 | assert(part.get_header("foo").empty()); 130 | assert(part.to_string() == "\r\n"); 131 | 132 | // Transplant headers 133 | part.clear(); 134 | part.set_header("From", "me"); 135 | { 136 | Mimesis::Part other; 137 | other.set_header("To", "you"); 138 | other.set_header("From", "us"); 139 | part.set_headers(other.get_headers()); 140 | assert(other.get_headers().size() == 2); 141 | } 142 | assert(part.get_headers().size() == 2); 143 | assert(part.get_header("From") == "us"); 144 | assert(part.get_header("To") == "you"); 145 | } 146 | -------------------------------------------------------------------------------- /src/string_view.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | /* Copyright (c) 2016, Pollard Banknote Limited 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without modification, 7 | are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation and/or 14 | other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its contributors 17 | may be used to endorse or promote products derived from this software without 18 | specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 21 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 22 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | */ 31 | 32 | #if __cplusplus >= 201703L 33 | #include 34 | #else 35 | #include 36 | #include 37 | #include 38 | 39 | namespace std 40 | { 41 | template< typename CharT, typename Traits = std::char_traits< CharT > > 42 | class basic_string_view 43 | { 44 | public: 45 | typedef Traits traits_type; 46 | typedef CharT value_type; 47 | typedef CharT* pointer; 48 | typedef const CharT* const_pointer; 49 | typedef CharT& reference; 50 | typedef const CharT& const_reference; 51 | typedef const CharT* const_iterator; 52 | typedef const_iterator iterator; 53 | typedef std::size_t size_type; 54 | typedef std::ptrdiff_t difference_type; 55 | 56 | static const size_type npos = size_type(-1); 57 | 58 | basic_string_view() 59 | : first(0), len(0) 60 | { 61 | } 62 | 63 | basic_string_view(const basic_string_view&) = default; 64 | basic_string_view( 65 | const CharT* s, 66 | size_type count 67 | ) 68 | : first(s), len(count) 69 | { 70 | } 71 | 72 | basic_string_view(const CharT* s) 73 | : first(s), len( Traits::length(s) ) 74 | { 75 | } 76 | 77 | // Cheat: the real string_view class doesn't have a constructor from string, 78 | // instead string has a conversion operator to basic_string_view. 79 | basic_string_view(const basic_string &s) 80 | : first(s.data()), len(s.size()) 81 | { 82 | } 83 | 84 | basic_string_view& operator=(const basic_string_view&) = default; 85 | 86 | const_iterator begin() const 87 | { 88 | return first; 89 | } 90 | 91 | const_iterator cbegin() const 92 | { 93 | return first; 94 | } 95 | 96 | const_iterator end() const 97 | { 98 | return first + len; 99 | } 100 | 101 | const_iterator cend() const 102 | { 103 | return first + len; 104 | } 105 | 106 | const_reference operator[](size_type i) const 107 | { 108 | return first[i]; 109 | } 110 | 111 | const_reference at(size_type i) const 112 | { 113 | if ( i >= len ) 114 | { 115 | throw std::out_of_range("Index is out of range for string view"); 116 | } 117 | 118 | return first[i]; 119 | } 120 | 121 | const_reference front() const 122 | { 123 | return first[0]; 124 | } 125 | 126 | const_reference back() const 127 | { 128 | return first + ( len - 1 ); 129 | } 130 | 131 | const_pointer data() const 132 | { 133 | return first; 134 | } 135 | 136 | size_type size() const 137 | { 138 | return len; 139 | } 140 | 141 | size_type length() const 142 | { 143 | return size(); 144 | } 145 | 146 | size_type max_size() const 147 | { 148 | return static_cast< size_type >( std::numeric_limits< difference_type >::max() ); 149 | } 150 | 151 | bool empty() const 152 | { 153 | return len == 0; 154 | } 155 | 156 | void remove_prefix(size_type n) 157 | { 158 | first += n; 159 | } 160 | 161 | void remove_suffix(size_type n) 162 | { 163 | len -= n; 164 | } 165 | 166 | void swap(basic_string_view& v) 167 | { 168 | const CharT* t = first; 169 | 170 | first = v.first; 171 | v.first = t; 172 | 173 | size_type n = len; 174 | len = v.len; 175 | v.len = n; 176 | } 177 | 178 | size_type copy( 179 | CharT* dest, 180 | size_type count, 181 | size_type pos = 0 182 | ) const 183 | { 184 | if ( pos < len ) 185 | { 186 | const char* p = first + pos; 187 | const size_type m = len - pos; 188 | const size_type n = count < m ? count : m; 189 | 190 | for ( size_t i = 0; i < n; ++i ) 191 | { 192 | dest[i] = p[i]; 193 | } 194 | 195 | return n; 196 | } 197 | 198 | return 0; 199 | } 200 | 201 | basic_string_view substr( 202 | size_type pos = 0, 203 | size_type count = npos 204 | ) const 205 | { 206 | const size_type n = ( pos < len ) 207 | ? ( count <= len - pos ? count : len - pos ) 208 | : 0; 209 | 210 | return basic_string_view(first + pos, n); 211 | } 212 | 213 | private: 214 | const CharT* first; 215 | size_type len; 216 | }; 217 | 218 | typedef basic_string_view< char > string_view; 219 | typedef basic_string_view< wchar_t > wstring_view; 220 | typedef basic_string_view< char16_t > u16string_view; 221 | typedef basic_string_view< char32_t > u32string_view; 222 | } 223 | #endif 224 | -------------------------------------------------------------------------------- /src/mimesis.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | /* Mimesis -- a library for parsing and creating RFC2822 messages 4 | Copyright © 2017 Guus Sliepen 5 | 6 | Mimesis is free software; you can redistribute it and/or modify it under the 7 | terms of the GNU Lesser General Public License as published by the Free 8 | Software Foundation, either version 3 of the License, or (at your option) 9 | any later version. 10 | 11 | This program is distributed in the hope that it will be useful, but WITHOUT 12 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 14 | more details. 15 | 16 | You should have received a copy of the GNU Lesser General Public License 17 | along with this program. If not, see . 18 | */ 19 | 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | 27 | namespace Mimesis { 28 | 29 | class Part { 30 | std::vector> headers; 31 | std::string preamble; 32 | std::string body; 33 | std::string epilogue; 34 | std::vector parts; 35 | std::string boundary; 36 | bool multipart; 37 | bool crlf; 38 | 39 | protected: 40 | bool message; 41 | 42 | public: 43 | Part(); 44 | friend bool operator==(const Part &lhs, const Part &rhs); 45 | friend bool operator!=(const Part &lhs, const Part &rhs); 46 | 47 | // Loading and saving a whole MIME message 48 | std::string load(std::istream &in, const std::string &parent_boundary = {}); 49 | void load(const std::string &filename); 50 | void save(std::ostream &out) const; 51 | void save(const std::string &filename) const; 52 | void from_string(const std::string &data); 53 | std::string to_string() const; 54 | 55 | // Low-level access 56 | std::string get_body() const; 57 | std::string get_preamble() const; 58 | std::string get_epilogue() const; 59 | std::string get_boundary() const; 60 | std::vector &get_parts(); 61 | const std::vector &get_parts() const; 62 | std::vector> &get_headers(); 63 | const std::vector> &get_headers() const; 64 | bool is_multipart() const; 65 | bool is_multipart(const std::string &subtype) const; 66 | bool is_singlepart() const; 67 | bool is_singlepart(const std::string &type) const; 68 | 69 | void set_body(const std::string &body); 70 | void set_preamble(const std::string &preamble); 71 | void set_epilogue(const std::string &epilogue); 72 | void set_boundary(const std::string &boundary); 73 | void set_parts(const std::vector &parts); 74 | void set_headers(const std::vector> &headers); 75 | 76 | void clear(); 77 | void clear_body(); 78 | 79 | // Header manipulation 80 | std::string get_header(const std::string &field) const; 81 | void set_header(const std::string &field, const std::string &value); 82 | std::string &operator[](const std::string &field); 83 | const std::string &operator[](const std::string &field) const; 84 | void append_header(const std::string &field, const std::string &value); 85 | void prepend_header(const std::string &field, const std::string &value); 86 | void erase_header(const std::string &field); 87 | void clear_headers(); 88 | 89 | // Specialized header functions 90 | std::string get_multipart_type() const; 91 | std::string get_header_value(const std::string &field) const; 92 | std::string get_header_parameter(const std::string &field, const std::string ¶meter) const; 93 | 94 | void set_header_value(const std::string &field, const std::string &value); 95 | void set_header_parameter(const std::string &field, const std::string ¶mter, const std::string &value); 96 | 97 | void add_received(const std::string &domain, const std::chrono::system_clock::time_point &date = std::chrono::system_clock::now()); 98 | void generate_msgid(const std::string &domain); 99 | void set_date(const std::chrono::system_clock::time_point &date = std::chrono::system_clock::now()); 100 | 101 | // Part manipulation 102 | Part &append_part(const Part &part = {}); 103 | Part &prepend_part(const Part &part = {}); 104 | void clear_parts(); 105 | void make_multipart(const std::string &type, const std::string &boundary = {}); 106 | bool flatten(); 107 | 108 | std::string get_mime_type() const; 109 | void set_mime_type(const std::string &type); 110 | bool is_mime_type(const std::string &type) const; 111 | bool has_mime_type() const; 112 | 113 | // Body and attachments 114 | Part &set_alternative(const std::string &subtype, const std::string &text); 115 | void set_plain(const std::string &text); 116 | void set_html(const std::string &text); 117 | 118 | const Part *get_first_matching_part(std::function predicate) const; 119 | Part *get_first_matching_part(std::function predicate); 120 | const Part *get_first_matching_part(const std::string &type) const; 121 | Part *get_first_matching_part(const std::string &type); 122 | std::string get_first_matching_body(const std::string &type) const; 123 | std::string get_text() const; 124 | std::string get_plain() const; 125 | std::string get_html() const; 126 | 127 | Part &attach(const Part &attachment); 128 | Part &attach(const std::string &data, const std::string &mime_type, const std::string &filename = {}); 129 | Part &attach(std::istream &in, const std::string &mime_type, const std::string &filename = {}); 130 | std::vector get_attachments() const; 131 | 132 | void clear_alternative(const std::string &subtype); 133 | void clear_text(); 134 | void clear_plain(); 135 | void clear_html(); 136 | void clear_attachments(); 137 | 138 | void simplify(); 139 | 140 | bool has_text() const; 141 | bool has_plain() const; 142 | bool has_html() const; 143 | bool has_attachments() const; 144 | bool is_attachment() const; 145 | bool is_inline() const; 146 | 147 | // Format manipulation 148 | void set_crlf(bool value = true); 149 | }; 150 | 151 | class Message: public Part { 152 | public: 153 | Message(); 154 | }; 155 | 156 | bool operator==(const Part &lhs, const Part &rhs); 157 | bool operator!=(const Part &lhs, const Part &rhs); 158 | 159 | } 160 | 161 | inline std::ostream &operator<<(std::ostream &out, const Mimesis::Part &part) { 162 | part.save(out); 163 | return out; 164 | } 165 | 166 | inline std::istream &operator>>(std::istream &in, Mimesis::Part &part) { 167 | part.load(in); 168 | return in; 169 | } 170 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /test/multipart.cpp: -------------------------------------------------------------------------------- 1 | /* This tests manipulating multipart messages. */ 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | 11 | using namespace std; 12 | 13 | int main() { 14 | Mimesis::Message msg; 15 | 16 | // Set simple body 17 | msg.set_header("From", "me"); 18 | msg.set_body("body\r\n"); 19 | assert(msg.is_multipart() == false); 20 | assert(msg.get_body() == "body\r\n"); 21 | assert(msg.to_string() == "From: me\r\n\r\nbody\r\n"); 22 | 23 | // Clear body 24 | msg.clear_body(); 25 | assert(msg.get_body().empty()); 26 | assert(!msg.get_headers().empty()); 27 | 28 | // Clear everything 29 | msg.set_body("body\r\n"); 30 | msg.clear(); 31 | assert(msg.get_header("From").empty()); 32 | assert(msg.get_body().empty()); 33 | 34 | // Make multipart 35 | msg.set_header("From", "me"); 36 | msg.set_body("body\r\n"); 37 | msg.make_multipart("mixed"); 38 | assert(msg.is_multipart() == true); 39 | assert(msg.get_body().empty()); 40 | assert(msg.get_header("MIME-Version") == "1.0"); 41 | assert(msg.get_header_value("Content-Type") == "multipart/mixed"); 42 | assert(!msg.get_header_parameter("Content-Type", "boundary").empty()); 43 | assert(msg.get_boundary().size() == 32); 44 | assert(msg.get_parts()[0].get_body() == "body\r\n"); 45 | 46 | { 47 | auto &part = msg.append_part(); 48 | part.set_mime_type("text/plain"); 49 | part.set_body("second body\r\n"); 50 | } 51 | 52 | assert(msg.get_parts().size() == 2); 53 | assert(msg.get_parts()[1].get_body() == "second body\r\n"); 54 | 55 | // Set preamble and epilogue 56 | msg.set_preamble("preamble\r\n"); 57 | msg.set_epilogue("epilogue\r\n"); 58 | 59 | // Intermediate result 60 | msg.set_boundary("-"); 61 | assert(msg.to_string() == 62 | "From: me\r\n" 63 | "MIME-Version: 1.0\r\n" 64 | "Content-Type: multipart/mixed; boundary=-\r\n" 65 | "\r\n" 66 | "preamble\r\n" 67 | "---\r\n" 68 | "\r\n" 69 | "body\r\n" 70 | "---\r\n" 71 | "Content-Type: text/plain\r\n" 72 | "\r\n" 73 | "second body\r\n" 74 | "-----\r\n" 75 | "epilogue\r\n"); 76 | 77 | // Idempotent make multipart 78 | msg.make_multipart("mixed"); 79 | assert(msg.get_parts().size() == 2); 80 | 81 | // Make different multipart 82 | msg.make_multipart("parallel", "="); 83 | assert(msg.is_multipart("parallel")); 84 | assert(msg.get_preamble().empty()); 85 | assert(msg.get_epilogue().empty()); 86 | assert(msg.get_parts().size() == 1); 87 | { 88 | auto &part = msg.get_parts()[0]; 89 | assert(part.is_multipart("mixed")); 90 | assert(part.get_preamble() == "preamble\r\n"); 91 | assert(part.get_epilogue() == "epilogue\r\n"); 92 | } 93 | 94 | // Constant part access 95 | const auto &cmsg = msg; 96 | assert(cmsg.get_parts().size() == 1); 97 | 98 | // Clear parts 99 | msg.clear_parts(); 100 | assert(msg.is_multipart() == true); 101 | assert(msg.get_parts().empty()); 102 | 103 | // Make singlepart 104 | { 105 | auto &part = msg.append_part(); 106 | part.set_mime_type("foo/bar"); 107 | part.set_body("third body\r\n"); 108 | } 109 | assert(!msg.get_parts().empty()); 110 | assert(msg.flatten()); 111 | assert(msg.is_singlepart("foo")); 112 | assert(!msg.is_singlepart("bar")); 113 | assert(msg.is_singlepart("foo/bar")); 114 | assert(msg.get_parts().empty()); 115 | assert(msg.get_body() == "third body\r\n"); 116 | 117 | // High-level functions 118 | msg.clear(); 119 | msg.set_plain("plain body\r\n"); 120 | assert(msg.is_singlepart("text/plain")); 121 | 122 | msg.set_html("html body\r\n"); 123 | assert(msg.is_multipart("alternative")); 124 | assert(msg.get_parts().size() == 2); 125 | assert(msg.get_parts()[0].is_singlepart("text/plain")); 126 | assert(msg.get_parts()[0].get_body() == "plain body\r\n"); 127 | assert(msg.get_parts()[1].is_singlepart("text/html")); 128 | assert(msg.get_parts()[1].get_body() == "html body\r\n"); 129 | 130 | msg.attach("attachment\r\n", "text/plain", "foo"); 131 | assert(msg.is_multipart("mixed")); 132 | assert(msg.get_parts().size() == 2); 133 | assert(msg.get_parts()[0].is_multipart("alternative")); 134 | assert(msg.get_parts()[0].get_header_value("Content-Disposition") != "attachment"); 135 | assert(msg.get_parts()[0].is_attachment() == false); 136 | assert(msg.get_parts()[0].has_attachments() == false); 137 | assert(msg.get_parts()[1].is_singlepart("text/plain")); 138 | assert(msg.get_parts()[1].get_header_value("Content-Disposition") == "attachment"); 139 | assert(msg.get_parts()[1].get_header_parameter("Content-Disposition", "filename") == "foo"); 140 | assert(msg.get_parts()[1].is_attachment() == true); 141 | assert(msg.get_parts()[1].has_attachments() == true); 142 | 143 | assert(msg.get_text() == "plain body\r\n"); 144 | assert(msg.get_plain() == "plain body\r\n"); 145 | assert(msg.get_html() == "html body\r\n"); 146 | assert(msg.has_attachments()); 147 | assert(msg.get_attachments().size() == 1); 148 | assert(msg.get_attachments()[0]->is_singlepart("text/plain")); 149 | assert(msg.get_attachments()[0]->get_body() == "attachment\r\n"); 150 | 151 | // Const access 152 | assert(cmsg.get_first_matching_body("text/plain") == "plain body\r\n"); 153 | assert(cmsg.get_first_matching_body("text/pdf").empty()); 154 | 155 | // Delete parts 156 | msg.clear_text(); 157 | assert(msg.has_text() == false); 158 | assert(msg.get_attachments().size() == 1); 159 | assert(msg.is_singlepart("text/plain")); 160 | 161 | msg.clear_attachments(); 162 | assert(msg.has_attachments() == false); 163 | assert(msg.get_body().empty()); 164 | assert(msg.get_header("Content-Type").empty()); 165 | assert(msg.has_mime_type() == false); 166 | 167 | // Different order 168 | msg.clear(); 169 | msg.attach("attachment\r\n", "text/plain", "foo"); 170 | msg.set_html("html body\r\n"); 171 | msg.set_plain("plain body\r\n"); 172 | assert(msg.is_multipart("mixed")); 173 | assert(msg.get_parts().size() == 2); 174 | assert(msg.get_parts()[0].is_multipart("alternative")); 175 | assert(msg.get_parts()[0].is_attachment() == false); 176 | assert(msg.get_parts()[1].is_singlepart("text/plain")); 177 | assert(msg.get_parts()[1].is_attachment() == true); 178 | assert(msg.get_parts()[1].get_header_parameter("Content-Disposition", "filename") == "foo"); 179 | { 180 | auto &alternative = msg.get_parts()[0]; 181 | assert(alternative.get_parts().size() == 2); 182 | assert(alternative.get_parts()[0].is_singlepart("text/html")); 183 | assert(alternative.get_parts()[1].is_singlepart("text/plain")); 184 | } 185 | 186 | assert(msg.get_text() == "html body\r\n"); 187 | assert(msg.get_plain() == "plain body\r\n"); 188 | assert(msg.get_html() == "html body\r\n"); 189 | assert(msg.get_attachments().size() == 1); 190 | assert(msg.get_attachments()[0]->is_singlepart("text/plain")); 191 | assert(msg.get_attachments()[0]->get_body() == "attachment\r\n"); 192 | 193 | // Delete parts 194 | msg.clear_attachments(); 195 | assert(msg.has_text() == true); 196 | assert(msg.has_attachments() == false); 197 | assert(msg.is_multipart("alternative")); 198 | 199 | msg.clear_plain(); 200 | assert(msg.has_text() == true); 201 | assert(msg.has_plain() == false); 202 | assert(msg.has_html() == true); 203 | assert(msg.is_singlepart("text/html")); 204 | 205 | // Add to existing multipart/alternative 206 | msg.clear(); 207 | msg.make_multipart("alternative"); 208 | msg.set_html("html body\r\n"); 209 | assert(msg.is_multipart("alternative")); 210 | assert(msg.get_parts().size() == 1); 211 | assert(msg.get_parts()[0].is_singlepart("text/html")); 212 | msg.clear_html(); 213 | assert(msg.is_multipart() == false); 214 | assert(msg.get_body().empty()); 215 | assert(msg.has_mime_type() == false); 216 | 217 | // Combine with existing text part 218 | msg.clear(); 219 | msg.make_multipart("alternative"); 220 | msg.set_plain("plain body\r\n"); 221 | msg.make_multipart("mixed"); 222 | msg.set_html("html body\r\n"); 223 | assert(msg.get_header_value("Content-Type") == "multipart/mixed"); 224 | assert(msg.get_parts().size() == 1); 225 | { 226 | auto &alternative = msg.get_parts()[0]; 227 | assert(alternative.is_multipart("alternative")); 228 | assert(alternative.get_parts().size() == 2); 229 | assert(alternative.get_parts()[0].is_singlepart("text/plain")); 230 | assert(alternative.get_parts()[1].is_singlepart("text/html")); 231 | } 232 | 233 | // Simplify 234 | msg.simplify(); 235 | assert(msg.is_multipart("alternative")); 236 | assert(msg.get_parts().size() == 2); 237 | assert(msg.get_parts()[0].is_singlepart("text/plain")); 238 | assert(msg.get_parts()[1].is_singlepart("text/html")); 239 | 240 | // Overwrite parts 241 | msg.set_html("html body 2\r\n"); 242 | msg.set_plain("plain body 2\r\n"); 243 | assert(msg.get_parts().size() == 2); 244 | assert(msg.get_parts()[0].get_body() == "plain body 2\r\n"); 245 | assert(msg.get_parts()[1].get_body() == "html body 2\r\n"); 246 | 247 | // Flatten 248 | assert(msg.flatten() == false); 249 | msg.clear_parts(); 250 | assert(msg.flatten() == true); 251 | assert(msg.flatten() == true); 252 | 253 | // Attach from stream 254 | msg.clear(); 255 | { 256 | istringstream in("Stream attachment\n"); 257 | msg.attach(in, "text/plain", "attachment.txt"); 258 | } 259 | assert(msg.get_attachments().size() == 1); 260 | { 261 | auto part = msg.get_attachments()[0]; 262 | assert(part->is_singlepart("text/plain")); 263 | assert(part->get_header_parameter("Content-Disposition", "filename") == "attachment.txt"); 264 | assert(part->get_body() == "Stream attachment\n"); 265 | } 266 | 267 | // Attach a part 268 | msg.clear(); 269 | { 270 | Mimesis::Part part; 271 | part.set_header("Content-Type", "text/plain"); 272 | part.set_body("plain body\r\n"); 273 | msg.attach(part); 274 | } 275 | assert(msg.is_singlepart("text/plain")); 276 | assert(msg.get_attachments().size() == 1); 277 | 278 | // Attach another part 279 | { 280 | Mimesis::Part part; 281 | part.set_header("Content-Type", "text/html"); 282 | part.set_body("html body\r\n"); 283 | msg.attach(part); 284 | } 285 | assert(msg.is_multipart("mixed")); 286 | assert(msg.get_attachments().size() == 2); 287 | assert(msg.get_attachments()[0]->is_singlepart("text/plain")); 288 | assert(msg.get_attachments()[1]->is_singlepart("text/html")); 289 | 290 | // Attach a message 291 | msg.clear(); 292 | { 293 | Mimesis::Message msg2; 294 | msg2.set_header("From", "me"); 295 | msg2.set_body("body\r\n"); 296 | msg.attach(msg2); 297 | } 298 | assert(msg.is_multipart() == false); 299 | assert(msg.is_singlepart() == true); 300 | assert(msg.get_header_value("Content-Type") == "message/rfc822"); 301 | assert(msg.get_attachments().size() == 1); 302 | 303 | // Attach another message 304 | { 305 | Mimesis::Message msg2; 306 | msg2.set_header("From", "me"); 307 | msg2.set_body("body\r\n"); 308 | msg.attach(msg2); 309 | } 310 | assert(msg.is_multipart("mixed")); 311 | assert(msg.get_attachments().size() == 2); 312 | assert(msg.get_attachments()[0]->is_singlepart("message/rfc822")); 313 | assert(msg.get_attachments()[1]->is_singlepart("message/rfc822")); 314 | 315 | // Transplant parts 316 | msg.clear(); 317 | msg.attach("attachment\r\n", "text/plain", "foo"); 318 | msg.attach("attachment\r\n", "text/plain", "bar"); 319 | { 320 | Mimesis::Part other; 321 | other.set_plain("plain body\r\n"); 322 | other.set_html("html body\r\n"); 323 | msg.set_parts(other.get_parts()); 324 | assert(other.get_parts().size() == 2); 325 | } 326 | assert(msg.get_parts().size() == 2); 327 | assert(msg.get_parts()[0].is_singlepart("text/plain")); 328 | assert(msg.get_parts()[1].is_singlepart("text/html")); 329 | 330 | } 331 | -------------------------------------------------------------------------------- /src/mimesis.cpp: -------------------------------------------------------------------------------- 1 | /* Mimesis -- a library for parsing and creating RFC2822 messages 2 | Copyright © 2017 Guus Sliepen 3 | 4 | Mimesis is free software; you can redistribute it and/or modify it under the 5 | terms of the GNU Lesser General Public License as published by the Free 6 | Software Foundation, either version 3 of the License, or (at your option) 7 | any later version. 8 | 9 | This program is distributed in the hope that it will be useful, but WITHOUT 10 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 11 | FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 12 | more details. 13 | 14 | You should have received a copy of the GNU Lesser General Public License 15 | along with this program. If not, see . 16 | */ 17 | 18 | #include "mimesis.hpp" 19 | 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | 28 | #include "base64.hpp" 29 | #include "charset.hpp" 30 | #include "quoted-printable.hpp" 31 | #include "string_view.hpp" 32 | 33 | using namespace std; 34 | 35 | namespace Mimesis { 36 | 37 | static std::random_device rnd; 38 | 39 | static string unquote(const string &str) { 40 | if (str.empty() || str[0] != '"') 41 | return str; 42 | 43 | string unquoted; 44 | int quotes_wanted = 2; 45 | 46 | for (auto &&c: str) { 47 | if (c == '"') { 48 | if (--quotes_wanted) 49 | continue; 50 | break; 51 | } 52 | if (c == '\\') 53 | continue; 54 | unquoted.push_back(c); 55 | } 56 | 57 | return unquoted; 58 | } 59 | 60 | static string quote(const string &str) { 61 | bool do_quote = false; 62 | 63 | for (auto &&c: str) { 64 | if (isalnum(c) || strchr("!#$%&'*+-/=?^_`{|}~", c)) 65 | continue; 66 | do_quote = true; 67 | break; 68 | } 69 | 70 | if (!do_quote) 71 | return str; 72 | 73 | string quoted = "\""; 74 | for (auto &&c: str) { 75 | if (c == '\"' || c == '\\') 76 | quoted.push_back('\\'); 77 | quoted.push_back(c); 78 | } 79 | quoted.push_back('"'); 80 | 81 | return quoted; 82 | } 83 | 84 | static bool streqi(const string &a, const string &b) { 85 | if (a.size() != b.size()) 86 | return false; 87 | 88 | for (size_t i = 0; i < a.size(); i++) 89 | if (tolower(a[i]) != tolower(b[i])) 90 | return false; 91 | 92 | return true; 93 | } 94 | 95 | static bool streqi(const string &a, size_t offset_a, size_t len_a, const string &b) { 96 | if (min(a.size() - offset_a, len_a) != b.size()) 97 | return false; 98 | 99 | for (size_t i = 0; i < len_a; i++) 100 | if (tolower(a[i + offset_a]) != tolower(b[i])) 101 | return false; 102 | 103 | return true; 104 | } 105 | 106 | static bool streqi(const string &a, size_t offset_a, size_t len_a, const string &b, size_t offset_b, size_t len_b) { 107 | if (min(a.size() - offset_a, len_a) != min(b.size() - offset_b, len_b)) 108 | return false; 109 | 110 | for (size_t i = 0; i < min(a.size() - offset_a, len_a); i++) 111 | if (tolower(a[i + offset_a]) != tolower(b[i + offset_b])) 112 | return false; 113 | 114 | return true; 115 | } 116 | 117 | static string encode_header(const string &str) { 118 | string encoded = "=?utf-8?b?"; 119 | encoded.append(base64_encode(str)); 120 | encoded.append("?="); 121 | return encoded; 122 | } 123 | 124 | static string decode_header(const string &str) { 125 | size_t start = str.find("=?"); 126 | 127 | if (start == str.npos) 128 | return str; 129 | 130 | size_t from = 0; 131 | string decoded; 132 | 133 | while (start != str.npos) { 134 | decoded.append(str, from, start - from); 135 | size_t encoding = str.find("?", start + 2); 136 | if (encoding == str.npos) 137 | return str; 138 | size_t text = str.find("?", encoding + 1); 139 | if (text == str.npos) 140 | return str; 141 | size_t end = str.find("?=", text + 1); 142 | if (end == str.npos) 143 | return str; 144 | string charset = str.substr(start + 2, encoding - (start + 2)); 145 | string todo = str.substr(text + 1, end - (text + 1)); 146 | if (streqi(str, encoding + 1, 1, "q")) 147 | todo = quoted_printable_decode(todo); 148 | else if (streqi(str, encoding + 1, 1, "b")) 149 | todo = base64_decode(todo); 150 | else 151 | return str; 152 | todo = charset_decode(charset, todo); 153 | decoded.append(todo); 154 | 155 | from = end + 2; 156 | start = str.find("=?", from); 157 | } 158 | 159 | return decoded; 160 | } 161 | 162 | static string generate_boundary() { 163 | unsigned int nonce[24 / sizeof(unsigned int)]; 164 | for (auto &val: nonce) 165 | val = rnd(); 166 | return base64_encode(string_view(reinterpret_cast(nonce), sizeof nonce)); 167 | } 168 | 169 | static bool is_boundary(const std::string &line, const std::string &boundary) { 170 | if (boundary.empty()) 171 | return false; 172 | 173 | if (line.compare(0, 2, "--")) 174 | return false; 175 | 176 | if (line.compare(2, boundary.size(), boundary)) 177 | return false; 178 | 179 | return true; 180 | } 181 | 182 | static bool is_final_boundary(const std::string &line, const std::string &boundary) { 183 | if (line.compare(2 + boundary.size(), 2, "--")) 184 | return false; 185 | 186 | return is_boundary(line, boundary); 187 | } 188 | 189 | static bool types_match(const std::string &a, const std::string &b) { 190 | auto a_slash = a.find('/'); 191 | auto b_slash = b.find('/'); 192 | if (a_slash == string::npos || b_slash == string::npos) 193 | return streqi(a, 0, a_slash, b, 0, b_slash); 194 | else 195 | return streqi(a, b); 196 | } 197 | 198 | static void set_value(string &str, const string &value) { 199 | size_t semicolon = str.find(';'); 200 | 201 | if (semicolon == string::npos) 202 | str = value; 203 | else 204 | str.replace(0, semicolon, value); 205 | } 206 | 207 | static string get_value(const string &str) { 208 | return str.substr(0, str.find(';')); 209 | } 210 | 211 | static pair get_parameter_value_range(const string &str, const string ¶meter) { 212 | size_t start = 0; 213 | size_t end = string::npos; 214 | 215 | // Find a semicolon, which marks the start of a parameter. 216 | while((start = str.find(';', start)) != string::npos) { 217 | start++; 218 | while (isspace(str[start])) 219 | start++; 220 | if (!streqi(str, start, parameter.size(), parameter)) { 221 | // It's not the wanted parameter. 222 | start = str.find('=', start); 223 | while (isspace(str[start])) 224 | start++; 225 | if (str[start] != '=') 226 | continue; 227 | while (isspace(str[start])) 228 | start++; 229 | // If it's a quoted parameter, skip over the quoted text. 230 | if (str[start] == '"') { 231 | start++; 232 | while (start < str.size() && str[start] != '"') { 233 | if (str[start] == '\\' && str.size() > start - 1) 234 | start++; 235 | start++; 236 | } 237 | } 238 | continue; 239 | } 240 | // Skip until we get to the value. 241 | start += parameter.size(); 242 | while (isspace(str[start])) 243 | start++; 244 | if (str[start] != '=') 245 | continue; 246 | start++; 247 | while (isspace(str[start])) 248 | start++; 249 | end = start; 250 | if (str[end] == '"') { 251 | // It's a quoted parameter. 252 | end++; 253 | while (end < str.size() && str[end] != '"') { 254 | if (str[end] == '\\' && str.size() > end - 1) 255 | end++; 256 | end++; 257 | } 258 | } else { 259 | while (end < str.size() && str[end] != ';' && !isspace(str[end])) 260 | end++; 261 | } 262 | 263 | break; 264 | } 265 | 266 | return make_pair(start, end); 267 | } 268 | 269 | static void set_parameter(string &str, const string ¶meter, const string &value) { 270 | auto range = get_parameter_value_range(str, parameter); 271 | auto start = range.first; 272 | auto end = range.second; 273 | 274 | if (start == string::npos) 275 | str += "; " + parameter + "=" + quote(value); 276 | else 277 | str.replace(start, end - start, quote(value)); 278 | } 279 | 280 | static string get_parameter(const string &str, const string ¶meter) { 281 | auto range = get_parameter_value_range(str, parameter); 282 | auto start = range.first; 283 | auto end = range.second; 284 | 285 | if (start == string::npos) 286 | return {}; 287 | 288 | return unquote(str.substr(start, end - start)); 289 | } 290 | 291 | static const string ending[2] = {"\n", "\r\n"}; 292 | 293 | Part::Part(): 294 | headers(), 295 | preamble(), 296 | body(), 297 | epilogue(), 298 | parts(), 299 | boundary(), 300 | multipart(false), 301 | crlf(true), 302 | message(false) 303 | {} 304 | 305 | // Loading and saving a whole MIME message 306 | 307 | string Part::load(istream &in, const string &parent_boundary) { 308 | string line; 309 | int ncrlf = 0; 310 | int nlf = 0; 311 | 312 | while (getline(in, line)) { 313 | if (is_boundary(line, parent_boundary)) 314 | return line; 315 | 316 | if (line.size() && line.back() == '\r') { 317 | ncrlf++; 318 | line.erase(line.size() - 1); 319 | } else { 320 | nlf++; 321 | } 322 | 323 | if (line.empty()) 324 | break; 325 | 326 | if (isspace(line[0])) { 327 | if (headers.empty()) 328 | throw runtime_error("invalid header line"); 329 | headers.back().second.append(line); 330 | continue; 331 | } 332 | 333 | size_t colon = string::npos; 334 | 335 | for (size_t i = 0; i < line.size(); ++i) { 336 | if (line[i] == ':') { 337 | colon = i; 338 | break; 339 | } 340 | 341 | if (line[i] < 33 || static_cast(line[i]) > 127) { 342 | if (i == 4 && line[i] == ' ' && line.compare(0, 4, "From") == 0 && headers.empty()) { 343 | colon = i; 344 | break; 345 | } 346 | throw runtime_error("invalid header line " + line + std::to_string(i)); 347 | } 348 | } 349 | 350 | if (colon == 0 || colon == string::npos) 351 | throw runtime_error("invalid header line"); 352 | 353 | if (line[colon] != ':') 354 | continue; 355 | 356 | auto start = colon + 1; 357 | while (start < line.size() && isspace(line[start])) 358 | start++; 359 | 360 | // Empty header values are allowed for most fields. 361 | 362 | auto field = line.substr(0, colon); 363 | auto value = line.substr(start); 364 | 365 | headers.emplace_back(field, value); 366 | } 367 | 368 | crlf = ncrlf > nlf; 369 | 370 | const string content_type = get_header("Content-Type"); 371 | 372 | if (types_match(get_value(content_type), "multipart")) { 373 | boundary = get_parameter(content_type, "boundary"); 374 | if (boundary.empty()) 375 | throw runtime_error("multipart but no boundary specified"); 376 | multipart = true; 377 | } else { 378 | multipart = false; 379 | } 380 | 381 | if (!multipart) { 382 | while (getline(in, line)) { 383 | if (is_boundary(line, parent_boundary)) 384 | return line; 385 | line.push_back('\n'); 386 | body.append(line); 387 | } 388 | } else { 389 | while (getline(in, line)) { 390 | if (is_boundary(line, parent_boundary)) 391 | return line; 392 | if (is_boundary(line, boundary)) 393 | break; 394 | line.push_back('\n'); 395 | preamble.append(line); 396 | } 397 | 398 | while (true) { 399 | parts.emplace_back(); 400 | string last_line = parts.back().load(in, boundary); 401 | if (!is_boundary(last_line, boundary)) 402 | throw runtime_error("invalid boundary"); 403 | if (is_final_boundary(last_line, boundary)) 404 | break; 405 | } 406 | 407 | while (getline(in, line)) { 408 | if (is_boundary(line, parent_boundary)) 409 | return line; 410 | line.push_back('\n'); 411 | epilogue.append(line); 412 | } 413 | } 414 | 415 | if (in.bad()) 416 | throw runtime_error("error reading message"); 417 | 418 | return {}; 419 | } 420 | 421 | void Part::save(ostream &out) const { 422 | bool has_headers = false; 423 | 424 | for (auto &header: headers) { 425 | if (!header.second.empty()) { 426 | out << header.first << ": " << header.second << ending[crlf]; 427 | has_headers = true; 428 | } 429 | } 430 | 431 | if (message && !has_headers) 432 | throw runtime_error("no headers specified"); 433 | 434 | out << ending[crlf]; 435 | 436 | if (parts.empty()) { 437 | out << body; 438 | } else { 439 | out << preamble; 440 | for (auto &part: parts) { 441 | out << "--" << boundary << ending[crlf]; 442 | part.save(out); 443 | } 444 | out << "--" << boundary << "--" << ending[crlf]; 445 | out << epilogue; 446 | } 447 | } 448 | 449 | void Part::load(const string &filename) { 450 | ifstream in(filename); 451 | if (!in.is_open()) 452 | throw runtime_error("could not open message file"); 453 | load(in); 454 | } 455 | 456 | void Part::save(const string &filename) const { 457 | ofstream out(filename); 458 | if (!out.is_open()) 459 | throw runtime_error("could not open message file"); 460 | save(out); 461 | out.close(); 462 | if (out.fail()) 463 | throw runtime_error("could not write message file"); 464 | } 465 | 466 | void Part::from_string(const string &data) { 467 | istringstream in(data); 468 | load(in); 469 | } 470 | 471 | string Part::to_string() const { 472 | ostringstream out; 473 | save(out); 474 | return out.str(); 475 | } 476 | 477 | void Part::set_crlf(bool value) { 478 | crlf = value; 479 | } 480 | 481 | // Low-level access 482 | 483 | string Part::get_body() const { 484 | string result; 485 | auto encoding = get_header_value("Content-Transfer-Encoding"); 486 | 487 | if (streqi(encoding, "quoted-printable")) 488 | result = quoted_printable_decode(body); 489 | if (streqi(encoding, "base64")) 490 | result = base64_decode(body); 491 | else 492 | result = body; 493 | 494 | if (is_mime_type("text")) { 495 | auto charset = get_header_parameter("Content-Type", "charset"); 496 | if (!charset.empty() && !streqi(charset, "utf-8") && !streqi(charset, "us-ascii") && !streqi(charset, "ascii")) { 497 | result = charset_decode(charset, result); 498 | } 499 | } 500 | 501 | return result; 502 | } 503 | 504 | string Part::get_preamble() const { 505 | return preamble; 506 | } 507 | 508 | string Part::get_epilogue() const { 509 | return epilogue; 510 | } 511 | 512 | string Part::get_boundary() const { 513 | return boundary; 514 | } 515 | 516 | vector &Part::get_parts() { 517 | return parts; 518 | } 519 | 520 | const vector &Part::get_parts() const { 521 | return parts; 522 | } 523 | 524 | vector> &Part::get_headers() { 525 | return headers; 526 | } 527 | 528 | const vector> &Part::get_headers() const { 529 | return headers; 530 | } 531 | 532 | bool Part::is_multipart() const { 533 | return multipart; 534 | } 535 | 536 | bool Part::is_multipart(const std::string &subtype) const { 537 | return multipart && get_header_value("Content-Type") == "multipart/" + subtype; 538 | } 539 | 540 | bool Part::is_singlepart() const { 541 | return !multipart; 542 | } 543 | 544 | bool Part::is_singlepart(const std::string &type) const { 545 | return !multipart && types_match(get_header_value("Content-Type"), type); 546 | } 547 | 548 | bool Part::is_attachment() const { 549 | return get_header_value("Content-Disposition") == "attachment"; 550 | } 551 | 552 | bool Part::is_inline() const { 553 | return get_header_value("Content-Disposition") == "inline"; 554 | } 555 | 556 | void Part::set_body(const string &value) { 557 | if (multipart) 558 | throw runtime_error("Cannot set body of a multipart message"); 559 | body = value; 560 | } 561 | 562 | void Part::set_preamble(const string &value) { 563 | if (!multipart) 564 | throw runtime_error("Cannot set preamble of a non-multipart message"); 565 | preamble = value; 566 | } 567 | 568 | void Part::set_epilogue(const string &value) { 569 | if (!multipart) 570 | throw runtime_error("Cannot set epilogue of a non-multipart message"); 571 | epilogue = value; 572 | } 573 | 574 | void Part::set_boundary(const std::string &value) { 575 | boundary = value; 576 | if (has_mime_type()) 577 | set_header_parameter("Content-Type", "boundary", boundary); 578 | } 579 | 580 | void Part::set_parts(const vector &value) { 581 | if (!multipart) 582 | throw runtime_error("Cannot set parts of a non-multipart message"); 583 | parts = value; 584 | } 585 | 586 | void Part::set_headers(const vector> &value) { 587 | headers = value; 588 | } 589 | 590 | void Part::clear() { 591 | headers.clear(); 592 | preamble.clear(); 593 | body.clear(); 594 | epilogue.clear(); 595 | parts.clear(); 596 | boundary.clear(); 597 | multipart = false; 598 | } 599 | 600 | void Part::clear_body() { 601 | body.clear(); 602 | } 603 | 604 | // Header manipulation 605 | 606 | static bool iequals(const string &a, const string &b) { 607 | if (a.size() != b.size()) 608 | return false; 609 | 610 | for (size_t i = 0; i < a.size(); ++i) 611 | if (tolower(a[i]) != tolower(b[i])) 612 | return false; 613 | 614 | return true; 615 | } 616 | 617 | string Part::get_header(const string &field) const { 618 | for (const auto &header: headers) 619 | if (iequals(header.first, field)) 620 | return header.second; 621 | 622 | return {}; 623 | } 624 | 625 | void Part::set_header(const string &field, const string &value) { 626 | for (auto &header: headers) { 627 | if (iequals(header.first, field)) { 628 | header.second = value; 629 | return; 630 | } 631 | } 632 | 633 | append_header(field, value); 634 | } 635 | 636 | string &Part::operator[](const string &field) { 637 | for (auto &header: headers) 638 | if (iequals(header.first, field)) 639 | return header.second; 640 | 641 | append_header(field, {}); 642 | return headers.back().second; 643 | } 644 | 645 | const string &Part::operator[](const string &field) const { 646 | for (auto &header: headers) 647 | if (iequals(header.first, field)) 648 | return header.second; 649 | 650 | static string empty_string; 651 | return empty_string; 652 | } 653 | 654 | void Part::append_header(const string &field, const string &value) { 655 | headers.push_back(make_pair(field, value)); 656 | } 657 | 658 | void Part::prepend_header(const string &field, const string &value) { 659 | headers.insert(begin(headers), make_pair(field, value)); 660 | } 661 | 662 | void Part::erase_header(const string &field) { 663 | headers.erase(remove_if(begin(headers), end(headers), [&](pair &header){ 664 | return header.first == field; 665 | }), end(headers)); 666 | } 667 | 668 | void Part::clear_headers() { 669 | headers.clear(); 670 | } 671 | 672 | string Part::get_header_value(const string &field) const { 673 | return get_value(get_header(field)); 674 | } 675 | 676 | string Part::get_header_parameter(const string &field, const string ¶meter) const { 677 | return get_parameter(get_header(field), parameter); 678 | } 679 | 680 | void Part::set_header_value(const string &field, const string &value) { 681 | for (auto &header: headers) { 682 | if (iequals(header.first, field)) { 683 | set_value(header.second, value); 684 | return; 685 | } 686 | } 687 | 688 | append_header(field, value); 689 | } 690 | 691 | void Part::set_header_parameter(const string &field, const string ¶meter, const string &value) { 692 | for (auto &header: headers) { 693 | if (iequals(header.first, field)) { 694 | set_parameter(header.second, parameter, value); 695 | return; 696 | } 697 | } 698 | 699 | append_header(field, "; " + parameter + "=" + value); 700 | } 701 | 702 | static string get_date_string(const chrono::system_clock::time_point &date = chrono::system_clock::now()) { 703 | time_t t = chrono::system_clock::to_time_t(date); 704 | struct tm tm{}; 705 | localtime_r(&t, &tm); 706 | char str[128]; 707 | char *oldlocale = setlocale(LC_TIME, "C"); 708 | size_t result = strftime(str, sizeof str, "%a, %d %b %Y %T %z", &tm); 709 | setlocale(LC_TIME, oldlocale); 710 | if (result == 0) 711 | throw runtime_error("Could not convert date to string"); 712 | return str; 713 | } 714 | 715 | void Part::add_received(const string &text, const chrono::system_clock::time_point &date) { 716 | prepend_header("Received", text + "; " + get_date_string(date)); 717 | } 718 | 719 | void Part::generate_msgid(const string &domain) { 720 | auto now = chrono::system_clock::now(); 721 | uint64_t buf[3]; 722 | buf[0] = ((uint64_t)rnd() << 32) | rnd(); 723 | buf[1] = chrono::duration_cast(now.time_since_epoch()).count(); 724 | buf[2] = ((uint64_t)rnd() << 32) | rnd(); 725 | string msgid = "<" + base64_encode(string_view(reinterpret_cast(buf), sizeof buf)) + "@" + domain + ">"; 726 | set_header("Message-ID", msgid); 727 | } 728 | 729 | void Part::set_date(const chrono::system_clock::time_point &date) { 730 | set_header("Date", get_date_string(date)); 731 | } 732 | 733 | // Part manipulation 734 | 735 | Part &Part::append_part(const Part &part) { 736 | parts.push_back(part); 737 | return parts.back(); 738 | } 739 | 740 | Part &Part::prepend_part(const Part &part) { 741 | parts.insert(begin(parts), part); 742 | return parts.front(); 743 | } 744 | 745 | void Part::clear_parts() { 746 | parts.clear(); 747 | } 748 | 749 | void Part::make_multipart(const string &subtype, const string &suggested_boundary) { 750 | if (multipart) { 751 | if (is_multipart(subtype)) 752 | return; 753 | Part part; 754 | part.preamble = move(preamble); 755 | part.epilogue = move(epilogue); 756 | part.parts = move(parts); 757 | part.boundary = move(boundary); 758 | part.multipart = true; 759 | part.set_header("Content-Type", get_header("Content-Type")); 760 | part.set_header("Content-Disposition", get_header("Content-Disposition")); 761 | erase_header("Content-Disposition"); 762 | part.crlf = crlf; 763 | parts.emplace_back(move(part)); 764 | } else { 765 | multipart = true; 766 | 767 | if (message) 768 | set_header("MIME-Version", "1.0"); 769 | 770 | if (!body.empty()) { 771 | auto &part = append_part(); 772 | part.set_header("Content-Type", get_header("Content-Type")); 773 | part.set_header("Content-Disposition", get_header("Content-Disposition")); 774 | erase_header("Content-Disposition"); 775 | part.body = move(body); 776 | } 777 | } 778 | 779 | if (!suggested_boundary.empty()) 780 | set_boundary(suggested_boundary); 781 | if (boundary.empty()) 782 | boundary = generate_boundary(); 783 | 784 | set_header("Content-Type", "multipart/" + subtype + "; boundary=" + boundary); 785 | } 786 | 787 | bool Part::flatten() { 788 | if (!multipart) 789 | return true; 790 | 791 | if (parts.empty()) { 792 | multipart = false; 793 | return true; 794 | } 795 | 796 | if (parts.size() > 1) 797 | return false; 798 | 799 | auto &part = parts.front(); 800 | set_header("Content-Type", part.get_header("Content-Type")); 801 | set_header("Content-Disposition", part.get_header("Content-Disposition")); 802 | 803 | if (part.multipart) { 804 | parts = move(part.parts); 805 | } else { 806 | multipart = false; 807 | set_body(part.get_body()); 808 | parts.clear(); 809 | } 810 | 811 | return true; 812 | } 813 | 814 | // Body and attachments 815 | 816 | string Part::get_mime_type() const { 817 | return get_header_value("Content-Type"); 818 | } 819 | 820 | void Part::set_mime_type(const std::string &type) { 821 | return set_header_value("Content-Type", type); 822 | } 823 | 824 | bool Part::is_mime_type(const std::string &type) const { 825 | return types_match(get_mime_type(), type); 826 | } 827 | 828 | bool Part::has_mime_type() const { 829 | return !get_mime_type().empty(); 830 | } 831 | 832 | const Part *Part::get_first_matching_part(function predicate) const { 833 | if (!multipart) { 834 | if (headers.empty() && body.empty()) 835 | return nullptr; 836 | if (is_attachment()) 837 | return nullptr; 838 | } 839 | 840 | if (predicate(*this)) 841 | return this; 842 | 843 | for (auto &part: parts) { 844 | auto result = part.get_first_matching_part(predicate); 845 | if (result) 846 | return result; 847 | } 848 | 849 | return nullptr; 850 | } 851 | 852 | Part *Part::get_first_matching_part(function predicate) { 853 | auto result = ((const Part *)this)->get_first_matching_part(predicate); 854 | return const_cast(result); 855 | } 856 | 857 | const Part *Part::get_first_matching_part(const string &type) const { 858 | return get_first_matching_part([type](const Part &part){ 859 | auto my_type = part.get_mime_type(); 860 | return types_match(my_type.empty() ? "text/plain" : my_type, type); 861 | }); 862 | } 863 | 864 | Part *Part::get_first_matching_part(const string &type) { 865 | auto result = ((const Part *)this)->get_first_matching_part(type); 866 | return const_cast(result); 867 | } 868 | 869 | string Part::get_first_matching_body(const string &type) const { 870 | const auto &part = get_first_matching_part(type); 871 | if (part) 872 | return part->get_body(); 873 | else 874 | return {}; 875 | } 876 | 877 | Part &Part::set_alternative(const string &subtype, const string &text) { 878 | string type = "text/" + subtype; 879 | Part *part = nullptr; 880 | 881 | // Try to put it in the body first. 882 | if (!multipart) { 883 | if (body.empty() || is_mime_type(type)) { 884 | part = this; 885 | } else if (is_mime_type("text") && !is_attachment()) { 886 | make_multipart("alternative"); 887 | part = &append_part(); 888 | } else { 889 | make_multipart("mixed"); 890 | part = &prepend_part(); 891 | } 892 | } else { 893 | // If there is already a text/plain part, use that one. 894 | part = get_first_matching_part(type); 895 | if (part) { 896 | part->set_mime_type(type); 897 | part->set_body(text); 898 | return *part; 899 | } 900 | 901 | // If there is already a multipart/alternative with text, use that one. 902 | part = get_first_matching_part([](const Part &part){ 903 | return part.is_multipart("alternative") 904 | && !part.parts.empty() 905 | && part.get_first_matching_part("text"); 906 | }); 907 | if (part) 908 | part = &part->append_part(); 909 | 910 | // If there is already inline text, make it multipart/alternative. 911 | 912 | if (!part && (part = get_first_matching_part("text"))) { 913 | part->make_multipart("alternative"); 914 | part = &part->append_part(); 915 | } 916 | 917 | // Otherwise, assume we're multipart/mixed. 918 | if (!part) 919 | part = &prepend_part(); 920 | } 921 | 922 | part->set_header("Content-Type", type); 923 | part->set_body(text); 924 | 925 | return *part; 926 | } 927 | 928 | void Part::set_plain(const string &text) { 929 | set_alternative("plain", text); 930 | } 931 | 932 | void Part::set_html(const string &html) { 933 | set_alternative("html", html); 934 | } 935 | 936 | string Part::get_plain() const { 937 | return get_first_matching_body("text/plain"); 938 | } 939 | 940 | string Part::get_html() const { 941 | return get_first_matching_body("text/html"); 942 | } 943 | 944 | string Part::get_text() const { 945 | return get_first_matching_body("text"); 946 | } 947 | 948 | Part &Part::attach(const Part &attachment) { 949 | if (!multipart && body.empty()) { 950 | if (attachment.message) { 951 | set_header("Content-Type", "message/rfc822"); 952 | body = attachment.to_string(); 953 | } else { 954 | set_header("Content-Type", attachment.get_header("Content-Type")); 955 | body = attachment.body; 956 | } 957 | set_header("Content-Disposition", "attachment"); 958 | return *this; 959 | } 960 | 961 | make_multipart("mixed"); 962 | auto &part = append_part(); 963 | if (attachment.message) { 964 | part.set_header("Content-Type", "message/rfc822"); 965 | part.body = attachment.to_string(); 966 | } else { 967 | part.set_header("Content-Type", attachment.get_header("Content-Type")); 968 | part.body = attachment.body; 969 | } 970 | part.set_header("Content-Disposition", "attachment"); 971 | return part; 972 | } 973 | 974 | Part &Part::attach(const string &data, const string &type, const string &filename) { 975 | if (!multipart && body.empty()) { 976 | set_header("Content-Type", type.empty() ? "text/plain" : type); 977 | set_header("Content-Disposition", "attachment"); 978 | if (!filename.empty()) 979 | set_header_parameter("Content-Disposition", "filename", filename); 980 | body = data; 981 | return *this; 982 | } 983 | 984 | make_multipart("mixed"); 985 | auto &part = append_part(); 986 | part.set_header("Content-Type", type.empty() ? "text/plain" : type); 987 | part.set_header("Content-Disposition", "attachment"); 988 | if (!filename.empty()) 989 | part.set_header_parameter("Content-Disposition", "filename", filename); 990 | part.set_body(data); 991 | return part; 992 | } 993 | 994 | Part &Part::attach(istream &in, const string &type, const string &filename) { 995 | auto &part = attach("", type, filename); 996 | char buffer[4096]; 997 | while (in.read(buffer, sizeof(buffer))) 998 | part.body.append(buffer, sizeof(buffer)); 999 | part.body.append(buffer, in.gcount()); 1000 | return part; 1001 | } 1002 | 1003 | vector Part::get_attachments() const { 1004 | vector attachments; 1005 | 1006 | if (!multipart && get_header_value("Content-Disposition") == "attachment") { 1007 | attachments.push_back(this); 1008 | return attachments; 1009 | } 1010 | 1011 | for (auto &part: parts) { 1012 | auto sub = part.get_attachments(); 1013 | attachments.insert(end(attachments), begin(sub), end(sub)); 1014 | } 1015 | 1016 | return attachments; 1017 | } 1018 | 1019 | void Part::simplify() { 1020 | if (!multipart) 1021 | return; 1022 | 1023 | for (auto &part: parts) 1024 | part.simplify(); 1025 | 1026 | parts.erase(remove_if(begin(parts), end(parts), [&](Part &part) { 1027 | return part.headers.empty() && part.body.empty(); 1028 | }), end(parts)); 1029 | 1030 | if (parts.empty()) { 1031 | if (message) { 1032 | erase_header("Content-Type"); 1033 | erase_header("Content-Disposition"); 1034 | multipart = false; 1035 | } else { 1036 | clear(); 1037 | } 1038 | } else if (parts.size() == 1) { 1039 | flatten(); 1040 | } 1041 | } 1042 | 1043 | void Part::clear_attachments() { 1044 | if (!multipart) { 1045 | if (get_header_value("Content-Disposition") == "attachment") { 1046 | if (message) { 1047 | erase_header("Content-Type"); 1048 | erase_header("Content-Disposition"); 1049 | body.clear(); 1050 | } else { 1051 | clear(); 1052 | } 1053 | } 1054 | } else { 1055 | for (auto &part: parts) 1056 | part.clear_attachments(); 1057 | simplify(); 1058 | } 1059 | } 1060 | 1061 | void Part::clear_alternative(const string &type) { 1062 | bool cleared = false; 1063 | Part *part; 1064 | while((part = get_first_matching_part(type))) { 1065 | part->clear(); 1066 | cleared = true; 1067 | } 1068 | 1069 | if (cleared) 1070 | simplify(); 1071 | } 1072 | 1073 | void Part::clear_text() { 1074 | clear_alternative("text"); 1075 | } 1076 | 1077 | void Part::clear_plain() { 1078 | clear_alternative("text/plain"); 1079 | } 1080 | 1081 | void Part::clear_html() { 1082 | clear_alternative("text/html"); 1083 | } 1084 | 1085 | bool Part::has_text() const { 1086 | return get_first_matching_part("text"); 1087 | } 1088 | 1089 | bool Part::has_plain() const { 1090 | return get_first_matching_part("text/plain"); 1091 | } 1092 | 1093 | bool Part::has_html() const { 1094 | return get_first_matching_part("text/html"); 1095 | } 1096 | 1097 | bool Part::has_attachments() const { 1098 | if (is_attachment()) 1099 | return true; 1100 | 1101 | for (auto &part: parts) 1102 | if (part.has_attachments()) 1103 | return true; 1104 | 1105 | return false; 1106 | } 1107 | 1108 | // RFC2822 messages 1109 | 1110 | Message::Message() { 1111 | message = true; 1112 | } 1113 | 1114 | // Comparison 1115 | 1116 | bool operator==(const Part &lhs, const Part &rhs) { 1117 | return lhs.crlf == rhs.crlf 1118 | && lhs.multipart == rhs.multipart 1119 | && lhs.preamble == rhs.preamble 1120 | && lhs.body == rhs.body 1121 | && lhs.epilogue == rhs.epilogue 1122 | && lhs.boundary == rhs.boundary 1123 | && lhs.headers == rhs.headers 1124 | && lhs.parts == rhs.parts; 1125 | } 1126 | 1127 | bool operator!=(const Part &lhs, const Part &rhs) { 1128 | return !(lhs == rhs); 1129 | } 1130 | 1131 | } 1132 | --------------------------------------------------------------------------------