├── .gitignore ├── Makefile ├── README.md ├── UNLICENSE ├── database.c ├── database.h ├── main.c ├── pop3.c ├── pop3.h ├── server.c ├── server.h ├── smtp.c └── smtp.h /.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | *.db 3 | minimail 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CFLAGS = -std=c99 -Wall -O2 2 | LDFLAGS = -pthread 3 | LDLIBS = -lsqlite3 4 | 5 | minimail : server.o main.o smtp.o pop3.o database.o 6 | $(CC) $(LDFLAGS) -o $@ $^ $(LDLIBS) 7 | 8 | run : minimail 9 | ./$^ 10 | 11 | clean : 12 | $(RM) *.o minimail 13 | 14 | database.o: database.c database.h 15 | main.o: main.c server.h database.h smtp.h pop3.h 16 | pop3.o: pop3.c database.h pop3.h 17 | server.o: server.c server.h 18 | smtp.o: smtp.c database.h 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Minimail 2 | 3 | Mini POP3 + SMTP server backed by SQLite. It's intended to serve as a 4 | local, isolated, embedded mail server. There's no authentication and 5 | the server accepts mail for any account. 6 | -------------------------------------------------------------------------------- /UNLICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /database.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "database.h" 6 | 7 | #define CHECK(db, expr, ret) \ 8 | if ((expr) != SQLITE_OK) { \ 9 | fprintf(stderr, "sqlite3: %s\n", sqlite3_errmsg((db)->db)); \ 10 | return ret; \ 11 | } 12 | 13 | static const char *SQL_TABLE = 14 | "CREATE TABLE IF NOT EXISTS email" 15 | "(id INTEGER PRIMARY KEY, user, message)"; 16 | 17 | static const char *SQL_SEND = 18 | "INSERT INTO email (user, message) VALUES (?, ?)"; 19 | 20 | static const char *SQL_LIST = 21 | "SELECT id, message FROM email WHERE user = ?"; 22 | 23 | static const char *SQL_DELETE = 24 | "DELETE FROM email WHERE id = ?"; 25 | 26 | int database_open(struct database *db, const char *file) 27 | { 28 | if (sqlite3_open(file, &db->db) != 0) 29 | return -1; 30 | CHECK(db, sqlite3_exec(db->db, SQL_TABLE, NULL, NULL, NULL), -1); 31 | CHECK(db, sqlite3_prepare_v2(db->db, SQL_SEND, -1, &db->send, NULL), -1); 32 | CHECK(db, sqlite3_prepare_v2(db->db, SQL_LIST, -1, &db->list, NULL), -1); 33 | CHECK(db, sqlite3_prepare_v2(db->db, SQL_DELETE, -1, &db->del, NULL), -1); 34 | return 0; 35 | } 36 | 37 | int database_close(struct database *db) 38 | { 39 | return sqlite3_close(db->db); 40 | } 41 | 42 | int database_send(struct database *db, const char *user, const char *message) 43 | { 44 | CHECK(db, sqlite3_reset(db->send), -1); 45 | CHECK(db, sqlite3_bind_text(db->send, 1, user, -1, SQLITE_TRANSIENT), -1); 46 | CHECK(db, sqlite3_bind_blob(db->send, 2, message, 47 | strlen(message), SQLITE_TRANSIENT), -1); 48 | return sqlite3_step(db->send) == SQLITE_ROW ? 0 : -1; 49 | } 50 | 51 | struct message * 52 | database_list(struct database *db, const char *user) 53 | { 54 | CHECK(db, sqlite3_reset(db->list), NULL); 55 | CHECK(db, sqlite3_bind_text(db->list, 1, user, -1, SQLITE_TRANSIENT), 56 | NULL); 57 | struct message *messages = NULL; 58 | while (sqlite3_step(db->list) == SQLITE_ROW) { 59 | int id = sqlite3_column_int(db->list, 0); 60 | const void *blob = sqlite3_column_blob(db->list, 1); 61 | int length = sqlite3_column_bytes(db->list, 1); 62 | struct message *message = malloc(sizeof(*message) + length); 63 | memcpy(message->content, blob, length); 64 | message->id = id; 65 | message->length = length; 66 | message->next = messages; 67 | messages = message; 68 | } 69 | return messages; 70 | } 71 | 72 | int database_delete(struct database *db, int id) 73 | { 74 | CHECK(db, sqlite3_reset(db->del), -1); 75 | CHECK(db, sqlite3_bind_int(db->del, 1, id), -1); 76 | while (sqlite3_step(db->del) == SQLITE_ROW); 77 | return 0; 78 | } 79 | -------------------------------------------------------------------------------- /database.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | struct database { 6 | sqlite3 *db; 7 | sqlite3_stmt *send, *list, *del; 8 | }; 9 | 10 | struct message { 11 | int id; 12 | int length; 13 | struct message *next; 14 | char content[]; 15 | }; 16 | 17 | int database_open(struct database *db, const char *file); 18 | int database_close(struct database *db); 19 | 20 | int database_send(struct database *db, const char *user, const char *message); 21 | struct message *database_list(struct database *db, const char *user); 22 | int database_delete(struct database *db, int id); 23 | -------------------------------------------------------------------------------- /main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "server.h" 7 | #include "database.h" 8 | #include "smtp.h" 9 | #include "pop3.h" 10 | 11 | #define ERROR(format, args...) \ 12 | do { \ 13 | fprintf(stderr, "%s: " format "\n", argv[0], args); \ 14 | exit(EXIT_FAILURE); \ 15 | } while (0) 16 | 17 | int main(int argc, char **argv) 18 | { 19 | /* Configuration */ 20 | int smtp_port = 7725; 21 | int pop3_port = 7110; 22 | char *dbfile = "email.db"; 23 | 24 | /* Option parsing */ 25 | static struct option options[] = { 26 | {"smtp-port", required_argument, 0, 's'}, 27 | {"pop3-port", required_argument, 0, 'p'}, 28 | {"database", required_argument, 0, 'd'}, 29 | {0} 30 | }; 31 | int option; 32 | while ((option = getopt_long(argc, argv, "", options, NULL)) != -1) { 33 | switch (option) { 34 | case 's': 35 | smtp_port = atoi(optarg); 36 | break; 37 | case 'p': 38 | pop3_port = atoi(optarg); 39 | break; 40 | case 'd': 41 | dbfile = optarg; 42 | break; 43 | default: 44 | exit(EXIT_FAILURE); 45 | } 46 | } 47 | 48 | /* Ensure database works */ 49 | struct database db; 50 | if (database_open(&db, dbfile) != 0) 51 | ERROR("failed to open database, %s", dbfile); 52 | database_close(&db); 53 | 54 | /* Launch servers. */ 55 | struct server smtp = { 56 | .port = smtp_port, 57 | .handler = smtp_handler, 58 | .arg = dbfile 59 | }; 60 | struct server pop3 = { 61 | .port = pop3_port, 62 | .handler = pop3_handler, 63 | .arg = dbfile 64 | }; 65 | if (server_start(&smtp) != 0) 66 | ERROR("%s", "failed to start SMTP server"); 67 | if (server_start(&pop3) != 0) 68 | ERROR("%s", "failed to start POP3 server"); 69 | pthread_join(smtp.thread, NULL); 70 | pthread_join(pop3.thread, NULL); 71 | return 0; 72 | } 73 | -------------------------------------------------------------------------------- /pop3.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "database.h" 6 | #include "pop3.h" 7 | 8 | void pop3_handler(FILE *client, void *arg) 9 | { 10 | pop3(client, (const char *) arg); 11 | } 12 | 13 | #define RESPOND(client, format, args...) \ 14 | fprintf(client, format "\r\n", args) 15 | 16 | void pop3(FILE *client, const char *dbfile) 17 | { 18 | struct database db; 19 | if (database_open(&db, dbfile) != 0) { 20 | RESPOND(client, "%s", "-ERR"); 21 | fclose(client); 22 | return; 23 | } else { 24 | RESPOND(client, "+OK %s", "POP3 server ready"); 25 | } 26 | 27 | char user[512] = {0}; 28 | struct message *messages = NULL; 29 | while (!feof(client)) { 30 | char line[512]; 31 | printf("%s", line); 32 | fgets(line, sizeof(line), client); 33 | char command[5] = {line[0], line[1], line[2], line[3]}; 34 | 35 | if (strcmp(command, "USER") == 0) { 36 | if (strlen(line) > 5) { 37 | char *a = line + 5, *b = user; 38 | while (isalnum(*a)) 39 | *(b++) = *(a++); 40 | *b = '\0'; 41 | RESPOND(client, "%s", "+OK"); 42 | } else { 43 | RESPOND(client, "%s", "-ERR"); 44 | } 45 | messages = database_list(&db, user); 46 | } else if (strcmp(command, "PASS") == 0) { 47 | RESPOND(client, "%s", "+OK"); // don't care 48 | } else if (strcmp(command, "STAT") == 0) { 49 | int count = 0, size = 0; 50 | for (struct message *m = messages; m; m = m->next) { 51 | count++; 52 | size += m->length; 53 | } 54 | RESPOND(client, "+OK %d %d", count, size); 55 | } else if (strcmp(command, "LIST") == 0) { 56 | RESPOND(client, "%s", "+OK"); 57 | for (struct message *m = messages; m; m = m->next) 58 | RESPOND(client, "%d %d", m->id, m->length); 59 | RESPOND(client, "%s", "."); 60 | } else if (strcmp(command, "RETR") == 0) { 61 | int id = atoi(line + 4); 62 | int found = 0; 63 | for (struct message *m = messages; m; m = m->next) 64 | if (m->id == id) { 65 | found = 1; 66 | RESPOND(client, "%s", "+OK"); 67 | RESPOND(client, "%s", m->content); 68 | } 69 | RESPOND(client, "%s", found ? "." : "-ERR"); 70 | } else if (strcmp(command, "DELE") == 0) { 71 | int id = atoi(line + 4); 72 | int found = 0; 73 | for (struct message *m = messages; m; m = m->next) { 74 | if (m->id == id) { 75 | RESPOND(client, "%s", "+OK"); 76 | database_delete(&db, id); 77 | break; 78 | } 79 | } 80 | if (!found) 81 | RESPOND(client, "%s", "-ERR"); 82 | } else if (strcmp(command, "TOP ") == 0) { 83 | int id, lines; 84 | sscanf(line, "TOP %d %d", &id, &lines); 85 | int found = 0; 86 | for (struct message *m = messages; m; m = m->next) { 87 | if (m->id == id) { 88 | RESPOND(client, "%s", "+OK"); 89 | found = 1; 90 | char *p = m->content; 91 | while (*p && memcmp(p, "\r\n\r\n", 4) != 0) { 92 | fputc(*p, client); 93 | p++; 94 | } 95 | if (*p) { 96 | p += 4; 97 | int line = 0; 98 | while (*p && line < lines) { 99 | if (*p == '\n') 100 | line++; 101 | p++; 102 | } 103 | } 104 | break; 105 | } 106 | } 107 | RESPOND(client, "%s", found ? "\r\n." : "-ERR"); 108 | } else if (strcmp(command, "QUIT") == 0) { 109 | RESPOND(client, "%s", "+OK"); 110 | break; 111 | } else { 112 | RESPOND(client, "%s", "-ERR"); 113 | } 114 | } 115 | 116 | while (messages) { 117 | struct message *dead = messages; 118 | messages = messages->next; 119 | free(dead); 120 | } 121 | fclose(client); 122 | database_close(&db); 123 | } 124 | -------------------------------------------------------------------------------- /pop3.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "database.h" 5 | 6 | void pop3(FILE *client, const char *dbfile); 7 | void pop3_handler(FILE *client, void *arg); 8 | -------------------------------------------------------------------------------- /server.c: -------------------------------------------------------------------------------- 1 | #define _POSIX_SOURCE 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "server.h" 13 | 14 | #define ERROR(format, args...) \ 15 | do { \ 16 | fprintf(stderr, "%s: " format "\n", strerror(errno), args); \ 17 | return errno; \ 18 | } while (0) 19 | 20 | struct job { 21 | struct server *server; 22 | FILE *client; 23 | }; 24 | 25 | static void *handler_thread(void *arg) 26 | { 27 | struct job *job = arg; 28 | job->server->handler(job->client, job->server->arg); 29 | free(job); 30 | return NULL; 31 | } 32 | 33 | static void *server_thread(void *arg) 34 | { 35 | struct server *server = arg; 36 | for (;;) { 37 | int fd; 38 | if ((fd = accept(server->fd, NULL, NULL)) == -1) { 39 | const char *err = strerror(errno); 40 | fprintf(stderr, "cannot accept connections: %s\n", err); 41 | return NULL; 42 | } 43 | struct job *job = malloc(sizeof(*job)); 44 | job->server = server; 45 | job->client = fdopen(fd, "r+"); 46 | pthread_t handler; 47 | if (pthread_create(&handler, NULL, handler_thread, job) != 0) { 48 | fprintf(stderr, "cannot start client thread\n"); 49 | fclose(job->client); 50 | } else { 51 | pthread_detach(handler); 52 | } 53 | } 54 | return NULL; 55 | } 56 | 57 | int server_start(struct server *server) 58 | { 59 | struct sockaddr_in addr = {0}; 60 | addr.sin_family = AF_INET; 61 | addr.sin_addr.s_addr = htonl(INADDR_ANY); 62 | addr.sin_port = htons(server->port); 63 | if ((server->fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) 64 | ERROR("%s", "could not create socket"); 65 | if (bind(server->fd, (void *) &addr, sizeof(addr)) != 0) 66 | ERROR("%s", "could not bind"); 67 | if (listen(server->fd, 1024) != 0) 68 | ERROR("%s", "could not listen"); 69 | if (pthread_create(&server->thread, NULL, server_thread, server) != 0) 70 | ERROR("%s", "could create server thread"); 71 | return 0; 72 | } 73 | -------------------------------------------------------------------------------- /server.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | typedef void (*handler_t)(FILE *client, void *arg); 6 | 7 | struct server { 8 | unsigned short port; 9 | handler_t handler; 10 | void *arg; 11 | int fd; 12 | pthread_t thread; 13 | }; 14 | 15 | int server_start(struct server *server); 16 | -------------------------------------------------------------------------------- /smtp.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "database.h" 5 | #include "smtp.h" 6 | 7 | void smtp_handler(FILE *client, void *arg) 8 | { 9 | smtp(client, (const char *) arg); 10 | } 11 | 12 | #define RESPOND(client, code, message) \ 13 | fprintf(client, "%d %s\r\n", code, message) 14 | 15 | struct recipient { 16 | char email[512]; 17 | struct recipient *next; 18 | }; 19 | 20 | void recipient_push(struct recipient **rs, char *email) 21 | { 22 | struct recipient *r = malloc(sizeof(*r)); 23 | char *p; 24 | for (p = r->email; *email && *email != '@'; p++, email++) 25 | *p = *email; 26 | *p = '\0'; 27 | r->next = *rs; 28 | *rs = r; 29 | } 30 | 31 | struct recipient *recipient_pop(struct recipient **rs) 32 | { 33 | struct recipient *r = *rs; 34 | if (*rs) 35 | *rs = (*rs)->next; 36 | return r; 37 | } 38 | 39 | void smtp(FILE *client, const char *dbfile) 40 | { 41 | struct database db; 42 | if (database_open(&db, dbfile) != 0) { 43 | RESPOND(client, 421, "database error"); 44 | fclose(client); 45 | return; 46 | } else { 47 | RESPOND(client, 220, "localhost"); 48 | } 49 | 50 | char from[512] = {0}; 51 | struct recipient *recipients = NULL; 52 | while (!feof(client)) { 53 | char line[512]; 54 | fgets(line, sizeof(line), client); 55 | char command[5] = {line[0], line[1], line[2], line[3]}; 56 | 57 | if (strcmp(command, "HELO") == 0) { 58 | RESPOND(client, 250, "localhost"); 59 | 60 | } else if (strcmp(command, "MAIL") == 0) { 61 | strcpy(from, line); 62 | RESPOND(client, 250, "OK"); 63 | 64 | } else if (strcmp(command, "RCPT") == 0) { 65 | if (!from[0]) { 66 | RESPOND(client, 503, "bad sequence"); 67 | } else if (strlen(line) < 12) { 68 | RESPOND(client, 501, "syntax error"); 69 | } else { 70 | recipient_push(&recipients, line + 9); 71 | RESPOND(client, 250, "OK"); 72 | } 73 | 74 | } else if (strcmp(command, "DATA") == 0) { 75 | RESPOND(client, 354, "end with ."); 76 | size_t size = 4096, fill = 0; 77 | char *content = malloc(size); 78 | for (;;) { 79 | fgets(line, sizeof(line), client); 80 | if (strcmp(line, ".\r\n") == 0) 81 | break; 82 | if (strlen(line) + fill >= size) { 83 | size *= 2; 84 | content = realloc(content, size); 85 | } 86 | strcpy(content + fill, line); 87 | fill += strlen(line); 88 | } 89 | while (recipients) { 90 | struct recipient *r = recipient_pop(&recipients); 91 | database_send(&db, r->email, content); 92 | free(r); 93 | } 94 | free(content); 95 | RESPOND(client, 250, "OK"); 96 | 97 | } else if (strcmp(command, "QUIT") == 0) { 98 | RESPOND(client, 221, "localhost"); 99 | break; 100 | 101 | } else { 102 | RESPOND(client, 500, "command unrecognized"); 103 | } 104 | } 105 | fclose(client); 106 | database_close(&db); 107 | } 108 | -------------------------------------------------------------------------------- /smtp.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "database.h" 5 | 6 | void smtp(FILE *client, const char *dbfile); 7 | void smtp_handler(FILE *client, void *arg); 8 | --------------------------------------------------------------------------------