├── .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 |
--------------------------------------------------------------------------------