2 # delay_mail delays mail and requeus it, and is released
3 # under the terms of the GPL version 2, or any later version, at your
4 # option. See the file README and COPYING for more information.
5 # Copyright 2006 by Don Armstrong <don@donarmstrong.com>.
6 # $Id: perl_script 495 2006-08-10 08:02:01Z don $
17 delay_mail - Delay mail to a specific time and send it back
21 delay_mail [options] < mail_message
24 --enqueue enqueues a message for sending out later
25 --delay length of delay (suitable for passing to at)
26 --email pull delay out from email address
27 --list list emails in queue
28 --queue directory to use as a queue
29 --process sends out a specific message that was enqueued
30 --debug, -d debugging level (Default 0)
31 --help, -h display this help
32 --man, -m display manual
40 Enqueue a message which should be sent out later
44 Length of delay (man at for details of specification; Debian systems
45 see /usr/share/doc/at/timespec)
49 The delay option is actually an email address; apply the following
52 $delay =~ m/[+-]d(?:ela?y?)?[-+]([^\@]+)/;
57 Thus, foo-delay-now+5_min@bar.baz becomes now+5 min and
58 foo-del+00=30=00@bar.baz becomes 00:30:00
62 List entries which are in the queue
66 Delete an entry from the queue
70 Who to mail the delayed mail to
74 The queue directory to use; defaults to ~/.delay_mail_queue
78 Process a specific entry in the queue (this is called by at at the
79 appropriate time; you shouldn't need call it manually.)
83 Debug verbosity. (Default 0)
87 Display brief useage information.
97 The following entry in your procmailrc (or similar) will do the dirty
101 * !Message-Id: .*delay[0-9]+@
103 |delay_mail --mailto you@foo.com --enqueue --email --delay $MATCH
111 will tell you that things are queued up and ready to go.
113 You can then bounce messages that you want to deal with later to
114 you+delay+10=30am_tomorrow@foo.com or similar, and you'll receive the
115 message again at 10:30 AM tomorrow to deal with.
122 my %options = (debug => 0,
128 GetOptions(\%options,'debug|d+','help|h|?','man|m',
129 'list|l','dequeue=s','process|p=s','enqueue|e',
131 'delay|D=s','email|E',
132 'mailto|mail-to|M=s',
135 pod2usage() if $options{help};
136 pod2usage({verbose=>2}) if $options{man};
138 $DEBUG = $options{debug};
140 use List::Util qw(sum);
141 use MIME::WordDecoder;
146 if (1 < grep {exists $options{$_}} qw(enqueue list process dequeue)) {
147 $ERROR .= "Exactly one of --enque, --list, --process, or --dequeue must be specified\n";
149 if (not $options{enqueue} and ($options{email} or exists $options{delay})) {
150 $ERROR .= "Setting email or delay is nonsensical unless enqueuing\n";
153 pod2usage($ERROR) if length $ERROR;
155 # create queue directory if it doesn't already exist
156 if (not exists $options{queue}) {
157 $options{queue} = "$ENV{HOME}/.delay_mail_queue";
159 if (not -d $options{queue}) {
160 mkdir($options{queue}) or
161 die "Unable to create queue directory $options{queue}: $!";
164 if (not exists $options{mailto}) {
165 if (exists $ENV{EMAIL}) {
166 $options{mailto} = $ENV{EMAIL};
168 elsif (exists $ENV{USER}) {
169 $options{mailto} = $ENV{USER};
172 $options{mailto} = qx(id -nu);
175 $options{mailto} =~ s/\n//g;
177 if (exists $options{enqueue}) {
179 my $delay = $options{delay};
181 if ($options{email}) {
182 $delay =~ m/[+-]d(?:ela?y?)?[-+]([^\@]+)/;
190 # rip subject out of email
191 # we cheat for now; this isn't correct at all.
192 my ($subject) = $email =~ /^Subject:\s*(.+)/mi;
193 $subject = decode_rfc1522($subject);
196 # create a queue entry
197 my $queue_fn = $time . $$;
198 my $q_fh = IO::File->new("$options{queue}/$queue_fn",'w') or
199 die "Unable to open $options{queue}/$queue_fn for writing";
200 print {$q_fh} "delay: $delay\n";
201 print {$q_fh} "time: $time\n";
202 print {$q_fh} "mailto: $options{mailto}\n";
203 print {$q_fh} "entry: $queue_fn\n";
204 print {$q_fh} "subject: $subject\n";
205 print {$q_fh} "#####\n";
206 print {$q_fh} $email;
208 my $pid = open($at_fh,'|-','at',$delay) or exit 1;
209 print {$at_fh} "$0 '--queue' '$options{queue}' '--process' '$queue_fn';\n";
210 close $at_fh or exit $?;
213 elsif ($options{list}) {
214 my $dir = IO::Dir->new($options{queue}) or
215 die "Unable to list contents of $options{queue}: $!";
218 while (defined($entry = $dir->read)) {
219 #valid queue entries are entirely numeric
220 print STDERR "Dealing with $entry\n" if $DEBUG;
221 last if not defined $entry;
222 next if $entry !~ /^\d+$/;
223 # they're also just readable files
224 next if not -f "$options{queue}/$entry" or not -r "$options{queue}/$entry";
225 print STDERR "Still dealing with $entry\n" if $DEBUG;
226 push @queue,parse_queue_entry($entry);
228 for my $q_e (@queue) {
230 print "$q_e->{entry}: send $q_e->{subject} to $q_e->{mailto} at $q_e->{delay} ($q_e->{time})\n";
233 elsif ($options{dequeue}) {
234 if (-e "$options{queue}/$options{dequeue}") {
235 unlink("$options{queue}/$options{dequeue}") or
236 die "Unable to unlink $options{queue}/$options{dequeue}";
239 print STDERR "$options{queue}/$options{dequeue} doesn't exist\n"
242 elsif ($options{process}) {
243 if (-e "$options{queue}/$options{process}") {
244 my $q_e = parse_queue_entry($options{process});
245 if (not defined $q_e) {
246 die "Unable to parse $options{process}";
248 # munge the message id
249 my ($message_id) = $q_e->{email} =~ m/^Message-Id:\s*(.+)/mi;
250 if (not $message_id =~ s/\@/delay$q_e->{entry}@/){
251 $message_id =~ s/(\w)/delay$q_e->{entry}$1/;
253 $q_e->{email} =~ s/^(Message-Id:\s*)(.+)/$1$message_id/mi;
256 open($sendmail_fh,'|-','/usr/sbin/sendmail',$q_e->{mailto}) or
257 die "Unable to open sendmail to send message";
258 print {$sendmail_fh} $q_e->{email};
259 close($sendmail_fh) or
260 print STDERR "Sendmail failed with $?\n" and
262 unlink("$options{queue}/$options{process}");
265 print STDERR "$options{queue}/$options{process} doesn't exist\n"
270 sub parse_queue_entry{
273 my $entry_fh = IO::File->new("$options{queue}/$entry",'r') or
275 my $queue_entry = {};
276 while (<$entry_fh>) {
280 my ($key,$value) = split /: /, $line,2;
281 $queue_entry->{$key} = $value;
284 $queue_entry->{email} = <$entry_fh>;
289 # These functions below I've jacked from Debbugs::MIME which I also
290 # wrote; probably should put them somewhere else eventually.
292 sub convert_to_utf8 {
293 my ($data, $charset) = @_;
294 # raw data just gets returned (that's the charset WordDecorder
295 # uses when it doesn't know what to do)
296 return $data if $charset eq 'raw' or is_utf8($data,1);
299 # this encode/decode madness is to make sure that the data
300 # really is valid utf8 and that the is_utf8 flag is off.
301 $result = encode("utf8",decode($charset,$data))
304 warn "Unable to decode charset; '$charset' and '$data': $@";
312 # Set up the default RFC1522 decoder, which turns all charsets that
313 # are supported into the appropriate UTF-8 charset.
314 MIME::WordDecoder->default(new MIME::WordDecoder(
315 ['*' => \&convert_to_utf8,
323 # this is craptacular, but leading space is hacked off by unmime.
325 my $leading_space = '';
326 $leading_space = $1 if $string =~ s/^(\s+)//;
327 # unmime calls the default MIME::WordDecoder handler set up at
328 # initialization time.
329 return $leading_space . MIME::WordDecoder::unmime($string);