Files
JetCore/__imap.h
T
Brad Arant 41fbf0b7d9 Add pagination to the email list (was hardcoded to newest 50, no way to see older mail)
listMessages() only ever fetched everything then kept the newest
limit (default 50) messages, with no offset parameter and no way for
the client to know how many messages existed beyond what it was
shown - older mail was completely unreachable through the webmail UI.

__imap::listMessages() now takes an offset (counted back from the
newest message) and returns {"total":N,"messages":[...]} instead of
a bare array, so the client can page backward/forward and know when
it has reached either end. __imaplist tag and /email/list both pass
offset through; /email/search is unchanged (search results are not
paginated). email.js tracks currentOffset (reset alongside
currentFolder/currentUid at every existing reset site), and renders
Prev/Next controls next to the unread count showing the current
1-based range and total.
2026-09-07 04:20:42 +00:00

115 lines
5.3 KiB
C++

#ifndef ____imap_h__
# define ____imap_h__
# include "Tag.h"
# include "ZString.h"
# include "MString.h"
# include <cstdint>
# include <openssl/ssl.h>
# include <string>
# include <vector>
namespace jet {
///
/// <imap config="./email.cfg" user="..." password="..." sessionid="">
///
/// Connects to the IMAP host named in the JSON file `config` (keys:
/// host, port, caCertificate - see email.cfg), TLS-handshakes
/// (implicit TLS, matching IMAP's standard port 993 - no STARTTLS
/// negotiation), and LOGINs with user/password. Throws
/// coreutils::Exception (with errorNumber set to a real HTTP status -
/// 401 for a bad login, 502 for a connection/protocol failure) on any
/// failure, exactly like __mysql throws on a failed
/// mysql_real_connect() - see ApiSession.cpp's exception->HTTP-status
/// mapping, which both this tag and __mysql rely on identically.
///
/// Registers itself into Global::imapSessions under sessionid (a
/// SEPARATE map from __mysql's Global::sessions - the two backends
/// aren't interchangeable and Global::sessions is concretely typed
/// __mysql*, not something a second backend could share without
/// widening every existing __mysql-only call site), the same way
/// __mysql registers into Global::sessions - later __imap* -family
/// tags (__imaplist, __imapmessage, __imapflag, __imapdelete) look
/// the live connection up by sessionid, mirroring how <sql> finds its
/// <database> by sessionid.
///
/// Connection pooling: a thread_local pool keyed by sessionid+user,
/// reused across requests on the same APIServer worker thread (an
/// IMAP TCP+TLS+LOGIN round trip is too expensive to redo on every
/// AJAX call the webmail UI makes) - directly mirrors __mysql.cpp's
/// own thread_local connectionPool, including using a cheap
/// keepalive probe (NOOP, IMAP's equivalent of mysql_ping()) to
/// detect and discard a connection the server itself already dropped
/// rather than handing a dead socket to the first real command.
///
class __imap : public Tag {
public:
__imap(coreutils::ZString &in, coreutils::MString &parentOut, Global &global, Tag *parent, Tag *local);
~__imap();
// Each returns a complete, ready-to-emit JSON value (object or
// array) - the __imap*-family wrapper tags (__imaplist.cpp etc.)
// just write whatever this returns straight into their own `out`,
// the same one-call-does-everything shape __sql's query() call
// has relative to <sql>.
/// offset counts back from the newest message (0 = the newest
/// page); the returned JSON is {"total":N,"messages":[...]}.
coreutils::MString listMessages(const std::string &folder, int limit, int offset);
coreutils::MString searchMessages(const std::string &folder, const std::string &query);
coreutils::MString fetchMessage(const std::string &folder, uint32_t uid);
coreutils::MString setFlag(const std::string &folder, uint32_t uid, const std::string &flag, bool add);
coreutils::MString deleteMessage(const std::string &folder, uint32_t uid);
coreutils::MString moveMessage(const std::string &folder, uint32_t uid, const std::string &destFolder);
coreutils::MString folderCounts();
coreutils::MString sessionId;
private:
SSL_CTX *ctx = nullptr;
SSL *ssl = nullptr;
int fd = -1;
int tagCounter = 0;
std::string recvBuffer;
size_t recvPos = 0;
std::string selectedFolder;
std::string nextTag();
void sendLine(const std::string &line);
char readByte();
// Reads one IMAP response line, INCLUDING any literal {n} content
// that line announces (rejoined into the same returned string,
// separated by "\r\n" markers) - see the .cpp for the full
// rationale; this is a pragmatic, purpose-built reader for the
// specific, known response shapes this client's own requests
// provoke from BARANTMail (whose own FETCH implementation - built
// earlier this same session - always uses literals for content,
// never quoted strings), not a fully general IMAP parser.
std::string readResponseLine();
// Reads response lines until the tagged completion line for `tag`
// (built earlier via nextTag()) appears; returns every line
// (untagged and the final tagged one) collected along the way.
// Throws if the tagged line's status isn't OK.
std::vector<std::string> readUntilTagged(const std::string &tag);
bool selectFolderIfNeeded(const std::string &folder);
static std::string jsonEscape(const std::string &s);
static uint32_t findUidField(const std::string &fetchLine);
static std::vector<std::string> findFlagsField(const std::string &fetchLine);
static long findSizeField(const std::string &fetchLine);
// Extracts the literal content immediately following a
// "BODY[...]" (or "BODY[...]<n>") marker in an already-
// literal-rejoined response line - see readResponseLine()'s own
// doc comment for the "\r\n"-separated rejoining this depends on.
static std::string findBodyLiteral(const std::string &fetchLine);
// Shared by listMessages() and searchMessages() - see its own
// doc comment in the .cpp.
static std::vector<std::string> parseSummaryLines(const std::vector<std::string> &lines);
};
}
#endif