Route external identities through provider SMTP
This commit is contained in:
@@ -13,8 +13,8 @@ target_link_libraries(bongocollector
|
||||
bongostreamio
|
||||
bongomsgapi
|
||||
bongocollectoraccounts
|
||||
${CURL_LIBRARIES}
|
||||
${SQLITE_LIBRARIES}
|
||||
CURL::libcurl
|
||||
SQLite3::SQLite3
|
||||
)
|
||||
|
||||
install(TARGETS bongocollector DESTINATION ${SBIN_INSTALL_DIR})
|
||||
@@ -27,6 +27,6 @@ if(BUILD_TESTING)
|
||||
target_include_directories(collector-transport-test PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
)
|
||||
target_link_libraries(collector-transport-test ${CURL_LIBRARIES})
|
||||
target_link_libraries(collector-transport-test CURL::libcurl)
|
||||
add_test(NAME collector-external-transport COMMAND collector-transport-test)
|
||||
endif()
|
||||
|
||||
@@ -73,6 +73,12 @@ static int CollectExternalAccount(sqlite3 *database,
|
||||
CollectorAccountsMarkResult(database, account->id, now, 0, error);
|
||||
goto done;
|
||||
}
|
||||
/* A successful mailbox listing proves the stored credentials even when a
|
||||
later individual message cannot be imported. */
|
||||
if (CollectorAccountsMarkVerified(database, account->id, now) != 0) {
|
||||
snprintf(error, sizeof(error), "cannot verify external sender identity");
|
||||
goto failed;
|
||||
}
|
||||
for (index = 0; index < ids.count && processed < EXTERNAL_MESSAGES_PER_POLL; index++) {
|
||||
FILE *message;
|
||||
size_t message_size;
|
||||
|
||||
@@ -322,6 +322,99 @@ CollectorAccountsMarkResult(sqlite3 *database, sqlite3_int64 id, long now,
|
||||
return result ? 0 : -1;
|
||||
}
|
||||
|
||||
int
|
||||
CollectorAccountsMarkVerified(sqlite3 *database, sqlite3_int64 id, long now)
|
||||
{
|
||||
sqlite3_stmt *statement = NULL;
|
||||
int result = -1;
|
||||
|
||||
if (database == NULL || id <= 0) return -1;
|
||||
if (sqlite3_exec(database, "BEGIN IMMEDIATE", NULL, NULL, NULL) != SQLITE_OK) return -1;
|
||||
if (sqlite3_prepare_v2(database,
|
||||
"UPDATE external_accounts SET verified_at = COALESCE(verified_at, ?), "
|
||||
"updated_at = ? WHERE id = ?", -1, &statement, NULL) != SQLITE_OK) goto done;
|
||||
sqlite3_bind_int64(statement, 1, (sqlite3_int64) now);
|
||||
sqlite3_bind_int64(statement, 2, (sqlite3_int64) now);
|
||||
sqlite3_bind_int64(statement, 3, id);
|
||||
if (sqlite3_step(statement) != SQLITE_DONE || sqlite3_changes(database) != 1) goto done;
|
||||
sqlite3_finalize(statement);
|
||||
statement = NULL;
|
||||
|
||||
/* Successful mailbox authentication proves only the address coupled to
|
||||
this account. Additional sender identities require their own proof. */
|
||||
if (sqlite3_prepare_v2(database,
|
||||
"UPDATE external_account_identities SET verified_at = COALESCE(verified_at, ?) "
|
||||
"WHERE account_id = ? AND email_address = "
|
||||
"(SELECT email_address FROM external_accounts WHERE id = ?)",
|
||||
-1, &statement, NULL) != SQLITE_OK) goto done;
|
||||
sqlite3_bind_int64(statement, 1, (sqlite3_int64) now);
|
||||
sqlite3_bind_int64(statement, 2, id);
|
||||
sqlite3_bind_int64(statement, 3, id);
|
||||
if (sqlite3_step(statement) != SQLITE_DONE || sqlite3_changes(database) != 1) goto done;
|
||||
sqlite3_finalize(statement);
|
||||
statement = NULL;
|
||||
if (sqlite3_exec(database, "COMMIT", NULL, NULL, NULL) != SQLITE_OK) goto done;
|
||||
result = 0;
|
||||
|
||||
done:
|
||||
if (statement != NULL) sqlite3_finalize(statement);
|
||||
if (result != 0) sqlite3_exec(database, "ROLLBACK", NULL, NULL, NULL);
|
||||
return result;
|
||||
}
|
||||
|
||||
int
|
||||
CollectorAccountsOutboundForSender(sqlite3 *database, const char *owner,
|
||||
const char *sender,
|
||||
CollectorOutboundAccount *account)
|
||||
{
|
||||
sqlite3_stmt *statement = NULL;
|
||||
const char query[] =
|
||||
"SELECT a.id, i.email_address, a.outbound_mode, "
|
||||
"COALESCE(a.outbound_host, ''), COALESCE(a.outbound_port, 0), "
|
||||
"a.outbound_tls, COALESCE(a.outbound_username, ''), "
|
||||
"COALESCE(a.outbound_secret_id, '') "
|
||||
"FROM external_account_identities i "
|
||||
"JOIN external_accounts a ON a.id = i.account_id "
|
||||
"WHERE a.owner = ? COLLATE NOCASE "
|
||||
"AND i.email_address = ? COLLATE NOCASE "
|
||||
"AND a.enabled = 1 AND i.verified_at IS NOT NULL "
|
||||
"ORDER BY i.is_default DESC, a.id LIMIT 1";
|
||||
int status;
|
||||
|
||||
if (database == NULL || owner == NULL || *owner == '\0' ||
|
||||
sender == NULL || *sender == '\0' || account == NULL ||
|
||||
sqlite3_prepare_v2(database, query, -1, &statement, NULL) != SQLITE_OK) {
|
||||
return -1;
|
||||
}
|
||||
memset(account, 0, sizeof(*account));
|
||||
sqlite3_bind_text(statement, 1, owner, -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(statement, 2, sender, -1, SQLITE_TRANSIENT);
|
||||
status = sqlite3_step(statement);
|
||||
if (status == SQLITE_DONE) {
|
||||
sqlite3_finalize(statement);
|
||||
return 0;
|
||||
}
|
||||
if (status != SQLITE_ROW) {
|
||||
sqlite3_finalize(statement);
|
||||
return -1;
|
||||
}
|
||||
account->account_id = sqlite3_column_int64(statement, 0);
|
||||
#define COPY_OUTBOUND_FIELD(column, field) \
|
||||
snprintf(account->field, sizeof(account->field), "%s", \
|
||||
sqlite3_column_text(statement, (column)) != NULL ? \
|
||||
(const char *) sqlite3_column_text(statement, (column)) : "")
|
||||
COPY_OUTBOUND_FIELD(1, email_address);
|
||||
COPY_OUTBOUND_FIELD(2, mode);
|
||||
COPY_OUTBOUND_FIELD(3, host);
|
||||
account->port = sqlite3_column_int(statement, 4);
|
||||
COPY_OUTBOUND_FIELD(5, tls);
|
||||
COPY_OUTBOUND_FIELD(6, username);
|
||||
COPY_OUTBOUND_FIELD(7, secret_id);
|
||||
#undef COPY_OUTBOUND_FIELD
|
||||
sqlite3_finalize(statement);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int
|
||||
CollectorAccountsSecretSet(sqlite3 *database, const char *id,
|
||||
const char *owner, const char *secret, long now)
|
||||
|
||||
@@ -21,6 +21,17 @@ typedef struct {
|
||||
int delete_after_days;
|
||||
} CollectorExternalAccount;
|
||||
|
||||
typedef struct {
|
||||
sqlite3_int64 account_id;
|
||||
char email_address[320];
|
||||
char mode[16];
|
||||
char host[256];
|
||||
int port;
|
||||
char tls[16];
|
||||
char username[256];
|
||||
char secret_id[128];
|
||||
} CollectorOutboundAccount;
|
||||
|
||||
sqlite3 *CollectorAccountsOpen(void);
|
||||
void CollectorAccountsClose(sqlite3 *database);
|
||||
int CollectorAccountsDueCount(sqlite3 *database, long now);
|
||||
@@ -28,6 +39,11 @@ int CollectorAccountsNextDue(sqlite3 *database, long now,
|
||||
CollectorExternalAccount *account);
|
||||
int CollectorAccountsMarkResult(sqlite3 *database, sqlite3_int64 id,
|
||||
long now, int success, const char *error);
|
||||
int CollectorAccountsMarkVerified(sqlite3 *database, sqlite3_int64 id,
|
||||
long now);
|
||||
int CollectorAccountsOutboundForSender(sqlite3 *database, const char *owner,
|
||||
const char *sender,
|
||||
CollectorOutboundAccount *account);
|
||||
int CollectorAccountsSecretSet(sqlite3 *database, const char *id,
|
||||
const char *owner, const char *secret, long now);
|
||||
int CollectorAccountsSecretGet(sqlite3 *database, const char *id,
|
||||
|
||||
@@ -11,6 +11,7 @@ target_link_libraries(bongosmtp
|
||||
bongojson
|
||||
bongomsgapi
|
||||
bongomailauth
|
||||
bongocollectoraccounts
|
||||
)
|
||||
|
||||
add_executable(bongosmtpc
|
||||
@@ -24,6 +25,8 @@ target_link_libraries(bongosmtpc
|
||||
bongojson
|
||||
bongomsgapi
|
||||
bongomailauth
|
||||
bongocollectoraccounts
|
||||
CURL::libcurl
|
||||
)
|
||||
|
||||
install(TARGETS bongosmtp DESTINATION ${SBIN_INSTALL_DIR})
|
||||
|
||||
+195
-21
@@ -23,6 +23,8 @@
|
||||
|
||||
#include <config.h>
|
||||
|
||||
#include <curl/curl.h>
|
||||
|
||||
#include <xpl.h>
|
||||
#include <memmgr.h>
|
||||
#include <logger.h>
|
||||
@@ -125,7 +127,8 @@ GenerateDKIMSignature(SMTPClient *queue)
|
||||
BongoMailAuthStatus status;
|
||||
|
||||
queue->hasDKIMSignature = FALSE;
|
||||
if (!MailAuth.dkim_sign_outgoing) {
|
||||
/* External submission providers apply their own aligned DKIM signature. */
|
||||
if (!MailAuth.dkim_sign_outgoing || queue->hasExternalRelay) {
|
||||
return BONGO_MAILAUTH_OK;
|
||||
}
|
||||
/* Do not add a new author signature to unauthenticated forwarded mail. */
|
||||
@@ -442,6 +445,172 @@ ConnectLMTP(RecipStruct *recipient, const char *host, int targetPort)
|
||||
return connection;
|
||||
}
|
||||
|
||||
static int
|
||||
ResolveExternalRelay(SMTPClient *queue)
|
||||
{
|
||||
sqlite3 *database;
|
||||
int found;
|
||||
|
||||
memset(&queue->externalRelay, 0, sizeof(queue->externalRelay));
|
||||
queue->hasExternalRelay = FALSE;
|
||||
if (queue->authSender == NULL || queue->authSender[0] == '-' ||
|
||||
queue->sender == NULL || queue->sender[0] == '-' ||
|
||||
queue->sender[0] == '\0') return 0;
|
||||
database = CollectorAccountsOpen();
|
||||
if (database == NULL) return -1;
|
||||
found = CollectorAccountsOutboundForSender(
|
||||
database, queue->authSender, queue->sender, &queue->externalRelay);
|
||||
CollectorAccountsClose(database);
|
||||
if (found < 0) return -1;
|
||||
queue->hasExternalRelay = found == 1 &&
|
||||
strcmp(queue->externalRelay.mode, "smtp") == 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void
|
||||
EraseSecret(char *secret, size_t length)
|
||||
{
|
||||
volatile unsigned char *cursor = (volatile unsigned char *) secret;
|
||||
while (length-- > 0) *cursor++ = 0;
|
||||
}
|
||||
|
||||
static int
|
||||
QueueMessageToFile(SMTPClient *queue, FILE *message, curl_off_t *messageSize)
|
||||
{
|
||||
char timeBuffer[80];
|
||||
char *line = NULL;
|
||||
unsigned long lineSize = 0;
|
||||
long long remaining;
|
||||
int result = -1;
|
||||
|
||||
MsgGetRFC822Date(-1, 0, timeBuffer);
|
||||
if (fprintf(message,
|
||||
"Received: from %s (%d.%d.%d.%d) by %s\r\n"
|
||||
"\twith NMAP (bongosmtpc Agent); %s\r\n",
|
||||
BongoGlobals.hostname,
|
||||
queue->conn->socketAddress.sin_addr.s_net,
|
||||
queue->conn->socketAddress.sin_addr.s_host,
|
||||
queue->conn->socketAddress.sin_addr.s_lh,
|
||||
queue->conn->socketAddress.sin_addr.s_impno,
|
||||
BongoGlobals.hostname, timeBuffer) < 0) goto done;
|
||||
if (queue->hasDKIMSignature &&
|
||||
fprintf(message, "DKIM-Signature: %s\r\n", queue->dkimSignature) < 0) goto done;
|
||||
|
||||
ConnWriteF(queue->conn, "QRETR %s MESSAGE\r\n", queue->qID);
|
||||
ConnFlush(queue->conn);
|
||||
ConnReadAnswer(queue->conn, queue->line, CONN_BUFSIZE);
|
||||
if (atoi(queue->line) != 2023) goto done;
|
||||
remaining = atoll(&queue->line[5]);
|
||||
while (remaining > 0) {
|
||||
size_t length;
|
||||
size_t consumed;
|
||||
ConnReadToAllocatedBuffer(queue->conn, &line, &lineSize);
|
||||
if (line == NULL) goto done;
|
||||
length = strlen(line);
|
||||
if (fwrite(line, 1, length, message) != length ||
|
||||
fwrite("\r\n", 1, 2, message) != 2) goto done;
|
||||
consumed = length + 2U;
|
||||
remaining -= remaining > (long long) consumed
|
||||
? (long long) consumed : remaining;
|
||||
}
|
||||
ConnReadAnswer(queue->conn, queue->line, CONN_BUFSIZE);
|
||||
if (atoi(queue->line) != 1000 || fflush(message) != 0) goto done;
|
||||
{
|
||||
off_t end = ftello(message);
|
||||
if (end < 0 || fseeko(message, 0, SEEK_SET) != 0) goto done;
|
||||
*messageSize = (curl_off_t) end;
|
||||
}
|
||||
result = 0;
|
||||
|
||||
done:
|
||||
if (line != NULL) MemFree(line);
|
||||
return result;
|
||||
}
|
||||
|
||||
static BOOL
|
||||
DeliverExternalRelay(SMTPClient *queue, RecipStruct *recipient)
|
||||
{
|
||||
sqlite3 *database = NULL;
|
||||
CURL *curl = NULL;
|
||||
struct curl_slist *recipients = NULL;
|
||||
FILE *message = NULL;
|
||||
char secret[4097] = {0};
|
||||
char url[560];
|
||||
char curlError[CURL_ERROR_SIZE] = {0};
|
||||
char rewrittenSender[1024];
|
||||
const char *envelopeSender = NULL;
|
||||
curl_off_t messageSize = 0;
|
||||
CURLcode code = CURLE_FAILED_INIT;
|
||||
long response = 0;
|
||||
BOOL success = FALSE;
|
||||
|
||||
recipient->Result = DELIVER_TRY_LATER;
|
||||
if (!queue->hasExternalRelay || queue->externalRelay.host[0] == '\0' ||
|
||||
queue->externalRelay.port < 1 || queue->externalRelay.port > 65535 ||
|
||||
queue->externalRelay.username[0] == '\0' ||
|
||||
queue->externalRelay.secret_id[0] == '\0' ||
|
||||
(strcmp(queue->externalRelay.tls, "implicit") != 0 &&
|
||||
strcmp(queue->externalRelay.tls, "starttls") != 0) ||
|
||||
strpbrk(queue->externalRelay.host, "/?#@\r\n") != NULL ||
|
||||
GetEnvelopeSender(queue, rewrittenSender, sizeof(rewrittenSender),
|
||||
&envelopeSender) != BONGO_MAILAUTH_OK) goto done;
|
||||
if (snprintf(url, sizeof(url), "%s://%s:%d",
|
||||
strcmp(queue->externalRelay.tls, "implicit") == 0
|
||||
? "smtps" : "smtp",
|
||||
queue->externalRelay.host,
|
||||
queue->externalRelay.port) >= (int) sizeof(url)) goto done;
|
||||
|
||||
database = CollectorAccountsOpen();
|
||||
if (database == NULL || CollectorAccountsSecretGet(
|
||||
database, queue->externalRelay.secret_id,
|
||||
secret, sizeof(secret)) != 1) goto done;
|
||||
CollectorAccountsClose(database);
|
||||
database = NULL;
|
||||
message = tmpfile();
|
||||
if (message == NULL || QueueMessageToFile(queue, message, &messageSize) != 0) goto done;
|
||||
curl = curl_easy_init();
|
||||
recipients = curl_slist_append(recipients, recipient->To);
|
||||
if (curl == NULL || recipients == NULL) goto done;
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url);
|
||||
curl_easy_setopt(curl, CURLOPT_USERNAME, queue->externalRelay.username);
|
||||
curl_easy_setopt(curl, CURLOPT_PASSWORD, secret);
|
||||
curl_easy_setopt(curl, CURLOPT_MAIL_FROM, envelopeSender);
|
||||
curl_easy_setopt(curl, CURLOPT_MAIL_RCPT, recipients);
|
||||
curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_READDATA, message);
|
||||
curl_easy_setopt(curl, CURLOPT_INFILESIZE_LARGE, messageSize);
|
||||
curl_easy_setopt(curl, CURLOPT_USE_SSL, CURLUSESSL_ALL);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
|
||||
curl_easy_setopt(curl, CURLOPT_NETRC, CURL_NETRC_IGNORED);
|
||||
curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 30L);
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 300L);
|
||||
curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, curlError);
|
||||
code = curl_easy_perform(curl);
|
||||
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response);
|
||||
if (code == CURLE_OK && response >= 200 && response < 300) {
|
||||
recipient->Result = DELIVER_SUCCESS;
|
||||
success = TRUE;
|
||||
} else if (response >= 500 && code != CURLE_LOGIN_DENIED) {
|
||||
recipient->Result = DELIVER_FAILURE;
|
||||
}
|
||||
Log(success ? LOG_INFO : LOG_WARN,
|
||||
"External SMTP relay %s returned %ld (%s) for message %s to %s",
|
||||
queue->externalRelay.host, response,
|
||||
code == CURLE_OK ? "SMTP response" :
|
||||
(curlError[0] != '\0' ? curlError : curl_easy_strerror(code)),
|
||||
queue->qID, recipient->To);
|
||||
|
||||
done:
|
||||
if (database != NULL) CollectorAccountsClose(database);
|
||||
if (curl != NULL) curl_easy_cleanup(curl);
|
||||
if (recipients != NULL) curl_slist_free_all(recipients);
|
||||
if (message != NULL) fclose(message);
|
||||
EraseSecret(secret, sizeof(secret));
|
||||
return success;
|
||||
}
|
||||
|
||||
BOOL
|
||||
DeliverMessage(SMTPClient *Queue, SMTPClient *Remote, RecipStruct *Recip) {
|
||||
int Extensions;
|
||||
@@ -878,6 +1047,12 @@ ProcessEntry(void *clientp, Connection *conn)
|
||||
}
|
||||
|
||||
/* now that we've got all recipients and locations, let's sort them to use connections more efficiently */
|
||||
if (ResolveExternalRelay(Queue) != 0) {
|
||||
Log(LOG_ERROR, "Cannot resolve external sender identity for queue %s",
|
||||
Queue->qID);
|
||||
g_array_free(Recipients, TRUE);
|
||||
return -1;
|
||||
}
|
||||
{
|
||||
BongoMailAuthStatus dkimStatus = GenerateDKIMSignature(Queue);
|
||||
if (dkimStatus != BONGO_MAILAUTH_OK) {
|
||||
@@ -912,27 +1087,26 @@ ProcessEntry(void *clientp, Connection *conn)
|
||||
|
||||
/* by the time i get here, CurrentRecip should be the last recip
|
||||
* of any string of duplicates */
|
||||
Remote.isLMTP = FindLMTPTransport(
|
||||
&CurrentRecip, Remote.transportHost,
|
||||
sizeof(Remote.transportHost), &Remote.transportPort);
|
||||
Remote.conn = Remote.isLMTP
|
||||
? ConnectLMTP(&CurrentRecip, Remote.transportHost,
|
||||
Remote.transportPort)
|
||||
: LookupRemoteMX(&CurrentRecip);
|
||||
if (!Remote.conn) {
|
||||
/* there was an error looking up or connecting to the remote server
|
||||
* if i get an error in this stage of the process i need to just skip
|
||||
* the rest of the email addresses on this domain */
|
||||
/* FIXME: */
|
||||
continue;
|
||||
}
|
||||
if (Queue->hasExternalRelay) {
|
||||
DeliverExternalRelay(Queue, &CurrentRecip);
|
||||
} else {
|
||||
Remote.isLMTP = FindLMTPTransport(
|
||||
&CurrentRecip, Remote.transportHost,
|
||||
sizeof(Remote.transportHost), &Remote.transportPort);
|
||||
Remote.conn = Remote.isLMTP
|
||||
? ConnectLMTP(&CurrentRecip, Remote.transportHost,
|
||||
Remote.transportPort)
|
||||
: LookupRemoteMX(&CurrentRecip);
|
||||
if (!Remote.conn) {
|
||||
/* There was an error looking up or connecting to the remote server. */
|
||||
continue;
|
||||
}
|
||||
|
||||
DeliverMessage(Queue, &Remote, &CurrentRecip);
|
||||
|
||||
// we're responsible for closing this connection
|
||||
ConnClose(Remote.conn);
|
||||
ConnFree(Remote.conn);
|
||||
Remote.conn = NULL;
|
||||
DeliverMessage(Queue, &Remote, &CurrentRecip);
|
||||
ConnClose(Remote.conn);
|
||||
ConnFree(Remote.conn);
|
||||
Remote.conn = NULL;
|
||||
}
|
||||
|
||||
/* now handle delivery result codes */
|
||||
switch(CurrentRecip.Result) {
|
||||
|
||||
@@ -30,6 +30,8 @@
|
||||
#include <bongoagent.h>
|
||||
#include <xpldns.h>
|
||||
|
||||
#include "external_accounts.h"
|
||||
|
||||
#include "smtp.h"
|
||||
|
||||
#define AGENT_NAME "smtpd_c"
|
||||
@@ -48,6 +50,8 @@ typedef struct {
|
||||
char qID[16]; /* holds the queueid pulled during handshake */
|
||||
BOOL hasDKIMSignature;
|
||||
char dkimSignature[8192];
|
||||
BOOL hasExternalRelay;
|
||||
CollectorOutboundAccount externalRelay;
|
||||
BOOL isLMTP;
|
||||
char transportHost[256];
|
||||
int transportPort;
|
||||
|
||||
+16
-4
@@ -45,6 +45,7 @@
|
||||
#include <bongoutil.h>
|
||||
#include "mailauth.h"
|
||||
#include "smtpd.h"
|
||||
#include "external_accounts.h"
|
||||
|
||||
struct {
|
||||
int port;
|
||||
@@ -516,13 +517,24 @@ SubmissionSenderAllowed(const char *user, const char *sender)
|
||||
{
|
||||
const char *at;
|
||||
size_t localLength;
|
||||
sqlite3 *database;
|
||||
CollectorOutboundAccount account;
|
||||
int found;
|
||||
if (SMTP.submission_allow_sender_override || user == NULL || sender == NULL ||
|
||||
*sender == '\0') return TRUE;
|
||||
if (strcasecmp(user, sender) == 0) return TRUE;
|
||||
if (strchr(user, '@') != NULL) return FALSE;
|
||||
at = strrchr(sender, '@');
|
||||
localLength = at != NULL ? (size_t) (at - sender) : strlen(sender);
|
||||
return strlen(user) == localLength && strncasecmp(user, sender, localLength) == 0;
|
||||
if (strchr(user, '@') == NULL) {
|
||||
at = strrchr(sender, '@');
|
||||
localLength = at != NULL ? (size_t) (at - sender) : strlen(sender);
|
||||
if (strlen(user) == localLength &&
|
||||
strncasecmp(user, sender, localLength) == 0) return TRUE;
|
||||
}
|
||||
|
||||
database = CollectorAccountsOpen();
|
||||
if (database == NULL) return FALSE;
|
||||
found = CollectorAccountsOutboundForSender(database, user, sender, &account);
|
||||
CollectorAccountsClose(database);
|
||||
return found == 1;
|
||||
}
|
||||
|
||||
static BOOL
|
||||
|
||||
@@ -39,11 +39,11 @@ target_link_libraries(bongostore
|
||||
bongoutil
|
||||
bongojson
|
||||
bongomsgapi
|
||||
${SQLITE_LIBRARIES}
|
||||
${GLIB2_LIBRARIES}
|
||||
SQLite3::SQLite3
|
||||
GLib2::GLib2
|
||||
${GCRYPT_LIBRARIES}
|
||||
${ICAL_LIBRARIES}
|
||||
${GMIME2_LIBRARIES}
|
||||
GMime::GMime
|
||||
)
|
||||
|
||||
install(TARGETS bongostore DESTINATION ${SBIN_INSTALL_DIR})
|
||||
|
||||
@@ -15,7 +15,6 @@ add_library(bongocal SHARED
|
||||
#target_link_libraries(bongo-import-tz
|
||||
# bongomemmgr
|
||||
# bongocal
|
||||
# ${GLIB2_LIBRARIES}
|
||||
# ${ICAL_LIBRARIES}
|
||||
# )
|
||||
|
||||
|
||||
@@ -23,7 +23,18 @@ target_include_directories(bongocollectoraccounts PUBLIC
|
||||
target_link_libraries(bongocollectoraccounts
|
||||
bongoxpl
|
||||
${GCRYPT_LIBRARIES}
|
||||
${SQLITE_LIBRARIES}
|
||||
SQLite3::SQLite3
|
||||
)
|
||||
|
||||
install(TARGETS bongocollectoraccounts DESTINATION ${LIB_INSTALL_DIR})
|
||||
|
||||
if(BUILD_TESTING)
|
||||
add_executable(collector-accounts-test
|
||||
tests/external-accounts-test.c
|
||||
)
|
||||
target_link_libraries(collector-accounts-test
|
||||
bongocollectoraccounts
|
||||
SQLite3::SQLite3
|
||||
)
|
||||
add_test(NAME collector-external-accounts COMMAND collector-accounts-test)
|
||||
endif()
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
#include <sqlite3.h>
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "external_accounts.h"
|
||||
|
||||
static int
|
||||
Check(int condition, const char *message)
|
||||
{
|
||||
if (condition) return 0;
|
||||
fprintf(stderr, "%s\n", message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int
|
||||
main(void)
|
||||
{
|
||||
sqlite3 *database = NULL;
|
||||
CollectorOutboundAccount account;
|
||||
sqlite3_stmt *statement = NULL;
|
||||
int primaryVerified = 0;
|
||||
int aliasVerified = 1;
|
||||
int failed = 0;
|
||||
const char schema[] =
|
||||
"CREATE TABLE external_accounts ("
|
||||
"id INTEGER PRIMARY KEY, owner TEXT, email_address TEXT, enabled INTEGER, "
|
||||
"verified_at INTEGER, updated_at INTEGER, outbound_mode TEXT, outbound_host TEXT, "
|
||||
"outbound_port INTEGER, outbound_tls TEXT, outbound_username TEXT, outbound_secret_id TEXT);"
|
||||
"CREATE TABLE external_account_identities ("
|
||||
"account_id INTEGER, email_address TEXT, verified_at INTEGER, is_default INTEGER);"
|
||||
"INSERT INTO external_accounts VALUES "
|
||||
"(1, 'alice', 'alice@example.test', 1, NULL, 1, 'smtp', "
|
||||
"'smtp.example.test', 587, 'starttls', 'alice@example.test', 'secret-1');"
|
||||
"INSERT INTO external_account_identities VALUES "
|
||||
"(1, 'alice@example.test', NULL, 1), (1, 'alias@example.test', NULL, 0);";
|
||||
|
||||
failed |= Check(sqlite3_open(":memory:", &database) == SQLITE_OK,
|
||||
"cannot open in-memory database");
|
||||
failed |= Check(!failed && sqlite3_exec(database, schema, NULL, NULL, NULL) == SQLITE_OK,
|
||||
"cannot initialize identity test schema");
|
||||
failed |= Check(!failed && CollectorAccountsOutboundForSender(
|
||||
database, "alice", "alice@example.test", &account) == 0,
|
||||
"unverified identity was accepted");
|
||||
failed |= Check(!failed && CollectorAccountsMarkVerified(database, 1, 42) == 0,
|
||||
"cannot verify collected account identity");
|
||||
failed |= Check(!failed && sqlite3_prepare_v2(database,
|
||||
"SELECT verified_at IS NOT NULL FROM external_account_identities "
|
||||
"WHERE account_id=1 ORDER BY is_default DESC", -1, &statement, NULL) == SQLITE_OK,
|
||||
"cannot query verification state");
|
||||
if (!failed && sqlite3_step(statement) == SQLITE_ROW) {
|
||||
primaryVerified = sqlite3_column_int(statement, 0);
|
||||
}
|
||||
if (!failed && sqlite3_step(statement) == SQLITE_ROW) {
|
||||
aliasVerified = sqlite3_column_int(statement, 0);
|
||||
}
|
||||
sqlite3_finalize(statement);
|
||||
statement = NULL;
|
||||
failed |= Check(primaryVerified == 1, "primary identity was not verified");
|
||||
failed |= Check(aliasVerified == 0, "additional identity was verified implicitly");
|
||||
failed |= Check(!failed && CollectorAccountsOutboundForSender(
|
||||
database, "ALICE", "ALICE@EXAMPLE.TEST", &account) == 1,
|
||||
"verified identity was not resolved case-insensitively");
|
||||
failed |= Check(!failed && account.account_id == 1 && account.port == 587 &&
|
||||
strcmp(account.host, "smtp.example.test") == 0,
|
||||
"resolved SMTP account is incorrect");
|
||||
failed |= Check(!failed && CollectorAccountsOutboundForSender(
|
||||
database, "alice", "alias@example.test", &account) == 0,
|
||||
"unverified additional identity was accepted");
|
||||
failed |= Check(!failed && CollectorAccountsOutboundForSender(
|
||||
database, "mallory", "alice@example.test", &account) == 0,
|
||||
"another user's identity was accepted");
|
||||
|
||||
sqlite3_close(database);
|
||||
return failed ? 1 : 0;
|
||||
}
|
||||
@@ -10,7 +10,7 @@ add_library(bongoconnio SHARED
|
||||
)
|
||||
|
||||
target_link_libraries(bongoconnio
|
||||
${GNUTLS_LIBRARIES}
|
||||
GnuTLS::GnuTLS
|
||||
)
|
||||
|
||||
install(TARGETS bongoconnio DESTINATION ${LIB_INSTALL_DIR})
|
||||
|
||||
@@ -9,7 +9,6 @@ add_library(bongomailauth
|
||||
target_include_directories(bongomailauth
|
||||
PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${PSL_INCLUDE_DIRS}
|
||||
)
|
||||
|
||||
target_link_libraries(bongomailauth
|
||||
@@ -18,7 +17,7 @@ target_link_libraries(bongomailauth
|
||||
OpenDKIM::OpenDKIM
|
||||
OpenDMARC::OpenDMARC
|
||||
SRS2::SRS2
|
||||
${PSL_LIBRARIES}
|
||||
LibPSL::LibPSL
|
||||
)
|
||||
|
||||
install(TARGETS bongomailauth DESTINATION ${LIB_INSTALL_DIR})
|
||||
|
||||
@@ -14,9 +14,9 @@ target_link_libraries(bongomsgapi
|
||||
bongoxpl
|
||||
bongocal
|
||||
bongojson
|
||||
${SQLITE_LIBRARIES}
|
||||
SQLite3::SQLite3
|
||||
${LIBICAL_LIBRARIES}
|
||||
${CURL_LIBRARIES}
|
||||
CURL::libcurl
|
||||
)
|
||||
|
||||
install(TARGETS bongomsgapi DESTINATION ${LIB_INSTALL_DIR})
|
||||
|
||||
@@ -62,7 +62,7 @@ target_link_libraries(_calendar
|
||||
target_link_libraries(_external_accounts
|
||||
bongocollectoraccounts
|
||||
bongoxpl
|
||||
${GLIB2_LIBRARIES}
|
||||
GLib2::GLib2
|
||||
${GCRYPT_LIBRARIES}
|
||||
${PYTHON_LIBRARIES}
|
||||
)
|
||||
|
||||
@@ -95,37 +95,48 @@ AccountCreate(PyObject *self, PyObject *args)
|
||||
{
|
||||
const char *owner, *secret, *label, *email, *protocol, *host, *tls;
|
||||
const char *username, *mailbox, *folder, *delete_policy;
|
||||
const char *outbound_mode, *outbound_host, *outbound_tls, *outbound_username;
|
||||
PyObject *settings;
|
||||
sqlite3 *database = NULL;
|
||||
sqlite3_stmt *statement = NULL;
|
||||
char secret_id[33];
|
||||
long port, poll, delete_days;
|
||||
long port, poll, delete_days, outbound_port;
|
||||
long now = (long) time(NULL);
|
||||
sqlite3_int64 account_id;
|
||||
int ok = 0;
|
||||
const char insert[] = "INSERT INTO external_accounts "
|
||||
"(owner, label, email_address, inbound_protocol, inbound_host, inbound_port, "
|
||||
"inbound_tls, inbound_username, inbound_secret_id, inbound_mailbox, poll_interval, "
|
||||
"destination_folder, delete_policy, delete_after_days, outbound_mode, created_at, updated_at) "
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'direct', ?, ?)";
|
||||
"destination_folder, delete_policy, delete_after_days, outbound_mode, outbound_host, "
|
||||
"outbound_port, outbound_tls, outbound_username, outbound_secret_id, created_at, updated_at) "
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
|
||||
(void) self;
|
||||
if (!PyArg_ParseTuple(args, "sO!s", &owner, &PyDict_Type, &settings, &secret)) return NULL;
|
||||
port = GetLong(settings, "inbound_port");
|
||||
poll = GetLong(settings, "poll_interval");
|
||||
delete_days = GetLong(settings, "delete_after_days");
|
||||
outbound_port = GetLong(settings, "outbound_port");
|
||||
if (!ValidText(owner, 255) || !ValidText(secret, 4096) ||
|
||||
GetText(settings, "label", 255, &label) || GetText(settings, "email_address", 319, &email) ||
|
||||
GetText(settings, "inbound_protocol", 7, &protocol) || GetText(settings, "inbound_host", 255, &host) ||
|
||||
GetText(settings, "inbound_tls", 15, &tls) || GetText(settings, "inbound_username", 255, &username) ||
|
||||
GetText(settings, "inbound_mailbox", 255, &mailbox) || GetText(settings, "destination_folder", 255, &folder) ||
|
||||
GetText(settings, "delete_policy", 15, &delete_policy) ||
|
||||
GetText(settings, "outbound_mode", 15, &outbound_mode) ||
|
||||
GetText(settings, "outbound_host", 255, &outbound_host) ||
|
||||
GetText(settings, "outbound_tls", 15, &outbound_tls) ||
|
||||
GetText(settings, "outbound_username", 255, &outbound_username) ||
|
||||
(strcmp(protocol, "imap") && strcmp(protocol, "pop3")) ||
|
||||
(strcmp(tls, "implicit") && strcmp(tls, "starttls")) ||
|
||||
(strcmp(delete_policy, "never") && strcmp(delete_policy, "after_import") && strcmp(delete_policy, "after_days")) ||
|
||||
port < 1 || port > 65535 || poll < 60 || poll > 86400 ||
|
||||
delete_days < 0 || delete_days > 3650 ||
|
||||
(!strcmp(delete_policy, "after_days") && delete_days == 0) ||
|
||||
strpbrk(host, "/?#@") != NULL || strpbrk(mailbox, "\"\\") != NULL || strpbrk(folder, "\"\\") != NULL) {
|
||||
(strcmp(outbound_mode, "direct") && strcmp(outbound_mode, "smtp")) ||
|
||||
(strcmp(outbound_tls, "implicit") && strcmp(outbound_tls, "starttls")) ||
|
||||
outbound_port < 1 || outbound_port > 65535 ||
|
||||
strpbrk(host, "/?#@") != NULL || strpbrk(outbound_host, "/?#@") != NULL ||
|
||||
strpbrk(mailbox, "\"\\") != NULL || strpbrk(folder, "\"\\") != NULL) {
|
||||
return PyErr_Format(PyExc_ValueError, "invalid external account settings");
|
||||
}
|
||||
database = CollectorAccountsOpen();
|
||||
@@ -150,11 +161,31 @@ AccountCreate(PyObject *self, PyObject *args)
|
||||
sqlite3_bind_text(statement, 12, folder, -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(statement, 13, delete_policy, -1, SQLITE_TRANSIENT);
|
||||
if (delete_days > 0) sqlite3_bind_int(statement, 14, (int) delete_days); else sqlite3_bind_null(statement, 14);
|
||||
sqlite3_bind_int64(statement, 15, now);
|
||||
sqlite3_bind_int64(statement, 16, now);
|
||||
sqlite3_bind_text(statement, 15, outbound_mode, -1, SQLITE_TRANSIENT);
|
||||
if (!strcmp(outbound_mode, "smtp")) {
|
||||
sqlite3_bind_text(statement, 16, outbound_host, -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_int(statement, 17, (int) outbound_port);
|
||||
sqlite3_bind_text(statement, 19, outbound_username, -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(statement, 20, secret_id, -1, SQLITE_TRANSIENT);
|
||||
} else {
|
||||
sqlite3_bind_null(statement, 16);
|
||||
sqlite3_bind_null(statement, 17);
|
||||
sqlite3_bind_null(statement, 19);
|
||||
sqlite3_bind_null(statement, 20);
|
||||
}
|
||||
sqlite3_bind_text(statement, 18, outbound_tls, -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_int64(statement, 21, now);
|
||||
sqlite3_bind_int64(statement, 22, now);
|
||||
if (sqlite3_step(statement) != SQLITE_DONE) goto done;
|
||||
account_id = sqlite3_last_insert_rowid(database);
|
||||
sqlite3_finalize(statement); statement = NULL;
|
||||
if (sqlite3_prepare_v2(database,
|
||||
"INSERT INTO external_account_identities (account_id, email_address, verified_at, is_default) "
|
||||
"VALUES (?, ?, NULL, 1)", -1, &statement, NULL) != SQLITE_OK) goto done;
|
||||
sqlite3_bind_int64(statement, 1, account_id);
|
||||
sqlite3_bind_text(statement, 2, email, -1, SQLITE_TRANSIENT);
|
||||
if (sqlite3_step(statement) != SQLITE_DONE) goto done;
|
||||
sqlite3_finalize(statement); statement = NULL;
|
||||
if (sqlite3_exec(database, "COMMIT", NULL, NULL, NULL) != SQLITE_OK) goto done;
|
||||
ok = 1;
|
||||
done:
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
add_library(bongosasl sasl.c)
|
||||
target_include_directories(bongosasl PRIVATE ${SASL2_INCLUDE_DIRS})
|
||||
target_link_libraries(bongosasl PRIVATE ${SASL2_LIBRARIES})
|
||||
target_link_libraries(bongosasl PRIVATE CyrusSASL::CyrusSASL)
|
||||
install(TARGETS bongosasl DESTINATION ${LIB_INSTALL_DIR})
|
||||
|
||||
if(BUILD_TESTING)
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
add_library(libbongosieve sieve.c store.c managesieve.c)
|
||||
set_target_properties(libbongosieve PROPERTIES OUTPUT_NAME bongosieve)
|
||||
target_include_directories(libbongosieve PRIVATE ${MAILUTILS_INCLUDE_DIR})
|
||||
target_include_directories(libbongosieve PRIVATE ${SQLITE_INCLUDE_DIRS})
|
||||
target_link_libraries(libbongosieve PRIVATE ${MAILUTILS_LIBRARIES} ${SQLITE_LIBRARIES})
|
||||
target_link_libraries(libbongosieve PRIVATE ${MAILUTILS_LIBRARIES} SQLite3::SQLite3)
|
||||
|
||||
install(TARGETS libbongosieve DESTINATION ${LIB_INSTALL_DIR})
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ add_library(bongoutil SHARED
|
||||
bongoutil.c)
|
||||
|
||||
target_link_libraries(bongoutil
|
||||
${GLIB2_LIBRARIES}
|
||||
GLib2::GLib2
|
||||
bongoconnio
|
||||
bongostreamio
|
||||
)
|
||||
|
||||
@@ -16,6 +16,7 @@ target_link_libraries(bongoxpl
|
||||
resolv
|
||||
pthread
|
||||
${GCRYPT_LIBRARIES}
|
||||
GLib2::GLib2
|
||||
)
|
||||
|
||||
install(TARGETS bongoxpl DESTINATION ${LIB_INSTALL_DIR})
|
||||
|
||||
@@ -40,6 +40,7 @@ def create_account(owner: str, payload: dict, presets: dict) -> int:
|
||||
raise ValueError("invalid external account fields")
|
||||
delete_policy = str(payload.get("delete_policy", "never"))
|
||||
delete_days = int(payload.get("delete_after_days", 0))
|
||||
smtp = provider.get("smtp")
|
||||
settings = {
|
||||
"label": str(payload.get("label", provider["name"])).strip(),
|
||||
"email_address": email,
|
||||
@@ -53,6 +54,11 @@ def create_account(owner: str, payload: dict, presets: dict) -> int:
|
||||
"poll_interval": int(payload.get("poll_interval", 300)),
|
||||
"delete_policy": delete_policy,
|
||||
"delete_after_days": delete_days,
|
||||
"outbound_mode": "smtp" if smtp else "direct",
|
||||
"outbound_host": smtp["host"] if smtp else "direct.invalid",
|
||||
"outbound_port": smtp["port"] if smtp else 1,
|
||||
"outbound_tls": smtp["tls"] if smtp else "starttls",
|
||||
"outbound_username": username,
|
||||
}
|
||||
return native.create_account(owner, settings, secret)
|
||||
|
||||
|
||||
@@ -31,7 +31,9 @@ class ExternalAccountWebTests(unittest.TestCase):
|
||||
"id": "example", "name": "Example",
|
||||
"authentication": ["app_password"],
|
||||
"imap": {"host": "imap.example.test", "port": 993,
|
||||
"tls": "implicit"}}]}
|
||||
"tls": "implicit"},
|
||||
"smtp": {"host": "smtp.example.test", "port": 587,
|
||||
"tls": "starttls"}}]}
|
||||
|
||||
def tearDown(self):
|
||||
sys.modules.pop("bongo_web.external_accounts", None)
|
||||
@@ -54,6 +56,12 @@ class ExternalAccountWebTests(unittest.TestCase):
|
||||
self.assertEqual(settings["destination_folder"], "INBOX")
|
||||
self.assertEqual(settings["inbound_host"], "imap.example.test")
|
||||
self.assertEqual(settings["inbound_port"], 993)
|
||||
self.assertEqual(settings["email_address"], "external@example.test")
|
||||
self.assertEqual(settings["outbound_mode"], "smtp")
|
||||
self.assertEqual(settings["outbound_host"], "smtp.example.test")
|
||||
self.assertEqual(settings["outbound_port"], 587)
|
||||
self.assertEqual(settings["outbound_username"],
|
||||
"external@example.test")
|
||||
|
||||
def test_oauth_only_provider_is_rejected_until_supported(self):
|
||||
presets = {"providers": [{"id": "oauth", "name": "OAuth",
|
||||
|
||||
Reference in New Issue
Block a user