/* * qtmaildir - a Qt6 mail client for notmuch-indexed Maildirs * Copyright (C) 2026 Danilo M. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #pragma once #include #include #include #include /// An inline part referenced by a cid: URL from the HTML body. struct InlinePart { QString mimeType; QByteArray data; }; struct Attachment { QString filename; ///< As it appeared in the message. Untrusted. QString mimeType; QByteArray data; /// filename reduced to a basename safe to join onto a directory. /// Attacker-controlled input: a filename may contain path separators or /// "..", so anything that could escape the target directory is stripped. /// Returns a generated name when nothing usable remains. QString safeFilename() const; /// Writes the attachment into directory. Returns the full path written, or /// an empty string on failure with *error set. /// /// **Overwrites an existing file of the same name.** That is right for a /// single save the user just confirmed a location for, and wrong for /// saving a batch: several messages in one thread commonly attach the /// same filename. Use saveWithoutOverwriting() there. QString saveTo(const QString &directory, QString *error) const; /// Writes the attachment into directory under a name that is not already /// taken, appending " (2)", " (3)" and so on before the extension. /// Returns the full path written, or an empty string on failure. /// /// Saving a thread's attachments with saveTo() silently destroyed files: /// six of sixteen were lost to same-name collisions and every write still /// reported success. QString saveWithoutOverwriting(const QString &directory, QString *error) const; /// True if candidatePath (need not exist) is directory itself or strictly /// beneath it, by path-boundary comparison after QDir::cleanPath on both /// sides (so ".." segments are resolved rather than compared textually). /// A bare QString::startsWith() is NOT sufficient here: it would let /// "/tmp/safe-evil" pass against "/tmp/safe" since one string is a /// textual prefix of the other despite being sibling directories. /// /// This is defence-in-depth, not currently load-bearing: saveTo() always /// sanitises the name with safeFilename() first, which reduces it to a /// plain basename, so no path reaching this check via saveTo()'s public /// interface can actually fail it today. It exists for a future change /// that stops sanitising, or that accepts a caller-supplied subpath. /// Exposed as its own function so that guarantee can be tested directly, /// independent of safeFilename() — a test driven purely through saveTo() /// cannot exercise this comparison at all, since safeFilename() always /// runs first and never produces a path that could fail it. static bool isPathInsideDirectory(const QString &directory, const QString &candidatePath); }; /// A directory name for a thread's saved attachments, " ". /// /// `rfc822Date` is a raw Date: header as ParsedMessage stores it; it is /// reduced to "yyyy-MM-dd" when it parses and dropped when it does not. /// /// Both inputs are untrusted: a subject is attacker-controlled and may carry /// path separators, "..", control characters, or nothing usable at all. The /// result is always a single plain component, never a path, and never "." or /// "..". Falls back to the date alone, then to a generated name, so it is /// never empty. /// /// Length is capped: many filesystems limit one component to 255 bytes, and a /// subject can be far longer than that. QString attachmentFolderName(const QString &rfc822Date, const QString &subject); struct ParsedMessage { bool ok = false; QString error; QString subject; QString from; QString to; QString cc; QString date; QString messageId; QString plainBody; QString htmlBody; QHash inlineParts; ///< Keyed by Content-ID, no <>. QList attachments; bool hasHtml() const { return !htmlBody.isEmpty(); } }; /// Parses a single message file using GMime. /// /// Hand-rolling this would mean reimplementing RFC 2047 encoded words, RFC 2231 /// parameter continuations, transfer encodings, and charset conversion, plus /// tolerance for malformed real-world mail. class MimeParser { public: MimeParser(); ParsedMessage parse(const QString &filePath) const; };