/* GNU Mailutils -- a suite of utilities for electronic mail
Copyright (C) 2005-2021 Free Software Foundation, Inc.
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 3 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General
Public License along with this library. If not, see
. */
/* Syntax: vacation [:days ]
[:subject ]
[:aliases ]
[:noreply ]
[:reply_regex ]
[:reply_prefix ]
[:sender ]
[:database ]
[:rfc2822]
[:file]
[:mime]
[:always_reply]
[:return_address ]
[:header ]
*/
#ifdef HAVE_CONFIG_H
# include
#endif
#include
#include
#include
#include
#include
#include
#include
#include
/* Build a mime response message from original message MSG. TEXT
is the message text.
*/
static int
build_mime (mu_sieve_machine_t mach, mu_mime_t *pmime,
mu_message_t msg, const char *text)
{
mu_mime_t mime = NULL;
mu_message_t newmsg;
mu_stream_t stream, input;
mu_header_t hdr;
mu_body_t body;
const char *header =
"Content-Type: text/plain;charset=" MU_SIEVE_CHARSET "\n"
"Content-Transfer-Encoding: 8bit\n\n";
int rc;
mu_mime_create (&mime, NULL, 0);
mu_message_create (&newmsg, NULL);
mu_message_get_body (newmsg, &body);
if ((rc = mu_static_memory_stream_create (&input, text, strlen (text))))
{
mu_sieve_error (mach,
_("cannot create temporary stream: %s"),
mu_strerror (rc));
mu_mime_destroy (&mime);
mu_message_destroy (&newmsg, NULL);
return 1;
}
if (mu_sieve_get_tag (mach, "mime", SVT_VOID, NULL))
{
mu_stream_t fstr;
rc = mu_filter_create (&fstr, input, "base64",
MU_FILTER_ENCODE,
MU_STREAM_READ);
mu_stream_unref (input);
if (rc == 0)
{
header = "Content-Type: text/plain;charset=" MU_SIEVE_CHARSET "\n"
"Content-Transfer-Encoding: base64\n\n";
input = fstr;
}
}
rc = mu_body_get_streamref (body, &stream);
if (rc)
{
mu_sieve_error (mach,
_("cannot get input body stream: %s"),
mu_strerror (rc));
mu_mime_destroy (&mime);
mu_message_destroy (&newmsg, NULL);
mu_stream_destroy (&input);
return 1;
}
rc = mu_stream_copy (stream, input, 0, NULL);
mu_stream_destroy (&input);
mu_stream_destroy (&stream);
if (rc)
{
mu_sieve_error (mach,
_("stream copy failed: %s"),
mu_strerror (rc));
mu_mime_destroy (&mime);
mu_message_destroy (&newmsg, NULL);
return 1;
}
mu_header_create (&hdr, header, strlen (header));
mu_message_set_header (newmsg, hdr, NULL);
mu_mime_add_part (mime, newmsg);
mu_message_unref (newmsg);
*pmime = mime;
return 0;
}
/* Produce diagnostic output. */
static int
diag (mu_sieve_machine_t mach)
{
mu_sieve_log_action (mach, "VACATION", NULL);
return mu_sieve_is_dry_run (mach);
}
struct addr_data
{
mu_address_t addr;
char *my_address;
};
static int
_compare (void *item, void *data)
{
struct addr_data *ad = data;
int rc = mu_address_contains_email (ad->addr, item);
if (rc)
ad->my_address = item;
return rc;
}
/* Check whether an alias from ADDRESSES is part of To: or Cc: headers
of the originating mail. Return non-zero if so and store a pointer
to the matching address in *MY_ADDRESS. */
static int
match_addresses (mu_sieve_machine_t mach, mu_header_t hdr, char *email,
mu_sieve_value_t *addresses, char const **my_address)
{
int match = 0;
const char *str;
struct addr_data ad;
ad.my_address = NULL;
if (mu_header_sget_value (hdr, MU_HEADER_TO, &str) == 0)
{
if (!mu_address_create (&ad.addr, str))
{
if (_compare (email, &ad))
match = 1;
else if (addresses)
match += mu_sieve_vlist_do (mach, addresses, _compare, &ad);
mu_address_destroy (&ad.addr);
}
}
if (!match && mu_header_sget_value (hdr, MU_HEADER_CC, &str) == 0)
{
if (!mu_address_create (&ad.addr, str))
{
if (_compare (email, &ad))
match = 1;
else if (addresses)
match += mu_sieve_vlist_do (mach, addresses, _compare, &ad);
mu_address_destroy (&ad.addr);
}
}
*my_address = ad.my_address;
return match;
}
struct regex_data
{
mu_sieve_machine_t mach;
char *email;
};
static int
regex_comparator (void *item, void *data)
{
regex_t preg;
int rc;
struct regex_data *d = data;
if (regcomp (&preg, item,
REG_EXTENDED | REG_NOSUB | REG_NEWLINE | REG_ICASE))
{
mu_sieve_error (d->mach,
_("%lu: cannot compile regular expression \"%s\""),
(unsigned long) mu_sieve_get_message_num (d->mach),
(char*) item);
return 0;
}
rc = regexec (&preg, d->email, 0, NULL, 0) == 0;
regfree (&preg);
return rc;
}
/* Decide whether EMAIL address should not be responded to.
*/
static int
noreply_address_p (mu_sieve_machine_t mach, char *email)
{
int i, rc = 0;
mu_sieve_value_t *arg;
struct regex_data rd;
static char *noreply_sender[] = {
".*-REQUEST@.*",
".*-RELAY@.*",
".*-OWNER@.*",
"^OWNER-.*",
"^postmaster@.*",
"^UUCP@.*",
"^MAILER@.*",
"^MAILER-DAEMON@.*",
NULL
};
rd.mach = mach;
rd.email = email;
for (i = 0; rc == 0 && noreply_sender[i]; i++)
rc = regex_comparator (noreply_sender[i], &rd);
if (!rc && (arg = mu_sieve_get_tag_untyped (mach, "noreply")) != NULL)
rc = mu_sieve_vlist_do (mach, arg, regex_comparator, &rd);
return rc;
}
/* Return T if letter precedence is 'bulk' or 'junk' */
static int
bulk_precedence_p (mu_header_t hdr)
{
int rc = 0;
const char *str;
if (mu_header_sget_value (hdr, MU_HEADER_PRECEDENCE, &str) == 0)
{
rc = mu_c_strcasecmp (str, "bulk") == 0
|| mu_c_strcasecmp (str, "junk") == 0;
}
return rc;
}
#define DAYS_DEFAULT 7
#define DAYS_MAX 60
static int
test_and_update_prop (mu_property_t prop, const char *from,
time_t now, unsigned int days,
mu_sieve_machine_t mach)
{
const char *result;
char *timebuf;
time_t last;
int rc = mu_property_sget_value (prop, from, &result);
switch (rc)
{
case MU_ERR_NOENT:
break;
case 0:
if (days == 0)
return 1;
last = (time_t) strtoul (result, NULL, 0);
if (last + (24 * 60 * 60 * days) > now)
return 1;
break;
default:
mu_sieve_error (mach, "%lu: mu_property_sget_value: %s",
(unsigned long) mu_sieve_get_message_num (mach),
mu_strerror (rc));
return -1;
}
rc = mu_asprintf (&timebuf, "%lu", (unsigned long) now);
if (rc)
{
mu_sieve_error (mach, "%lu: mu_asprintf: %s",
(unsigned long) mu_sieve_get_message_num (mach),
mu_strerror (rc));
return -1;
}
rc = mu_property_set_value (prop, from, timebuf, 1);
free (timebuf);
if (rc)
{
mu_sieve_error (mach, "%lu: mu_property_set_value: %s",
(unsigned long) mu_sieve_get_message_num (mach),
mu_strerror (rc));
return -1;
}
rc = mu_property_save (prop);
if (rc)
{
mu_sieve_error (mach, "%lu: mu_property_save: %s",
(unsigned long) mu_sieve_get_message_num (mach),
mu_strerror (rc));
return -1;
}
return 0;
}
/* Check and update the vacation database. Return 0 if the mail should
be answered, 0 if it should not, and throw exception if an error
occurs. */
static int
check_db (mu_sieve_machine_t mach, char *from)
{
mu_property_t prop;
char *file;
unsigned int days;
int rc;
mu_stream_t str;
mu_locker_t locker;
const char *dbfile = "~/.vacation";
size_t n;
if (mu_sieve_get_tag (mach, "days", SVT_NUMBER, &n))
{
days = n;
if (days > DAYS_MAX)
days = DAYS_MAX;
}
else
days = DAYS_DEFAULT;
mu_sieve_get_tag (mach, "database", SVT_STRING, &dbfile);
file = mu_tilde_expansion (dbfile, MU_HIERARCHY_DELIMITER, NULL);
if (!file)
{
mu_sieve_error (mach, _("%lu: cannot build db file name"),
(unsigned long) mu_sieve_get_message_num (mach));
mu_sieve_abort (mach);
}
rc = mu_locker_create_ext (&locker, file, NULL);
if (rc)
{
mu_sieve_error (mach, _("%lu: cannot lock %s: %s"),
(unsigned long) mu_sieve_get_message_num (mach),
file,
mu_strerror (rc));
free (file);
mu_sieve_abort (mach);
}
rc = mu_file_stream_create (&str, file, MU_STREAM_RDWR|MU_STREAM_CREAT);
if (rc)
{
mu_sieve_error (mach, "%lu: mu_file_stream_create(%s): %s",
(unsigned long) mu_sieve_get_message_num (mach),
file,
mu_strerror (rc));
mu_locker_destroy (&locker);
free (file);
mu_sieve_abort (mach);
}
free (file);
rc = mu_property_create_init (&prop, mu_assoc_property_init, str);
if (rc)
{
mu_sieve_error (mach, "%lu: mu_property_create_init: %s",
(unsigned long) mu_sieve_get_message_num (mach),
mu_strerror (rc));
mu_locker_destroy (&locker);
mu_sieve_abort (mach);
}
rc = mu_locker_lock (locker);
if (rc)
{
mu_sieve_error (mach, "%lu: cannot lock vacation database: %s",
(unsigned long) mu_sieve_get_message_num (mach),
mu_strerror (rc));
mu_property_destroy (&prop);
mu_sieve_abort (mach);
}
rc = test_and_update_prop (prop, from, time (NULL), days, mach);
mu_property_destroy (&prop);
mu_locker_unlock (locker);
mu_locker_destroy (&locker);
if (rc == -1)
mu_sieve_abort (mach);
return rc;
}
/* Add a reply prefix to the subject. *PSUBJECT points to the
original subject, which must be allocated using malloc. Before
returning its value is freed and replaced with the new one.
Default reply prefix is "Re: ", unless overridden by
"reply_prefix" tag.
*/
static void
re_subject (mu_sieve_machine_t mach, char **psubject)
{
char *subject;
char *prefix = "Re";
mu_sieve_get_tag (mach, "reply_prefix", SVT_STRING, &prefix);
subject = malloc (strlen (*psubject) + strlen (prefix) + 3);
if (!subject)
{
mu_sieve_error (mach,
_("%lu: not enough memory"),
(unsigned long) mu_sieve_get_message_num (mach));
return;
}
strcpy (subject, prefix);
strcat (subject, ": ");
strcat (subject, *psubject);
free (*psubject);
*psubject = subject;
}
/* Process reply subject header.
If :subject is set, its value is used.
Otherwise, if original subject matches reply_regex, it is used verbatim.
Otherwise, reply_prefix is prepended to it. */
static void
vacation_subject (mu_sieve_machine_t mach,
mu_message_t msg, mu_header_t newhdr)
{
char *value;
char *subject;
int subject_allocated = 0;
mu_header_t hdr;
if (mu_sieve_get_tag (mach, "subject", SVT_STRING, &subject))
/* nothing */;
else if (mu_message_get_header (msg, &hdr) == 0
&& mu_header_aget_value_unfold (hdr, MU_HEADER_SUBJECT,
&value) == 0)
{
char *p;
int rc = mu_rfc2047_decode (MU_SIEVE_CHARSET, value, &p);
subject_allocated = 1;
if (rc)
{
subject = value;
value = NULL;
}
else
{
subject = p;
}
if (mu_sieve_get_tag (mach, "reply_regex", SVT_STRING, &p))
{
char *err = NULL;
rc = mu_unre_set_regex (p, 0, &err);
if (rc)
{
mu_sieve_error (mach,
_("%lu: cannot compile reply prefix regexp: %s: %s"),
(unsigned long) mu_sieve_get_message_num (mach),
mu_strerror (rc),
mu_prstr (err));
}
}
if (mu_unre_subject (subject, NULL))
re_subject (mach, &subject);
free (value);
}
else
subject = "Re: Your mail";
if (mu_rfc2047_encode (MU_SIEVE_CHARSET, "quoted-printable",
subject, &value))
mu_header_set_value (newhdr, MU_HEADER_SUBJECT, subject, 1);
else
{
mu_header_set_value (newhdr, MU_HEADER_SUBJECT, value, 1);
free (value);
}
if (subject_allocated)
free (subject);
}
static int
header_split (const char *str, char **hname, char **hval)
{
char *p, *q, *fn, *fv;
size_t n;
q = strchr (str, ':');
if (!q)
return MU_ERR_FORMAT;
for (p = q; p > str && mu_isspace (p[-1]); --p)
;
if (p == str)
return MU_ERR_FORMAT;
n = p - str;
fn = malloc (n + 1);
if (!fn)
return ENOMEM;
memcpy (fn, str, n);
fn[n] = 0;
for (++q; *q && mu_isspace (*q); ++q)
;
fv = strdup (q);
if (!fv)
{
free (fn);
return ENOMEM;
}
*hname = fn;
*hval = fv;
return 0;
}
struct header_closure
{
mu_sieve_machine_t mach;
mu_header_t hdr;
};
static int
add_header (void *item, void *data)
{
char const *str = item;
struct header_closure *hc = data;
char *fn, *fv;
int rc;
rc = header_split (str, &fn, &fv);
if (rc)
{
mu_sieve_error (hc->mach,
_("%lu: can't add header \"%s\": %s"),
(unsigned long) mu_sieve_get_message_num (hc->mach),
str, mu_strerror (rc));
return 0;
}
rc = mu_header_append (hc->hdr, fn, fv);
free (fn);
free (fv);
if (rc)
mu_sieve_error (hc->mach,
_("%lu: can't add header \"%s\": %s"),
(unsigned long) mu_sieve_get_message_num (hc->mach),
str, mu_strerror (rc));
return 0;
}
/* Generate and send the reply message */
static int
vacation_reply (mu_sieve_machine_t mach, mu_message_t msg,
char const *text, char const *to, char const *from)
{
mu_mime_t mime = NULL;
mu_message_t newmsg;
mu_header_t newhdr;
mu_address_t to_addr = NULL, from_addr = NULL;
char *value;
mu_mailer_t mailer;
int rc;
mu_sieve_value_t *val;
if (mu_sieve_get_tag (mach, "file", SVT_VOID, NULL))
{
mu_stream_t instr;
rc = mu_mapfile_stream_create (&instr, text, MU_STREAM_READ);
if (rc)
{
mu_sieve_error (mach,
_("%lu: cannot open message file %s: %s"),
(unsigned long) mu_sieve_get_message_num (mach),
text,
mu_strerror (rc));
return -1;
}
if (mu_sieve_get_tag (mach, "rfc2822", SVT_VOID, NULL))
{
rc = mu_stream_to_message (instr, &newmsg);
mu_stream_unref (instr);
if (rc)
{
mu_sieve_error (mach,
_("%lu: cannot read message from file %s: %s"),
(unsigned long) mu_sieve_get_message_num (mach),
text,
mu_strerror (rc));
return -1;
}
}
else
{
mu_stream_t text_stream;
mu_transport_t trans[2];
rc = mu_memory_stream_create (&text_stream, MU_STREAM_RDWR);
if (rc)
{
mu_stream_unref (instr);
mu_sieve_error (mach,
_("%lu: cannot create memory stream: %s"),
(unsigned long) mu_sieve_get_message_num (mach),
mu_strerror (rc));
return -1;
}
rc = mu_stream_copy (text_stream, instr, 0, NULL);
mu_stream_unref (instr);
if (rc == 0)
rc = mu_stream_write (text_stream, "", 1, NULL);
if (rc)
{
mu_sieve_error (mach,
_("%lu: failed reading from %s: %s"),
(unsigned long) mu_sieve_get_message_num (mach),
text,
mu_strerror (rc));
return -1;
}
rc = mu_stream_ioctl (text_stream, MU_IOCTL_TRANSPORT,
MU_IOCTL_OP_GET, trans);
if (rc)
{
mu_stream_unref (text_stream);
mu_sieve_error (mach,
"%lu: mu_stream_ioctl: %s",
(unsigned long) mu_sieve_get_message_num (mach),
mu_strerror (rc));
return -1;
}
if (build_mime (mach, &mime, msg, (char const *) trans[0]))
{
mu_stream_unref (text_stream);
return -1;
}
mu_mime_get_message (mime, &newmsg);
mu_message_unref (newmsg);
mu_stream_unref (text_stream);
}
}
else
{
if (build_mime (mach, &mime, msg, text))
return -1;
mu_mime_get_message (mime, &newmsg);
mu_message_unref (newmsg);
}
mu_message_get_header (newmsg, &newhdr);
rc = mu_address_create (&to_addr, to);
if (rc)
{
mu_sieve_error (mach,
_("%lu: cannot create recipient address <%s>: %s"),
(unsigned long) mu_sieve_get_message_num (mach),
from, mu_strerror (rc));
}
else
{
mu_header_set_value (newhdr, MU_HEADER_TO, to, 1);
val = mu_sieve_get_tag_untyped (mach, "header");
if (val)
{
struct header_closure hc;
hc.mach = mach;
hc.hdr = newhdr;
mu_sieve_vlist_do (mach, val, add_header, &hc);
}
vacation_subject (mach, msg, newhdr);
if (from)
{
if (mu_address_create (&from_addr, from))
from_addr = NULL;
}
else
{
from_addr = NULL;
}
if (mu_rfc2822_in_reply_to (msg, &value) == 0)
{
mu_header_set_value (newhdr, MU_HEADER_IN_REPLY_TO, value, 1);
free (value);
}
if (mu_rfc2822_references (msg, &value) == 0)
{
mu_header_set_value (newhdr, MU_HEADER_REFERENCES, value, 1);
free (value);
}
mailer = mu_sieve_get_mailer (mach);
if (mailer)
{
rc = mu_mailer_send_message (mailer, newmsg, from_addr, to_addr);
}
else
rc = MU_ERR_FAILURE;
}
mu_address_destroy (&to_addr);
mu_address_destroy (&from_addr);
mu_mime_destroy (&mime);
return rc;
}
int
sieve_action_vacation (mu_sieve_machine_t mach)
{
int rc;
char *text, *from = NULL;
char const *return_address;
mu_message_t msg;
mu_header_t hdr;
char *my_address;
if (diag (mach))
return 0;
mu_sieve_get_arg (mach, 0, SVT_STRING, &text);
msg = mu_sieve_get_message (mach);
mu_message_get_header (msg, &hdr);
if (mu_sieve_get_tag (mach, "sender", SVT_STRING, &from))
{
/* Debugging hook: :sender sets fake reply address */
from = strdup (from);
if (!from)
{
mu_sieve_error (mach, "%lu: %s",
(unsigned long) mu_sieve_get_message_num (mach),
mu_strerror (ENOMEM));
mu_sieve_abort (mach);
}
}
else if ((rc = mu_sieve_get_message_sender (msg, &from)) != 0)
{
mu_sieve_error (mach,
_("%lu: cannot get sender address: %s"),
(unsigned long) mu_sieve_get_message_num (mach),
mu_strerror (rc));
mu_sieve_abort (mach);
}
my_address = mu_get_user_email (NULL);
if (mu_sieve_get_tag (mach, "always_reply", SVT_VOID, NULL))
return_address = my_address;
else
{
mu_sieve_value_t *val = mu_sieve_get_tag_untyped (mach, "aliases");
if (match_addresses (mach, hdr, my_address, val, &return_address) == 0)
{
free (my_address);
return 0;
}
}
if (noreply_address_p (mach, from)
|| bulk_precedence_p (hdr)
|| check_db (mach, from))
{
free (from);
free (my_address);
return 0;
}
mu_sieve_get_tag (mach, "return_address", SVT_STRING,
&return_address);
rc = vacation_reply (mach, msg, text, from, return_address);
free (from);
free (my_address);
if (rc == -1)
mu_sieve_abort (mach);
return rc;
}
/* Tagged arguments: */
static mu_sieve_tag_def_t vacation_tags[] = {
{"days", SVT_NUMBER},
{"subject", SVT_STRING},
{"aliases", SVT_STRING_LIST},
{"noreply", SVT_STRING_LIST},
{"reply_regex", SVT_STRING},
{"reply_prefix", SVT_STRING},
{"sender", SVT_STRING},
{"database", SVT_STRING},
{"mime", SVT_VOID},
{"file", SVT_VOID},
{"always_reply", SVT_VOID},
{"return_address", SVT_STRING},
{"header", SVT_STRING_LIST},
{"rfc2822", SVT_VOID},
{NULL}
};
static mu_sieve_tag_group_t vacation_tag_groups[] = {
{vacation_tags, NULL},
{NULL}
};
/* Required arguments: */
static mu_sieve_data_type vacation_args[] = {
SVT_STRING, /* message text */
SVT_VOID
};
int SIEVE_EXPORT (vacation, init) (mu_sieve_machine_t mach)
{
mu_sieve_register_action (mach, "vacation", sieve_action_vacation,
vacation_args, vacation_tag_groups, 1);
return 0;
}