This commit is contained in:
SJ 2011-11-22 12:31:54 +01:00
parent d7318f94cf
commit 7a0aa44ad9
14 changed files with 219 additions and 104 deletions

View File

@ -48,6 +48,9 @@ libpiler.a: $(OBJS) $(MYSQL_OBJS)
ln -sf libpiler.so.$(LIBPILER_VERSION) libpiler.so.$(PILER_VERSION)
pilerdecrypt: pilerdecrypt.c cfg.o misc.o tai.o store.o
$(CC) $(CFLAGS) $(INCDIR) $(DEFS) -o $@ $^ $(LIBS) $(LIBDIR)
pilerconf: pilerconf.c cfg.o misc.o tai.o
$(CC) $(CFLAGS) $(INCDIR) $(DEFS) -o $@ $^ $(LIBS) $(LIBDIR)

View File

@ -27,7 +27,7 @@ int store_attachments(struct session_data *sdata, struct _state *state, struct _
found = 0;
id = 0;
if(strlen(state->attachments[i].filename) > 4){
if(strlen(state->attachments[i].filename) > 4 && state->attachments[i].size > 10){
snprintf(s, sizeof(s)-1, "SELECT `id` FROM `%s` WHERE `sig`='%s'", SQL_ATTACHMENT_TABLE, state->attachments[i].digest);
@ -57,11 +57,6 @@ int store_attachments(struct session_data *sdata, struct _state *state, struct _
if(mysql_real_query(&(sdata->mysql), s, strlen(s))){
syslog(LOG_PRIORITY, "%s attachment sql error: *%s*", sdata->ttmpfile, mysql_error(&(sdata->mysql)));
if(found == 0){
/* TODO: remove the previously stored attachment */
}
return 1;
}

View File

@ -83,11 +83,11 @@ struct _state {
int line_num;
int message_state;
int is_header;
int is_1st_header;
int textplain;
int texthtml;
int message_rfc822;
int base64;
int has_base64;
int utf8;
int qp;
int htmltag;
@ -109,6 +109,9 @@ struct _state {
unsigned long n_body_token;
unsigned long n_chain_token;
char filename[TINYBUFSIZE];
char type[TINYBUFSIZE];
struct list *boundaries;
int n_attachments;
@ -127,6 +130,7 @@ struct session_data {
int need_scan;
float __acquire, __parsed, __av, __store, __compress, __encrypt;
char bodydigest[2*DIGEST_LENGTH+1];
char digest[2*DIGEST_LENGTH+1];
time_t now, sent;
#ifdef NEED_MYSQL
MYSQL mysql;

View File

@ -15,12 +15,12 @@
int make_body_digest(struct session_data *sdata, struct __config *cfg){
int i=0, n, fd;
char *p, *body=NULL;
int i=0, n, fd, hdr_len=0, offset=3;
char *body=NULL;
unsigned char buf[MAXBUFSIZE], md[DIGEST_LENGTH];
SHA256_CTX context;
if(cfg->verbosity >= _LOG_DEBUG) syslog(LOG_PRIORITY, "%s: digesting", sdata->ttmpfile);
//if(cfg->verbosity >= _LOG_DEBUG) syslog(LOG_PRIORITY, "%s: digesting", sdata->ttmpfile);
memset(sdata->bodydigest, 0, 2*DIGEST_LENGTH+1);
SHA256_Init(&context);
@ -31,23 +31,30 @@ int make_body_digest(struct session_data *sdata, struct __config *cfg){
while((n = read(fd, buf, MAXBUFSIZE)) > 0){
body = (char *)&buf[0];
i++;
if(i == 1){
p = strstr((char*)buf, "\n\n");
if(p){
body = p+2;
n = strlen(body);
} else {
p = strstr((char*)buf, "\n\r\n");
if(p){
body = p+3;
n = strlen(body);
}
if(i == 0){
hdr_len = searchStringInBuffer(body, MAXBUFSIZE, "\n\r\n", 3);
if(hdr_len == 0){
searchStringInBuffer(body, 2*MAXBUFSIZE+1, "\n\n", 2);
offset = 2;
}
if(hdr_len > 0){
hdr_len += offset;
sdata->hdr_len = hdr_len;
body += hdr_len;
n -= hdr_len;
if(cfg->verbosity >= _LOG_DEBUG) syslog(LOG_PRIORITY, "%s: hdr_len: %d, offset: %d", sdata->ttmpfile, hdr_len, offset);
}
}
SHA256_Update(&context, body, n);
i++;
}
close(fd);

View File

@ -145,7 +145,7 @@ int store_meta_data(struct session_data *sdata, struct _state *state, struct __c
goto ENDE_META;
}
snprintf(s, MAXBUFSIZE-1, "INSERT INTO %s (`from`,`to`,`subject`,`arrived`,`sent`,`size`,`hlen`,`piler_id`,`message_id`,`bodydigest`) VALUES(?,?,?,%ld,%ld,%d,%d,'%s',?,'%s')", SQL_METADATA_TABLE, sdata->now, sdata->sent, sdata->tot_len, sdata->hdr_len, sdata->ttmpfile, sdata->bodydigest);
snprintf(s, MAXBUFSIZE-1, "INSERT INTO %s (`from`,`to`,`subject`,`arrived`,`sent`,`size`,`hlen`,`attachments`,`piler_id`,`message_id`,`digest`,`bodydigest`) VALUES(?,?,?,%ld,%ld,%d,%d,%d,'%s',?,'%s','%s')", SQL_METADATA_TABLE, sdata->now, sdata->sent, sdata->tot_len, sdata->hdr_len, state->n_attachments, sdata->ttmpfile, sdata->digest, sdata->bodydigest);
if(cfg->verbosity >= _LOG_DEBUG) syslog(LOG_PRIORITY, "%s: meta sql: *%s*", sdata->ttmpfile, s);
@ -203,7 +203,6 @@ LABEL1:
rc = mysql_stmt_execute(stmt);
//rc = mysql_real_query(&(sdata->mysql), s, strlen(s));
if(rc){
syslog(LOG_PRIORITY, "%s: meta sql error: *%s*", sdata->ttmpfile, mysql_error(&(sdata->mysql)));
@ -249,12 +248,13 @@ int processMessage(struct session_data *sdata, struct _state *state, struct __co
if(store_attachments(sdata, state, cfg)) return ERR;
rc = store_attachments(sdata, state, cfg);
for(i=0; i<state->n_attachments; i++){
for(i=1; i<=state->n_attachments; i++){
unlink(state->attachments[i].internalname);
}
if(rc) return ERR;
rc = store_file(sdata, sdata->tmpframe, 0, 0, cfg);
if(rc == 0){

View File

@ -49,9 +49,10 @@ struct _state parse_message(struct session_data *sdata, struct __config *cfg){
free_list(state.boundaries);
for(i=0; i<state.n_attachments; i++){
for(i=1; i<=state.n_attachments; i++){
digest_file(state.attachments[i].internalname, &(state.attachments[i].digest[0]));
if(cfg->verbosity >= _LOG_DEBUG) syslog(LOG_PRIORITY, "%s: attachment list: i:%d, name=*%s*, type: *%s*, size: %d, int.name: %s, digest: %s\n", sdata->ttmpfile, i, state.attachments[i].filename, state.attachments[i].type, state.attachments[i].size, state.attachments[i].internalname, state.attachments[i].digest);
if(cfg->verbosity >= _LOG_DEBUG) syslog(LOG_PRIORITY, "%s: attachment list: i:%d, name=*%s*, type: *%s*, size: %d, int.name: %s, digest: %s", sdata->ttmpfile, i, state.attachments[i].filename, state.attachments[i].type, state.attachments[i].size, state.attachments[i].internalname, state.attachments[i].digest);
//printf("attachment list: i:%d, name=*%s*, type: *%s*, size: %d, int.name: %s, digest: %s\n", i, state.attachments[i].filename, state.attachments[i].type, state.attachments[i].size, state.attachments[i].internalname, state.attachments[i].digest);
}
@ -73,17 +74,20 @@ int parse_line(char *buf, struct _state *state, struct session_data *sdata, stru
char *p, *r, puf[SMALLBUFSIZE];
int x, len, b64_len, boundary_line=0;
state->line_num++;
len = strlen(buf);
if(state->message_state == MSG_BODY && state->has_to_dump == 1 && state->pushed_pointer == 0){
snprintf(puf, sizeof(puf)-1, "ATTACHMENT_POINTER_%s.%d\n", sdata->ttmpfile, state->n_attachments);
write(state->mfd, puf, strlen(puf));
state->pushed_pointer = 1;
//printf("buf: %s", buf);
if(state->message_rfc822 == 0 && (buf[0] == '\r' || buf[0] == '\n') ){
state->message_state = MSG_BODY;
if(state->is_header == 1) state->is_header = 0;
state->is_1st_header = 0;
}
if(state->message_state == MSG_BODY && state->has_to_dump == 1 && is_item_on_string(state->boundaries, buf) == 0){
if(state->message_state == MSG_BODY && state->fd != -1 && is_item_on_string(state->boundaries, buf) == 0){
//printf("dumping: %s", buf);
write(state->fd, buf, len);
state->attachments[state->n_attachments].size += len;
@ -95,6 +99,34 @@ int parse_line(char *buf, struct _state *state, struct session_data *sdata, stru
}
if(state->message_state == MSG_BODY && state->has_to_dump == 1 && state->pushed_pointer == 0){
//printf("####name: %s, type: %s, base64: %d\n", state->filename, state->type, state->base64);
state->pushed_pointer = 1;
// this is a real attachment to dump
if(state->base64 == 1 && strlen(state->filename) > 5 && strlen(state->type) > 3 && state->n_attachments < MAX_ATTACHMENTS-1){
state->n_attachments++;
snprintf(state->attachments[state->n_attachments].filename, TINYBUFSIZE-1, "%s", state->filename);
snprintf(state->attachments[state->n_attachments].type, TINYBUFSIZE-1, "%s", state->type);
snprintf(state->attachments[state->n_attachments].internalname, TINYBUFSIZE-1, "%s.a%d", sdata->ttmpfile, state->n_attachments);
//printf("DUMP FILE: %s\n", state->attachments[state->n_attachments].internalname);
state->fd = open(state->attachments[state->n_attachments].internalname, O_CREAT|O_RDWR, S_IRUSR|S_IWUSR);
snprintf(puf, sizeof(puf)-1, "ATTACHMENT_POINTER_%s.a%d", sdata->ttmpfile, state->n_attachments);
write(state->mfd, puf, strlen(puf));
//printf("%s", puf);
}
else {
state->has_to_dump = 0;
}
}
if(*buf == '.' && *(buf+1) == '.') buf++;
@ -104,10 +136,6 @@ int parse_line(char *buf, struct _state *state, struct session_data *sdata, stru
/* skip empty lines */
if(state->message_rfc822 == 0 && (buf[0] == '\r' || buf[0] == '\n') ){
state->message_state = MSG_BODY;
if(state->is_header == 1) state->is_header = 0;
return 0;
}
@ -168,9 +196,9 @@ int parse_line(char *buf, struct _state *state, struct session_data *sdata, stru
if(p){
p++;
if(*p == ' ' || *p == '\t') p++;
snprintf(state->attachments[state->n_attachments].type, TINYBUFSIZE-1, "%s", p);
snprintf(state->type, TINYBUFSIZE-1, "%s", p);
state->content_type_is_set = 1;
p = strchr(state->attachments[state->n_attachments].type, ';');
p = strchr(state->type, ';');
if(p) *p = '\0';
}
@ -202,34 +230,19 @@ int parse_line(char *buf, struct _state *state, struct session_data *sdata, stru
}
if(state->message_state == MSG_CONTENT_TYPE || state->message_state == MSG_CONTENT_DISPOSITION){
if(strlen(state->attachments[state->n_attachments].filename) < 5){
extractNameFromHeaderLine(buf, "name", state->attachments[state->n_attachments].filename);
snprintf(state->attachments[state->n_attachments].internalname, TINYBUFSIZE-1, "%s.a%d", sdata->ttmpfile, state->n_attachments);
}
if(strlen(state->attachments[state->n_attachments].filename) > 4 && state->has_to_dump == 0){
state->has_to_dump = 1;
//printf("DUMP FILE: %s\n", state->attachments[state->n_attachments].internalname);
state->fd = open(state->attachments[state->n_attachments].internalname, O_CREAT|O_RDWR, S_IRUSR|S_IWUSR);
}
if((state->message_state == MSG_CONTENT_TYPE || state->message_state == MSG_CONTENT_DISPOSITION) && strlen(state->filename) < 5){
extractNameFromHeaderLine(buf, "name", state->filename);
}
if(state->message_state == MSG_CONTENT_TRANSFER_ENCODING){
if(strcasestr(buf, "base64")){
state->base64 = 1;
state->has_base64 = 1;
}
if(strcasestr(buf, "base64")) state->base64 = 1;
if(strcasestr(buf, "quoted-printable")) state->qp = 1;
}
/* skip the boundary itself */
/* boundary check, and reset variables */
boundary_line = is_item_on_string(state->boundaries, buf);
@ -242,17 +255,7 @@ int parse_line(char *buf, struct _state *state, struct session_data *sdata, stru
}
if(state->n_attachments < MAX_ATTACHMENTS-1) state->n_attachments++;
/*
Use the previous attachment slot if there was not an attached file.
This is also the case if the filename field is empty.
*/
if(state->n_attachments > 0 && strlen(state->attachments[state->n_attachments-1].filename) < 5){
state->n_attachments--;
}
state->has_to_dump = 0;
state->has_to_dump = 1;
state->base64 = 0; state->textplain = 0; state->texthtml = state->octetstream = 0;
state->skip_html = 0;
@ -263,6 +266,11 @@ int parse_line(char *buf, struct _state *state, struct session_data *sdata, stru
state->pushed_pointer = 0;
memset(state->filename, 0, TINYBUFSIZE);
memset(state->type, 0, TINYBUFSIZE);
state->message_state = MSG_UNDEF;
return 0;
}
@ -339,13 +347,13 @@ int parse_line(char *buf, struct _state *state, struct session_data *sdata, stru
len = strlen(puf);
if(state->message_state == MSG_SUBJECT && strlen(state->b_subject) < MAXBUFSIZE-len-1)
if(state->message_state == MSG_SUBJECT && state->is_1st_header == 1 && strlen(state->b_subject) < MAXBUFSIZE-len-1)
memcpy(&(state->b_subject[strlen(state->b_subject)]), puf, len);
else if(state->message_state == MSG_FROM && strchr(puf, '@') && strlen(state->b_from) < SMALLBUFSIZE-len-1)
else if(state->message_state == MSG_FROM && strchr(puf, '@') && state->is_1st_header == 1 && state->b_from[0] == '\0' && strlen(state->b_from) < SMALLBUFSIZE-len-1)
memcpy(&(state->b_from[strlen(state->b_from)]), puf, len);
else if((state->message_state == MSG_TO || state->message_state == MSG_CC) && strchr(puf, '@') && strlen(state->b_to) < SMALLBUFSIZE-len-1)
else if((state->message_state == MSG_TO || state->message_state == MSG_CC) && state->is_1st_header == 1 && strchr(puf, '@') && strlen(state->b_to) < SMALLBUFSIZE-len-1)
memcpy(&(state->b_to[strlen(state->b_to)]), puf, len);
else if(state->message_state == MSG_BODY && strlen(state->b_body) < BIGBUFSIZE-len-1)

View File

@ -27,6 +27,7 @@ void init_state(struct _state *state){
state->line_num = 0;
state->is_header = 1;
state->is_1st_header = 1;
state->textplain = 1; /* by default we are a text/plain message */
state->texthtml = 0;
@ -47,18 +48,20 @@ void init_state(struct _state *state){
memset(state->message_id, 0, SMALLBUFSIZE);
memset(state->miscbuf, 0, MAX_TOKEN_LEN);
memset(state->filename, 0, TINYBUFSIZE);
memset(state->type, 0, TINYBUFSIZE);
state->has_to_dump = 0;
state->fd = -1;
state->mfd = -1;
state->realbinary = 0;
state->octetstream = 0;
state->pushed_pointer = 1;
state->pushed_pointer = 0;
state->saved_size = 0;
state->boundaries = NULL;
state->n_attachments = 0;
state->has_base64 = 0;
for(i=0; i<MAX_ATTACHMENTS; i++){
state->attachments[i].size = 0;

View File

@ -111,25 +111,6 @@ void initialise_configuration(){
}
int read_key(struct __config *cfg){
int fd, n;
fd = open(KEYFILE, O_RDONLY);
if(fd == -1){
syslog(LOG_PRIORITY, "cannot read keyfile: %s", KEYFILE);
return -1;
}
n = read(fd, cfg->key, KEYLEN);
close(fd);
if(n > 5) return 0;
return 1;
}
int main(int argc, char **argv){
int i, new_sd, yes=1, pid, daemonise=0;
unsigned int clen;

View File

@ -26,6 +26,8 @@
#include "memc.h"
#endif
int read_key(struct __config *cfg);
int do_av_check(struct session_data *sdata, char *rcpttoemail, char *fromemail, char *virusinfo, struct __data *data, struct __config *cfg);
int make_body_digest(struct session_data *sdata, struct __config *cfg);

74
src/pilerdecrypt.c Normal file
View File

@ -0,0 +1,74 @@
/*
* pilerdecrypt.c, SJ
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <sys/types.h>
#include <sys/mman.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <syslog.h>
#include <openssl/blowfish.h>
#include <openssl/evp.h>
#include <piler.h>
char *configfile = CONFIG_FILE;
int main(int argc, char **argv){
int fd, n, olen, tlen;
unsigned char inbuf[BIGBUFSIZE], outbuf[BIGBUFSIZE+EVP_MAX_BLOCK_LENGTH];
EVP_CIPHER_CTX ctx;
struct __config cfg;
cfg = read_config(configfile);
if(read_key(&cfg)){
printf("%s\n", ERR_READING_KEY);
return 1;
}
EVP_CIPHER_CTX_init(&ctx);
EVP_DecryptInit_ex(&ctx, EVP_bf_cbc(), NULL, cfg.key, cfg.iv);
if(argc != 2){
printf("usage: $0 <encrypted file>\n");
return 1;
}
fd = open(argv[1], O_RDONLY);
if(fd == -1){
printf("error reading file: %s\n", argv[0]);
return 1;
}
while((n = read(fd, inbuf, sizeof(inbuf)))){
bzero(&outbuf, sizeof(outbuf));
if(EVP_DecryptUpdate(&ctx, outbuf, &olen, inbuf, n) != 1){
return 0;
}
if(EVP_DecryptFinal(&ctx, outbuf + olen, &tlen) != 1){
return 0;
}
olen += tlen;
write(1, outbuf, olen);
}
EVP_CIPHER_CTX_cleanup(&ctx);
close(fd);
return 0;
}

View File

@ -85,10 +85,10 @@ void handle_smtp_session(int new_sd, struct __data *data, struct __config *cfg){
memcpy(last2buf+prevlen, puf, MAXBUFSIZE);
if(sdata.hdr_len == 0){
/*if(sdata.hdr_len == 0){
sdata.hdr_len = searchStringInBuffer(last2buf, 2*MAXBUFSIZE+1, "\n\r\n", 3);
if(sdata.hdr_len == 0) searchStringInBuffer(last2buf, 2*MAXBUFSIZE+1, "\n\n", 2);
}
}*/
pos = searchStringInBuffer(last2buf, 2*MAXBUFSIZE+1, SMTP_CMD_PERIOD, 5);
if(pos > 0){
@ -106,7 +106,7 @@ void handle_smtp_session(int new_sd, struct __data *data, struct __config *cfg){
/* fix posistion! */
pos += strlen(SMTP_CMD_PERIOD);
if(cfg->verbosity >= _LOG_DEBUG) syslog(LOG_PRIORITY, "%s: got: (.), header length=%d", sdata.ttmpfile, sdata.hdr_len);
if(cfg->verbosity >= _LOG_DEBUG) syslog(LOG_PRIORITY, "%s: got: (.)", sdata.ttmpfile);
state = SMTP_STATE_PERIOD;
@ -146,6 +146,7 @@ void handle_smtp_session(int new_sd, struct __data *data, struct __config *cfg){
sdata.need_scan = 1;
digest_file(sdata.ttmpfile, sdata.digest);
make_body_digest(&sdata, cfg);
#ifdef HAVE_ANTIVIRUS
@ -191,7 +192,7 @@ void handle_smtp_session(int new_sd, struct __data *data, struct __config *cfg){
arule = check_againt_ruleset(data->rules, sstate.b_from, sstate.b_to, sstate.b_subject, sdata.tot_len);
if(arule){
syslog(LOG_PRIORITY, "%s: discard message by policy: *%s*", sdata.ttmpfile, arule);
syslog(LOG_PRIORITY, "%s: discarding message by policy: *%s*", sdata.ttmpfile, arule);
inj = OK;
}
else {

View File

@ -19,6 +19,25 @@
#include <openssl/evp.h>
int read_key(struct __config *cfg){
int fd, n;
fd = open(KEYFILE, O_RDONLY);
if(fd == -1){
syslog(LOG_PRIORITY, "cannot read keyfile: %s", KEYFILE);
return -1;
}
n = read(fd, cfg->key, KEYLEN);
close(fd);
if(n > 5) return 0;
return 1;
}
int store_file(struct session_data *sdata, char *filename, int startpos, int len, struct __config *cfg){
int ret=0, rc, fd, n;
char *addr, *p, *p0, *p1, *p2, s[SMALLBUFSIZE];
@ -35,11 +54,15 @@ int store_file(struct session_data *sdata, char *filename, int startpos, int len
fd = open(filename, O_RDONLY);
if(fd == -1) return ret;
if(fd == -1){
syslog(LOG_PRIORITY, "%s: cannot open: %s", sdata->ttmpfile, filename);
return ret;
}
if(len == 0){
if(fstat(fd, &st)) return ret;
len = st.st_size;
if(len == 0) return 1;
}
gettimeofday(&tv1, &tz);
@ -55,6 +78,7 @@ int store_file(struct session_data *sdata, char *filename, int startpos, int len
if(z == NULL){
munmap(addr, len);
syslog(LOG_PRIORITY, "%s: cannot malloc for z buffer", sdata->ttmpfile);
return ret;
}
@ -116,13 +140,20 @@ int store_file(struct session_data *sdata, char *filename, int startpos, int len
*p0 = '/';
fd = open(s, O_CREAT|O_RDWR, S_IRUSR|S_IWUSR|S_IRGRP);
if(fd == -1) goto ENDE;
if(fd == -1){
syslog(LOG_PRIORITY, "%s: cannot open: %s", sdata->ttmpfile, s);
goto ENDE;
}
n = write(fd, outbuf, outlen);
if(n == outlen){
ret = 1;
}
else {
syslog(LOG_PRIORITY, "%s: cannot write %d bytes (only %d)", sdata->ttmpfile, outlen, n);
}
fsync(fd);

View File

@ -16,7 +16,7 @@
int main(int argc, char **argv){
int rc;
int i, rc;
struct stat st;
struct session_data sdata;
struct _state state;
@ -49,7 +49,6 @@ int main(int argc, char **argv){
data.rules = NULL;
load_archiving_rules(&sdata, &(data.rules));
rc = 0;
@ -62,6 +61,7 @@ int main(int argc, char **argv){
snprintf(sdata.ttmpfile, SMALLBUFSIZE-1, "%s", argv[1]);
snprintf(sdata.tmpframe, SMALLBUFSIZE-1, "%s.m", argv[1]);
state = parse_message(&sdata, &cfg);
printf("message-id: %s\n", state.message_id);
@ -73,14 +73,18 @@ int main(int argc, char **argv){
make_body_digest(&sdata, &cfg);
rule = check_againt_ruleset(data.rules, state.b_from, state.b_to, state.b_subject, st.st_size);
printf("body digest: %s\n", sdata.bodydigest);
//printf("body digest: %s\n", sdata.bodydigest);
printf("rules check: %s\n", rule);
//printf("rules check: %s\n", rule);
mysql_close(&(sdata.mysql));
free_rule(data.rules);
for(i=1; i<=state.n_attachments; i++){
printf("i:%d, name=*%s*, type: *%s*, size: %d, int.name: %s, digest: %s\n", i, state.attachments[i].filename, state.attachments[i].type, state.attachments[i].size, state.attachments[i].internalname, state.attachments[i].digest);
}
printf("\n\n");
return 0;

View File

@ -32,8 +32,10 @@ create table `metadata` (
`deleted` int default 0,
`size` int default 0,
`hlen` int default 0,
`attachments` int default 0,
`piler_id` char(36) not null,
`message_id` char(128) character set 'latin1' not null,
`digest` char(64) not null,
`bodydigest` char(64) not null,
primary key (`id`), unique(`to`,`message_id`)
) Engine=InnoDB;