aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorAnders Betts <anders.betts@gmail.com>2026-09-20 15:50:24 +0200
committerAnders Betts <anders.betts@gmail.com>2026-09-20 15:50:42 +0200
commit5e624647d2f2750cf1fe7f97d8c89718d187879d (patch)
tree6909b8ddc7ee6a1843ecab064fbf8b96e662eaa0 /tests
parentfb2addedce94bea29c9be5eab2786e621a5d0e65 (diff)
downloadbokf-5e624647d2f2750cf1fe7f97d8c89718d187879d.tar.gz
bokf-5e624647d2f2750cf1fe7f97d8c89718d187879d.zip
smtp: send messages with a PDF attachment
Diffstat (limited to 'tests')
-rw-r--r--tests/smtp_check.c663
1 files changed, 663 insertions, 0 deletions
diff --git a/tests/smtp_check.c b/tests/smtp_check.c
new file mode 100644
index 0000000..331f4a9
--- /dev/null
+++ b/tests/smtp_check.c
@@ -0,0 +1,663 @@
+#include <arpa/inet.h>
+#include <errno.h>
+#include <netinet/in.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include <openssl/ssl.h>
+
+#include "smtp.h"
+#include "util.h"
+
+enum {
+ SRV_NOAUTH = 0,
+ SRV_AUTH_PLAIN,
+ SRV_AUTH_LOGIN,
+ SRV_RCPT_REJECT,
+ SRV_TLS,
+ SRV_STARTTLS
+};
+
+struct srv_result {
+ int saw_ehlo;
+ int saw_auth;
+ int saw_auth_plain;
+ int saw_auth_login;
+ int saw_starttls;
+ size_t data_len;
+};
+
+static int failures;
+
+static void check(int cond, const char *what)
+{
+ if (!cond) {
+ printf("FAIL %s\n", what);
+ failures++;
+ } else {
+ printf("ok %s\n", what);
+ }
+}
+
+static int contains(const unsigned char *data, size_t len, const char *needle)
+{
+ size_t n = strlen(needle);
+ if (n > len)
+ return 0;
+ for (size_t i = 0; i + n <= len; i++)
+ if (memcmp(data + i, needle, n) == 0)
+ return 1;
+ return 0;
+}
+
+static int count_occ(const unsigned char *data, size_t len, const char *needle)
+{
+ size_t n = strlen(needle);
+ int c = 0;
+ if (n == 0 || n > len)
+ return 0;
+ for (size_t i = 0; i + n <= len; i++)
+ if (memcmp(data + i, needle, n) == 0)
+ c++;
+ return c;
+}
+
+static ssize_t read_full(int fd, void *buf, size_t n)
+{
+ unsigned char *p = buf;
+ size_t got = 0;
+ while (got < n) {
+ ssize_t r = read(fd, p + got, n - got);
+ if (r < 0) {
+ if (errno == EINTR)
+ continue;
+ return -1;
+ }
+ if (r == 0)
+ break;
+ got += (size_t)r;
+ }
+ return (ssize_t)got;
+}
+
+static int write_full(int fd, const void *buf, size_t n)
+{
+ const unsigned char *p = buf;
+ size_t done = 0;
+ while (done < n) {
+ ssize_t w = write(fd, p + done, n - done);
+ if (w < 0) {
+ if (errno == EINTR)
+ continue;
+ return -1;
+ }
+ done += (size_t)w;
+ }
+ return 0;
+}
+
+struct srv_io {
+ int fd;
+ SSL *ssl;
+ unsigned char buf[4096];
+ size_t len;
+ size_t pos;
+};
+
+static int srv_write_all(struct srv_io *io, const char *s)
+{
+ size_t n = strlen(s);
+ size_t o = 0;
+ while (o < n) {
+ ssize_t w = io->ssl ? SSL_write(io->ssl, s + o, (int)(n - o))
+ : write(io->fd, s + o, n - o);
+ if (w <= 0)
+ return -1;
+ o += (size_t)w;
+ }
+ return 0;
+}
+
+static ssize_t srv_line(struct srv_io *io, char *out, size_t outsz)
+{
+ size_t o = 0;
+ for (;;) {
+ if (io->pos >= io->len) {
+ ssize_t n = io->ssl ? SSL_read(io->ssl, io->buf, (int)sizeof io->buf)
+ : read(io->fd, io->buf, sizeof io->buf);
+ if (n <= 0)
+ return -1;
+ io->len = (size_t)n;
+ io->pos = 0;
+ }
+ unsigned char ch = io->buf[io->pos++];
+ if (ch == '\n') {
+ while (o > 0 && out[o - 1] == '\r')
+ o--;
+ out[o] = '\0';
+ return (ssize_t)o;
+ }
+ if (o + 1 < outsz)
+ out[o++] = (char)ch;
+ }
+}
+
+static void srv_tls_accept(struct srv_io *io)
+{
+ SSL_CTX *ctx = SSL_CTX_new(TLS_server_method());
+ if (!ctx ||
+ SSL_CTX_use_certificate_chain_file(ctx,
+ "tests/tls_test_cert.pem") != 1 ||
+ SSL_CTX_use_PrivateKey_file(ctx, "tests/tls_test_key.pem",
+ SSL_FILETYPE_PEM) != 1)
+ _exit(1);
+ SSL *ssl = SSL_new(ctx);
+ if (!ssl || SSL_set_fd(ssl, io->fd) != 1 || SSL_accept(ssl) != 1)
+ _exit(1);
+ io->ssl = ssl;
+}
+
+static void srv_ehlo(struct srv_io *io, int mode, int after_tls)
+{
+ srv_write_all(io, "250-test.local hello\r\n");
+ if (mode == SRV_AUTH_PLAIN)
+ srv_write_all(io, "250-AUTH PLAIN LOGIN\r\n");
+ else if (mode == SRV_AUTH_LOGIN)
+ srv_write_all(io, "250-AUTH LOGIN\r\n");
+ if (mode == SRV_STARTTLS && !after_tls)
+ srv_write_all(io, "250-STARTTLS\r\n");
+ srv_write_all(io, "250 SIZE 10485760\r\n");
+}
+
+static int plain_ok(const char *b64)
+{
+ static const unsigned char want[] = { 0, 'm', 'a', 'i', 'l',
+ 'e', 'r', 0, 's', '3',
+ 'c', 'r', 'e', 't' };
+ unsigned char *raw = NULL;
+ size_t len = 0;
+ if (util_b64_decode(b64, strlen(b64), &raw, &len) != 0) {
+ if (getenv("SMTP_CHECK_DUMP"))
+ fprintf(stderr, "server: AUTH PLAIN decode failed [%s]\n", b64);
+ return 0;
+ }
+ int ok = len == sizeof want && memcmp(raw, want, len) == 0;
+ if (!ok && getenv("SMTP_CHECK_DUMP"))
+ fprintf(stderr, "server: AUTH PLAIN mismatch len=%zu [%s]\n", len,
+ b64);
+ free(raw);
+ return ok;
+}
+
+static void server_main(int wfd, int mode)
+{
+ int sfd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
+ if (sfd < 0)
+ _exit(1);
+ struct sockaddr_in sa;
+ memset(&sa, 0, sizeof sa);
+ sa.sin_family = AF_INET;
+ sa.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
+ sa.sin_port = 0;
+ if (bind(sfd, (struct sockaddr *)&sa, sizeof sa) != 0 ||
+ listen(sfd, 1) != 0)
+ _exit(1);
+ socklen_t slen = sizeof sa;
+ if (getsockname(sfd, (struct sockaddr *)&sa, &slen) != 0)
+ _exit(1);
+ int port = ntohs(sa.sin_port);
+ if (write_full(wfd, &port, sizeof port) != 0)
+ _exit(1);
+ int cfd = accept(sfd, NULL, NULL);
+ if (cfd < 0)
+ _exit(1);
+ close(sfd);
+
+ struct srv_io io;
+ memset(&io, 0, sizeof io);
+ io.fd = cfd;
+ struct srv_result res;
+ memset(&res, 0, sizeof res);
+ struct buf data;
+ buf_init(&data);
+
+ char *b64_user = util_b64((const unsigned char *)"mailer", 6);
+ char *b64_pass = util_b64((const unsigned char *)"s3cret", 6);
+
+ if (mode == SRV_TLS)
+ srv_tls_accept(&io);
+ srv_write_all(&io, "220 test.local ESMTP\r\n");
+
+ int in_data = 0;
+ char line[2048];
+ for (;;) {
+ ssize_t n = srv_line(&io, line, sizeof line);
+ if (n < 0)
+ break;
+ if (in_data) {
+ if (strcmp(line, ".") == 0) {
+ res.data_len = data.len;
+ in_data = 0;
+ if (srv_write_all(&io, "250 2.0.0 queued\r\n") != 0)
+ break;
+ continue;
+ }
+ const char *payload = line[0] == '.' ? line + 1 : line;
+ buf_append(&data, payload, strlen(payload));
+ buf_append(&data, "\r\n", 2);
+ continue;
+ }
+ if (strncmp(line, "EHLO", 4) == 0) {
+ res.saw_ehlo++;
+ srv_ehlo(&io, mode, mode == SRV_TLS || io.ssl != NULL);
+ } else if (strcmp(line, "STARTTLS") == 0) {
+ res.saw_starttls = 1;
+ srv_write_all(&io, "220 2.0.0 ready\r\n");
+ srv_tls_accept(&io);
+ } else if (strncmp(line, "AUTH PLAIN", 10) == 0) {
+ res.saw_auth = 1;
+ res.saw_auth_plain = 1;
+ const char *arg = line + 10;
+ while (*arg == ' ')
+ arg++;
+ srv_write_all(&io, plain_ok(arg)
+ ? "235 2.7.0 authenticated\r\n"
+ : "535 5.7.8 bad credentials\r\n");
+ } else if (strcmp(line, "AUTH LOGIN") == 0) {
+ res.saw_auth = 1;
+ res.saw_auth_login = 1;
+ srv_write_all(&io, "334 VXNlcm5hbWU6\r\n");
+ if (srv_line(&io, line, sizeof line) < 0)
+ _exit(1);
+ if (strcmp(line, b64_user) != 0) {
+ srv_write_all(&io, "535 5.7.8 bad credentials\r\n");
+ continue;
+ }
+ srv_write_all(&io, "334 UGFzc3dvcmQ6\r\n");
+ if (srv_line(&io, line, sizeof line) < 0)
+ _exit(1);
+ srv_write_all(&io, strcmp(line, b64_pass) == 0
+ ? "235 2.7.0 authenticated\r\n"
+ : "535 5.7.8 bad credentials\r\n");
+ } else if (strncmp(line, "MAIL FROM:", 10) == 0) {
+ srv_write_all(&io, "250 2.1.0 ok\r\n");
+ } else if (strncmp(line, "RCPT TO:", 8) == 0) {
+ srv_write_all(&io, mode == SRV_RCPT_REJECT
+ ? "550 5.1.1 no such user\r\n"
+ : "250 2.1.5 ok\r\n");
+ } else if (strcmp(line, "DATA") == 0) {
+ if (mode == SRV_RCPT_REJECT) {
+ srv_write_all(&io, "503 5.5.1 need RCPT\r\n");
+ } else {
+ srv_write_all(&io, "354 end with .\r\n");
+ in_data = 1;
+ }
+ } else if (strcmp(line, "QUIT") == 0) {
+ srv_write_all(&io, "221 2.0.0 bye\r\n");
+ break;
+ } else {
+ srv_write_all(&io, "500 5.5.1 unknown command\r\n");
+ }
+ }
+
+ (void)write_full(wfd, &res, sizeof res);
+ if (data.len)
+ (void)write_full(wfd, data.p, data.len);
+ close(wfd);
+ close(cfd);
+ buf_free(&data);
+ free(b64_user);
+ free(b64_pass);
+ _exit(0);
+}
+
+struct case_result {
+ int rc;
+ char err[512];
+ struct srv_result res;
+ unsigned char data[131072];
+ size_t data_len;
+ int server_ok;
+};
+
+static void run_case(int mode, struct smtp_message m, struct case_result *out)
+{
+ signal(SIGPIPE, SIG_IGN);
+ int pfd[2];
+ memset(out, 0, sizeof *out);
+ if (pipe(pfd) != 0) {
+ printf("FAIL pipe\n");
+ failures++;
+ return;
+ }
+ pid_t pid = fork();
+ if (pid == 0) {
+ close(pfd[0]);
+ server_main(pfd[1], mode);
+ }
+ close(pfd[1]);
+ int port = 0;
+ if (read_full(pfd[0], &port, sizeof port) != (ssize_t)sizeof port)
+ port = 0;
+ m.port = port;
+ out->rc = smtp_send(&m, out->err, sizeof out->err);
+
+ struct srv_result res;
+ memset(&res, 0, sizeof res);
+ if (read_full(pfd[0], &res, sizeof res) == (ssize_t)sizeof res) {
+ out->res = res;
+ size_t cap = res.data_len < sizeof out->data ? res.data_len
+ : sizeof out->data;
+ if (cap && read_full(pfd[0], out->data, cap) == (ssize_t)cap)
+ out->data_len = cap;
+ }
+ close(pfd[0]);
+ int status = 0;
+ waitpid(pid, &status, 0);
+ out->server_ok = WIFEXITED(status) && WEXITSTATUS(status) == 0;
+}
+
+static void check_delivered(const char *what, const struct case_result *c)
+{
+ check(c->server_ok && c->rc == 0, what);
+ if (c->server_ok && c->rc == 0)
+ return;
+ if (!c->server_ok)
+ printf(" server aborted\n");
+ if (c->rc != 0)
+ printf(" err: %s\n", c->err);
+}
+
+static struct smtp_message base_message(void)
+{
+ struct smtp_message m;
+ memset(&m, 0, sizeof m);
+ m.host = "localhost";
+ m.security = "plain";
+ m.from = "invoice@example.test";
+ m.from_name = "Bokf AB";
+ m.to = "kund@example.test";
+ m.subject = "Faktura 1001";
+ m.body = "Hej!\nBifogad faktura.\n";
+ return m;
+}
+
+static const unsigned char pdf[] =
+ "%PDF-1.4\n1 0 obj\n<< /Type /Catalog >>\nendobj\ntrailer\n"
+ "<< /Root 1 0 R >>\n%%EOF\n";
+
+static void flatten(const unsigned char *data, size_t len,
+ unsigned char **out, size_t *out_len)
+{
+ unsigned char *flat = malloc(len + 1);
+ size_t o = 0;
+ for (size_t i = 0; i < len; i++)
+ if (data[i] != '\r' && data[i] != '\n')
+ flat[o++] = data[i];
+ flat[o] = '\0';
+ *out = flat;
+ *out_len = o;
+}
+
+static void check_crlf(const unsigned char *data, size_t len)
+{
+ int crlf = 1;
+ int max = 0;
+ size_t start = 0;
+ for (size_t i = 0; i < len; i++) {
+ if (data[i] != '\n')
+ continue;
+ size_t l = i - start;
+ if (l > 0 && data[i - 1] == '\r')
+ l--;
+ else
+ crlf = 0;
+ if (l > (size_t)max)
+ max = (int)l;
+ start = i + 1;
+ }
+ check(crlf, "message uses CRLF only");
+ check(max > 0 && max < 998, "line lengths under 998");
+}
+
+static void test_multipart(void)
+{
+ struct smtp_message m = base_message();
+ m.from_name = "Bokf \xc3\x84rende";
+ m.subject = "Faktura p\xc3\xa5 svenska";
+ m.attach_name = "faktura.pdf";
+ m.attach = pdf;
+ m.attach_len = sizeof pdf - 1;
+
+ struct case_result c;
+ run_case(SRV_NOAUTH, m, &c);
+ check_delivered("multipart: delivered", &c);
+ check(c.res.saw_ehlo == 1, "multipart: one EHLO");
+ check(!c.res.saw_auth, "multipart: no AUTH when user empty");
+
+ const unsigned char *d = c.data;
+ size_t n = c.data_len;
+ if (getenv("SMTP_CHECK_DUMP"))
+ printf("--- message ---\n%.*s\n---\n", (int)n, d);
+ char *name_word = NULL;
+ char *subj_word = NULL;
+ {
+ char *b = util_b64((const unsigned char *)m.from_name,
+ strlen(m.from_name));
+ size_t l = strlen(b) + 13;
+ name_word = malloc(l);
+ snprintf(name_word, l, "=?UTF-8?B?%s?=", b);
+ free(b);
+ b = util_b64((const unsigned char *)m.subject, strlen(m.subject));
+ l = strlen(b) + 13;
+ subj_word = malloc(l);
+ snprintf(subj_word, l, "=?UTF-8?B?%s?=", b);
+ free(b);
+ }
+ char from_hdr[256];
+ snprintf(from_hdr, sizeof from_hdr, "From: %s <invoice@example.test>",
+ name_word);
+ char subj_hdr[256];
+ snprintf(subj_hdr, sizeof subj_hdr, "Subject: %s", subj_word);
+
+ check(contains(d, n, "MIME-Version: 1.0\r\n"), "multipart: MIME-Version");
+ check(contains(d, n, from_hdr), "multipart: From with B-encoded name");
+ check(contains(d, n, "To: <kund@example.test>\r\n"), "multipart: To");
+ check(contains(d, n, subj_hdr), "multipart: B-encoded subject");
+ check(contains(d, n, "Date: ") && contains(d, n, "+0000\r\n"),
+ "multipart: UTC Date");
+ check(contains(d, n, "Message-ID: <"), "multipart: Message-ID");
+ check(contains(d, n, "Content-Type: multipart/mixed; boundary=\""),
+ "multipart: multipart content type");
+ check(count_occ(d, n, "Content-Type: text/plain; charset=utf-8") == 1,
+ "multipart: text part");
+ check(contains(d, n, "Content-Type: application/pdf; name=\"faktura.pdf\""),
+ "multipart: attachment type and name");
+ check(contains(d, n,
+ "Content-Disposition: attachment; filename=\"faktura.pdf\""),
+ "multipart: attachment disposition");
+ check(count_occ(d, n, "Content-Transfer-Encoding: base64") == 2,
+ "multipart: two base64 parts");
+
+ unsigned char *flat = NULL;
+ size_t flat_len = 0;
+ flatten(d, n, &flat, &flat_len);
+ char *b64 = util_b64(pdf, sizeof pdf - 1);
+ check(contains(flat, flat_len, b64), "multipart: attachment base64");
+ free(b64);
+ b64 = util_b64((const unsigned char *)m.body, strlen(m.body));
+ check(contains(flat, flat_len, b64), "multipart: body base64");
+ free(b64);
+ free(flat);
+
+ check(count_occ(d, n, "--=_bokf_") == 3, "multipart: boundary delimiters");
+ check_crlf(d, n);
+ free(name_word);
+ free(subj_word);
+}
+
+static void test_text_only(void)
+{
+ struct smtp_message m = base_message();
+ struct case_result c;
+ run_case(SRV_NOAUTH, m, &c);
+ check_delivered("text-only: delivered", &c);
+ const unsigned char *d = c.data;
+ size_t n = c.data_len;
+ check(contains(d, n, "From: Bokf AB <invoice@example.test>\r\n"),
+ "text-only: plain display name");
+ check(contains(d, n, "Subject: Faktura 1001\r\n"),
+ "text-only: plain subject");
+ check(contains(d, n, "Content-Type: text/plain; charset=utf-8\r\n"),
+ "text-only: text content type");
+ check(!contains(d, n, "multipart/mixed"), "text-only: not multipart");
+ check_crlf(d, n);
+}
+
+static void test_rcpt_reject(void)
+{
+ struct smtp_message m = base_message();
+ m.attach_name = "faktura.pdf";
+ m.attach = pdf;
+ m.attach_len = sizeof pdf - 1;
+ struct case_result c;
+ run_case(SRV_RCPT_REJECT, m, &c);
+ check(c.server_ok, "reject: server completed session");
+ check(c.rc == -1, "reject: smtp_send failed");
+ check(strstr(c.err, "550") != NULL, "reject: err has 550");
+ check(strstr(c.err, "no such user") != NULL, "reject: err has server text");
+ check(!contains(c.data, c.data_len, "faktura"),
+ "reject: no DATA delivered");
+}
+
+static void test_auth_plain(void)
+{
+ struct smtp_message m = base_message();
+ m.user = "mailer";
+ m.password = "s3cret";
+ struct case_result c;
+ run_case(SRV_AUTH_PLAIN, m, &c);
+ check_delivered("auth plain: delivered", &c);
+ check(c.res.saw_auth_plain == 1, "auth plain: PLAIN used");
+ check(c.res.saw_auth_login == 0, "auth plain: LOGIN not used");
+ check(!contains(c.data, c.data_len, "s3cret"),
+ "auth plain: no secret in message");
+}
+
+static void test_auth_login(void)
+{
+ struct smtp_message m = base_message();
+ m.user = "mailer";
+ m.password = "s3cret";
+ struct case_result c;
+ run_case(SRV_AUTH_LOGIN, m, &c);
+ check_delivered("auth login: delivered", &c);
+ check(c.res.saw_auth_login == 1, "auth login: LOGIN used");
+ check(c.res.saw_auth_plain == 0, "auth login: PLAIN not used");
+}
+
+static void test_auth_reject(void)
+{
+ struct smtp_message m = base_message();
+ m.user = "mailer";
+ m.password = "wrong";
+ struct case_result c;
+ run_case(SRV_AUTH_PLAIN, m, &c);
+ check(c.rc == -1, "auth reject: smtp_send failed");
+ check(strstr(c.err, "535") != NULL, "auth reject: err has 535");
+ check(strstr(c.err, "s3cret") == NULL, "auth reject: no secret in err");
+}
+
+static void test_validation(void)
+{
+ char err[256];
+ check(smtp_send(NULL, err, sizeof err) == -1, "validation: NULL message");
+
+ struct smtp_message m = base_message();
+ m.port = 25;
+ m.security = "bogus";
+ check(smtp_send(&m, err, sizeof err) == -1, "validation: bad security");
+ check(strstr(err, "security") != NULL, "validation: bad security text");
+
+ m = base_message();
+ m.from = "";
+ check(smtp_send(&m, err, sizeof err) == -1, "validation: empty from");
+
+ m = base_message();
+ m.host = NULL;
+ check(smtp_send(&m, err, sizeof err) == -1, "validation: NULL host");
+}
+
+static int tls_available(void)
+{
+ return access("tests/tls_test_cert.pem", R_OK) == 0 &&
+ access("tests/tls_test_key.pem", R_OK) == 0;
+}
+
+static void with_test_ca(void (*fn)(struct smtp_message *, struct case_result *))
+{
+ char *saved = getenv("SSL_CERT_FILE");
+ char *copy = saved ? strdup(saved) : NULL;
+ setenv("SSL_CERT_FILE", "tests/tls_test_cert.pem", 1);
+ struct smtp_message m = base_message();
+ struct case_result c;
+ fn(&m, &c);
+ check_delivered("TLS case delivered", &c);
+ if (saved)
+ setenv("SSL_CERT_FILE", copy, 1);
+ else
+ unsetenv("SSL_CERT_FILE");
+ free(copy);
+}
+
+static void tls_case(struct smtp_message *m, struct case_result *c)
+{
+ m->security = "tls";
+ m->attach_name = "faktura.pdf";
+ m->attach = pdf;
+ m->attach_len = sizeof pdf - 1;
+ run_case(SRV_TLS, *m, c);
+}
+
+static void starttls_case(struct smtp_message *m, struct case_result *c)
+{
+ m->security = "starttls";
+ m->attach_name = "faktura.pdf";
+ m->attach = pdf;
+ m->attach_len = sizeof pdf - 1;
+ run_case(SRV_STARTTLS, *m, c);
+ check(c->res.saw_starttls == 1, "starttls: STARTTLS sent");
+ check(c->res.saw_ehlo == 2, "starttls: EHLO before and after");
+}
+
+int main(void)
+{
+ signal(SIGPIPE, SIG_IGN);
+ alarm(120);
+
+ test_multipart();
+ test_text_only();
+ test_rcpt_reject();
+ test_auth_plain();
+ test_auth_login();
+ test_auth_reject();
+ test_validation();
+ if (tls_available()) {
+ with_test_ca(tls_case);
+ with_test_ca(starttls_case);
+ } else {
+ printf("skip TLS cases: tests/tls_test_cert.pem missing\n");
+ }
+
+ if (failures) {
+ printf("%d failures\n", failures);
+ return 1;
+ }
+ puts("all ok");
+ return 0;
+}