commit - da1136e4275efd1152585f31dab183ab5089370d
commit + 58a006b393da74d6143bdf2a5378f87f87bd70be
blob - 50638422e9585c4f4108abfb0fa57a2695ca3baa
blob + 152b3f5b275cc7d4db1e7535377d06795b9e40fa
--- README.md
+++ README.md
A from-scratch IMAP4rev2 ([RFC 9051](https://www.rfc-editor.org/rfc/rfc9051)) server for OpenBSD, written in C in the privilege-separated tradition of `smtpd(8)`, `httpd(8)`, and `ntpd(8)`. No third-party IMAP library.
-**Status:** 0.2.1 Pre-release, actively developed. Not a port. See [Getting the source](#getting-the-source) below for the repository.
+**Status:** 0.2.2 Pre-release, actively developed. Not a port. See [Getting the source](#getting-the-source) below for the repository.
## What it is
blob - 7e0c578730502999d1136d4258eab53efa0c9d4b
blob + 596c4dfcef0195bd1299f70fbea1d32586cee463
--- src/append_cmd.c
+++ src/append_cmd.c
else if (res->error == MBOX_OP_ERR_BUSY)
session_reply(s, s->pending_tag, "NO",
IMAP_BUSY_TEXT);
+ else if (res->error == MBOX_OP_ERR_OVERQUOTA)
+ session_reply(s, s->pending_tag, "NO",
+ IMAP_OVERQUOTA_TEXT);
else
session_reply(s, s->pending_tag, "NO",
"APPEND failed");
blob - 7cbe25a59044bfe29bab71adac0297c25f905621
blob + a4b13bb83eaccee953142bf9ebf1432fe2958254
--- src/imapd.conf.5
+++ src/imapd.conf.5
.\" Written for the OpenIMAPD project. Public domain / no rights reserved,
.\" matching the project's ports-oriented, OpenBSD-base-inclusion goal.
.\"
-.Dd $Mdocdate: October 3 2026 $
+.Dd $Mdocdate: October 4 2026 $
.Dt IMAPD.CONF 5
.Os
.Sh NAME
writable by mail users;
.Xr imapd 8
does not check this.
+When less than 5% of the space or inodes of the filesystem holding a
+mailbox is free to non-root users,
+.Xr imapd 8
+refuses to add to it: APPEND, COPY, MOVE to another mailbox and CREATE
+fail with the
+.Dq OVERQUOTA
+response code.
+Flags can still be changed and messages expunged.
+Mail delivered by
+.Xr smtpd 8
+is not refused.
The default is
.Pa /var/mail/imapd .
.It Ic startups begin Ar count Ic rate Ar percent Ic full Ar count
blob - 178a56bd0c968d14e88ac309af53431fb69dbae2
blob + ad6ba2b57e341a039b4ee83e8985f20671d4446f
--- src/imapd.h
+++ src/imapd.h
#include <imsg.h>
#include <stdint.h>
-#define IMAPD_VERSION "0.2.1"
+#define IMAPD_VERSION "0.2.2"
#define IMAPD_USER "_imapd"
#define IMAPD_AUTH_USER "_imapauth"
#define MBOX_NAME_MAX 256
-/* RFC 5530 NONEXISTENT/ALREADYEXISTS/LIMIT; RFC 9051 INUSE, NOTSAVED */
+/* RFC 5530 SS3 response codes, and RFC 9051 NOTSAVED */
enum mbox_op_error {
MBOX_ERR_UNSET = 0,
MBOX_OP_OK,
MBOX_OP_ERR_EXPUNGEISSUED,
MBOX_OP_ERR_LIMIT,
MBOX_OP_ERR_NOTSAVED,
+ MBOX_OP_ERR_OVERQUOTA,
};
/* QRESYNC select-param (RFC 7162 SS3.2.5). */
blob - 2c32ea030166b5b383da1b7d1c3561933e8c9407
blob + 68126e28fffe04e0f26c21b5e2882ed2b8e61d41
--- src/index.c
+++ src/index.c
index_save(int dfd, const struct mbox_index *idx)
{
FILE *fp;
- int fd;
+ int fd, serrno;
size_t i;
/* O_EXCL: never follow a planted symlink */
(unsigned long long)idx->highestmodseq) < 0) {
log_warnx("session %u: write index header failed",
session_id);
- fclose(fp);
- return (-1);
+ goto fail;
}
for (i = 0; i < idx->nlines; i++) {
if (fprintf(fp, "%s\n", idx->lines[i]) < 0) {
log_warnx("session %u: write index line failed",
session_id);
- fclose(fp);
- return (-1);
+ goto fail;
}
}
if (fflush(fp) != 0) {
log_warn("session %u: fflush %s", session_id,
STORE_INDEX_TMP_NAME);
- fclose(fp);
- return (-1);
+ goto fail;
}
if (fsync(fileno(fp)) == -1) {
log_warn("session %u: fsync %s", session_id,
STORE_INDEX_TMP_NAME);
- fclose(fp);
- return (-1);
+ goto fail;
}
if (fclose(fp) != 0) {
log_warn("session %u: fclose %s", session_id,
log_warn("session %u: fsync mailbox directory "
"(continuing)", session_id);
return (0);
+
+fail:
+ /* the caller reads errno: ENOSPC answers OVERQUOTA */
+ serrno = errno;
+ fclose(fp);
+ errno = serrno;
+ return (-1);
}
int
refresh_index(int dfd, struct mbox_index *idx, int fd)
{
- int added;
+ int added, serrno;
if (index_load(fd, idx) == -1)
return (-1);
return (0);
if (index_save(dfd, idx) == -1) {
+ serrno = errno;
index_free(idx);
+ errno = serrno;
return (-1);
}
return (0);
blob - 900627b3d2989560a5d97bc7a6a83e9a5e7e8ed8
blob + 14c2f93c66bdbc2b25143b61ec596f8d7db838fd
--- src/listener.h
+++ src/listener.h
extern struct imsgev iev_parent;
extern struct tls *listener_tls_ctx;
-/* RFC 9051 SS7.1 INUSE */
+/* RFC 9051 SS7.1 INUSE, OVERQUOTA */
#define IMAP_BUSY_TEXT "[INUSE] mailbox busy, try again"
+#define IMAP_OVERQUOTA_TEXT "[OVERQUOTA] not enough free space"
extern uint32_t listener_idle_poll_secs;
extern uint32_t listener_login_grace_secs;
blob - c91609ea6e57b9ca568497435bfef3cbb3f02b97
blob + 00ffff1b63663f3599aa5c68bef8d0360370412d
--- src/mbox_copy.c
+++ src/mbox_copy.c
}
left -= n;
}
- if (fsync(tmpfd) == -1)
- log_warn("session %u: COPY: fsync %s (continuing)", session_id,
- tmppath);
- close(tmpfd);
+ if (fsync(tmpfd) == -1) {
+ log_warn("session %u: COPY: fsync %s", session_id, tmppath);
+ goto fail;
+ }
+ if (close(tmpfd) == -1) {
+ log_warn("session %u: COPY: close %s", session_id, tmppath);
+ tmpfd = -1;
+ goto fail;
+ }
close(srcfd);
if (renameat(dfd, tmppath, dfd, curpath) == -1) {
log_warn("session %u: COPY: rename %s -> %s", session_id,
return (0);
fail:
- close(tmpfd);
+ if (tmpfd != -1)
+ close(tmpfd);
close(srcfd);
unlinkat(dfd, tmppath, 0);
return (-1);
index_lock_release(il_b);
if (result->error != MBOX_OP_ERR_NO_SUCH_MAILBOX &&
- result->error != MBOX_OP_ERR_LIMIT)
+ result->error != MBOX_OP_ERR_LIMIT &&
+ result->error != MBOX_OP_ERR_OVERQUOTA)
result->error = ok ? MBOX_OP_OK : MBOX_OP_ERR_GENERIC;
index_free(idx_a);
index_free(idx_b);
goto done;
}
dfd = cross_mailbox ? destfd : ss->mailbox_dir_fd;
+ if (!store_space_ok(dfd)) {
+ result.error = MBOX_OP_ERR_OVERQUOTA;
+ ok = 0;
+ goto close_dest;
+ }
/* RFC 2180 SS4.4.2: the numbers are those before the EXPUNGEs */
if ((got = saved_resolve(ss, ranges, nranges, vr, &nvr,
&req->by_uid, &used)) != 0) {
ok = 0;
goto done;
}
+ if (cross_mailbox && !store_space_ok(destfd)) {
+ result.error = MBOX_OP_ERR_OVERQUOTA;
+ ok = 0;
+ close(destfd);
+ goto done;
+ }
if ((got = saved_resolve(ss, ranges, nranges, vr, &nvr,
&req->by_uid, &used)) != 0) {
blob - 8b2586daa09bc057f91e13740e82ca128037d069
blob + 4a85168d6e91df742c5d7a3a265c65d69f6f2598
--- src/mbox_fetch.c
+++ src/mbox_fetch.c
else {
log_debug("session %u: STATUS %s: invalid mailbox name",
session_id, req->mailbox);
- reply.error = MBOX_OP_ERR_GENERIC;
+ reply.error = MBOX_OP_ERR_NO_SUCH_MAILBOX;
goto send;
}
if ((tfd = mailbox_open_dir(target)) == -1) {
- log_debug("session %u: STATUS %s: no such mailbox",
- session_id, req->mailbox);
- reply.error = MBOX_OP_ERR_GENERIC;
+ reply.error = errno == ENOENT || errno == ENOTDIR ?
+ MBOX_OP_ERR_NO_SUCH_MAILBOX : MBOX_OP_ERR_GENERIC;
+ if (reply.error == MBOX_OP_ERR_GENERIC)
+ log_warn("session %u: STATUS %s", session_id,
+ req->mailbox);
goto send;
}
}
if (refresh_index(tfd, &idx, il.fd) == -1) {
+ reply.error = errno == ENOSPC || errno == EDQUOT ?
+ MBOX_OP_ERR_OVERQUOTA : MBOX_OP_ERR_GENERIC;
index_lock_release(&il);
- reply.error = MBOX_OP_ERR_GENERIC;
goto send;
}
blob - 8e433d36a9774891a96bfabd88ce6621fbbabced
blob + 87c2870025ee974842c127c02c638b19577c9cda
--- src/mbox_manage.c
+++ src/mbox_manage.c
/*
* Copyright (c) 2026 David Williams <dhw@openimapd.dev>
+ * Copyright (c) 2011 Gilles Chehade <gilles@poolp.org>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
#include <sys/types.h>
#include <sys/file.h>
+#include <sys/mount.h>
#include <sys/stat.h>
#include <dirent.h>
else {
log_debug("session %u: SELECT %s: invalid mailbox name",
session_id, req->mailbox);
- reply.error = MBOX_OP_ERR_GENERIC;
+ reply.error = MBOX_OP_ERR_NO_SUCH_MAILBOX;
goto send;
}
if ((dfd = mailbox_open_dir(target)) == -1) {
- log_debug("session %u: SELECT %s: no such mailbox",
- session_id, req->mailbox);
- reply.error = MBOX_OP_ERR_GENERIC;
+ reply.error = errno == ENOENT || errno == ENOTDIR ?
+ MBOX_OP_ERR_NO_SUCH_MAILBOX : MBOX_OP_ERR_GENERIC;
+ if (reply.error == MBOX_OP_ERR_GENERIC)
+ log_warn("session %u: SELECT %s", session_id,
+ req->mailbox);
goto send;
}
}
if (refresh_index(dfd, &idx, il.fd) == -1) {
+ reply.error = errno == ENOSPC || errno == EDQUOT ?
+ MBOX_OP_ERR_OVERQUOTA : MBOX_OP_ERR_GENERIC;
index_lock_release(&il);
close(dfd);
- reply.error = MBOX_OP_ERR_GENERIC;
goto send;
}
}
+#define STORE_MINSPACE 5
+#define STORE_MININODES 5
+
+/* growth needs 5% of space and inodes free, as smtpd's queue_fs.c */
+int
+store_space_ok(int fd)
+{
+ struct statfs sfs;
+ uint64_t used, avail;
+
+ if (fstatfs(fd, &sfs) == -1) {
+ log_warn("session %u: fstatfs", session_id);
+ return (0);
+ }
+ used = sfs.f_blocks - sfs.f_bfree;
+ avail = sfs.f_bavail > 0 ? (uint64_t)sfs.f_bavail : 0;
+ if (sfs.f_blocks > 0 &&
+ avail * 100 < (avail + used) * STORE_MINSPACE) {
+ log_warnx("session %u: less than %d%% of the mail store's "
+ "space is free, refusing to add to it", session_id,
+ STORE_MINSPACE);
+ return (0);
+ }
+ used = sfs.f_files - sfs.f_ffree;
+ avail = sfs.f_favail > 0 ? (uint64_t)sfs.f_favail : 0;
+ if (sfs.f_files > 0 &&
+ avail * 100 < (avail + used) * STORE_MININODES) {
+ log_warnx("session %u: less than %d%% of the mail store's "
+ "inodes are free, refusing to add to it", session_id,
+ STORE_MININODES);
+ return (0);
+ }
+ return (1);
+}
+
/* RFC 9051 SS6.3.4 CREATE */
void
goto send;
}
+ if (!store_space_ok(maildir_root_fd)) {
+ result.error = MBOX_OP_ERR_OVERQUOTA;
+ goto send;
+ }
if (mkdirat(maildir_root_fd, req->mailbox, 0700) == -1) {
if (errno != EEXIST) {
+ result.error = errno == ENOSPC || errno == EDQUOT ?
+ MBOX_OP_ERR_OVERQUOTA : MBOX_OP_ERR_GENERIC;
log_warn("session %u: CREATE: mkdir %s", session_id,
req->mailbox);
- result.error = MBOX_OP_ERR_GENERIC;
} else {
log_debug("session %u: CREATE %s: already exists",
session_id, req->mailbox);
ap->error = MBOX_OP_ERR_NO_SUCH_MAILBOX;
return;
}
+ if (!store_space_ok(ap->tfd)) {
+ ap->error = MBOX_OP_ERR_OVERQUOTA;
+ return;
+ }
if (ensure_maildir_dirs(ap->tfd, "") == -1)
return;
}
if ((ap->tmpfd = openat(ap->tfd, ap->tmppath,
O_WRONLY | O_CREAT | O_EXCL, 0600)) == -1) {
+ if (errno == ENOSPC || errno == EDQUOT)
+ ap->error = MBOX_OP_ERR_OVERQUOTA;
log_warn("session %u: open %s", session_id, ap->tmppath);
/* not ours to remove, O_EXCL may have found another file */
ap->tmppath[0] = '\0';
if (n == -1) {
if (errno == EINTR)
continue;
+ ap->error = errno == ENOSPC || errno == EDQUOT ?
+ MBOX_OP_ERR_OVERQUOTA : MBOX_OP_ERR_GENERIC;
log_warn("session %u: write %s", session_id,
ap->tmppath);
ap->failed = 1;
- ap->error = MBOX_OP_ERR_GENERIC;
return;
}
written += (size_t)n;
/* A re-run after a busy lock finds this already done. */
if (ap->tmpfd != -1) {
- if (fsync(ap->tmpfd) == -1)
- log_warn("session %u: fsync %s (continuing)",
- session_id, ap->tmppath);
- close(ap->tmpfd);
- ap->tmpfd = -1;
+ int r;
+
+ if ((r = fsync(ap->tmpfd)) == 0) {
+ r = close(ap->tmpfd);
+ ap->tmpfd = -1;
+ }
+ if (r == -1) {
+ reply.error = errno == ENOSPC || errno == EDQUOT ?
+ MBOX_OP_ERR_OVERQUOTA : MBOX_OP_ERR_GENERIC;
+ log_warn("session %u: fsync or close %s", session_id,
+ ap->tmppath);
+ goto done;
+ }
}
locked = index_lock_acquire(ap->tfd, &il, LOCK_EX | LOCK_NB);
blob - 17234463d2693ea76eb2ecc26bc1cd5a72c89042
blob + 35ffc566b73f498e61f59bf40776748d26b87b89
--- src/store_cmd.c
+++ src/store_cmd.c
res->error == MBOX_OP_ERR_LIMIT)
snprintf(text, sizeof(text), "[LIMIT] %s failed",
cmdname);
+ else if (!s->copy_alloc_failed &&
+ res->error == MBOX_OP_ERR_OVERQUOTA)
+ snprintf(text, sizeof(text), "%s",
+ IMAP_OVERQUOTA_TEXT);
else
snprintf(text, sizeof(text), "%s failed", cmdname);
session_reply(s, s->pending_tag, "NO", text);
blob - 256654b5b1046de87793173ceb0b869e49e58304
blob + af036bd14d59347e1db3ab7d2c57f9914359fe22
--- src/store_internal.h
+++ src/store_internal.h
const char *append_hostname(void);
int ensure_maildir_dirs(int, const char *);
+int store_space_ok(int);
uint32_t letters_to_sysflags(const char *);
int merge_keywords(int, const char *, const char *, char *,
size_t);
blob - a7dcc836654c3556913a0bde6116b715907c581d
blob + fb6781900013f0b414aa6a72d370df8ce6e14ddc
--- src/store_ipc.c
+++ src/store_ipc.c
char buf[128];
if (res->error != MBOX_OP_OK || s->qresync_alloc_failed) {
- /* RFC 5530 NONEXISTENT */
+ /* RFC 5530 NONEXISTENT only when there is no such mailbox */
s->state = SESSION_AUTHENTICATED;
session_reply(s, s->pending_tag, "NO",
s->qresync_alloc_failed ? "[UNAVAILABLE] SELECT failed" :
res->error == MBOX_OP_ERR_BUSY ? IMAP_BUSY_TEXT :
- "[NONEXISTENT] no such mailbox");
+ res->error == MBOX_OP_ERR_NO_SUCH_MAILBOX ?
+ "[NONEXISTENT] no such mailbox" :
+ res->error == MBOX_OP_ERR_OVERQUOTA ? IMAP_OVERQUOTA_TEXT :
+ s->mbox_readonly ? "EXAMINE failed" : "SELECT failed");
/* a compromised store child may have streamed resync data */
free(s->vanished_ranges);
s->state = s->status_prev_state;
if (res->error != MBOX_OP_OK) {
- /* a missing mailbox and an I/O failure look alike */
+ /* RFC 5530 NONEXISTENT only when there is no such mailbox */
session_reply(s, s->pending_tag, "NO",
res->error == MBOX_OP_ERR_BUSY ? IMAP_BUSY_TEXT :
- "[NONEXISTENT] no such mailbox");
+ res->error == MBOX_OP_ERR_NO_SUCH_MAILBOX ?
+ "[NONEXISTENT] no such mailbox" :
+ res->error == MBOX_OP_ERR_OVERQUOTA ? IMAP_OVERQUOTA_TEXT :
+ "STATUS failed");
return;
}
session_reply(s, s->pending_tag, "NO",
"[ALREADYEXISTS] mailbox already exists");
return;
+ case MBOX_OP_ERR_OVERQUOTA:
+ session_reply(s, s->pending_tag, "NO", IMAP_OVERQUOTA_TEXT);
+ return;
default:
snprintf(text, sizeof(text), "%s failed", cmdname);
session_reply(s, s->pending_tag, "NO", text);