eMail notifications for reading and delivery

7 minute read

Inspired by Bob’s article on email reading notifications, I wanted to check out the details about all these notifications: reading, delivery and whatsoever.

But first of all, I do know that you can’t rely on any notifications for various reasons and that’s it’s therefore questionable if you should use them or not. But still, it’s interesting from a technical point of view.

Although I know that you can use mail on the command line with the -a option to set arbitrary header, I decided to use Perl with Net::SMTP. It gives me the flexibility I need and I can automate some parts of the jobs.

Delivery notification

I started to read RFC 3461: SMTP Service Extension for Delivery Status Notifications (DSN) which explains how to add DSN in EHLO replies. Requesting DSN with Net::SMTP is quite easy, you simply have to adjust the parameter of the perlto()-method.

my $smtp = Net::SMTP->new( Host => $data->{'Mailhost'}, Hello => $data->{'Maildomain'}, Timeout => $data->{'Timeout'}, Debug => 1 ) || die "unable to setup SMTP ($!)"; $smtp->to( @{ $data->{'To'} }, { Notify => ['FAILURE','DELAY', 'SUCCESS'], SkipBad => 1 } ); ``` This will request DSN in case of failure, delay and success if the server supports it. If you enabled the debugging as in the above example you get something like: ``` Net::SMTP=GLOB(0xe69390)<<< 220 mail.example.com ESMTP Postfix (Debian/GNU) Net::SMTP=GLOB(0xe69390)>>> EHLO mydomain.com Net::SMTP=GLOB(0xe69390)<<< 250-mail.example.com Net::SMTP=GLOB(0xe69390)<<< 250-PIPELINING Net::SMTP=GLOB(0xe69390)<<< 250-SIZE 40000000 Net::SMTP=GLOB(0xe69390)<<< 250-VRFY Net::SMTP=GLOB(0xe69390)<<< 250-ETRN Net::SMTP=GLOB(0xe69390)<<< 250-ENHANCEDSTATUSCODES Net::SMTP=GLOB(0xe69390)<<< 250-8BITMIME Net::SMTP=GLOB(0xe69390)<<< 250 DSN Net::SMTP=GLOB(0xa4fa30)>>> MAIL FROM:<user@example.com> Net::SMTP=GLOB(0xa4fa30)<<< 250 2.1.0 Ok Net::SMTP=GLOB(0xa4fa30)>>> RCPT TO:<user@example.com> NOTIFY=FAILURE,DELAY,SUCCESS ``` As you can see the server sends back the supported features and DSN is one of them. And in line 13 you see that the notification is requested by the client. (Chapter 4.1 in RFC 3461). So in this case you'll get a success notification. This is something on the server level that's independent of the mail user agent/mail client (MUA) someone uses. But what happens in case of relaying or the server doesn't support this feature? In case of relaying the DSN is passed to the next server to ensure that a DSN could be sent. Imagine the following scenario:
client --> local mail server --> relay mail server --> destination server
This setup isn't very unlikely especially for home boxes that have to use a relay server. What happens in such a case?
  • client checks and requests a DSN from the local mail server
  • DSN request is passed to the relay mail server by the local one
  • relay mail server passes the request to the destination server
In case any of these server don't support a DSN the last server in the chain that does will send out a DSN to the sender. This could lead to very irritating situations. Imagine the destination server doesn't support a DSN, hence the relay server will send one out, because the mail is given to the destination server. On the destination's server side the actual delivery fails. Now you'll proably get a delivery failure notification although you already have a success delivery notification... Weird.

Reading notification

However, this is the server side, but what about the client side? Besides the delivery notification I can request a reading notification. This is even more problematic since it depends on the actual MUA and its settings. Although the MUA supports this feature the user could have deactivated it or neglected to send out a notification. Besides all these pitfalls I want to know how it works in general. Again I started with RFC 3798: Message Disposition Notification (MDN). This is not on the SMTP protocoll level itself, but about defining a header that it part of the actual mail in fact (or SMTP payload if you like). The MDN is based on a new message header called Disposition-Notification-To. The content of the header is the email address to send the notification to. @smtp_header = ("From: $data->{From}", "To: $rcpt", "X-Mailer: $xmailer", "Subject: $data->{'Subject'}", ); push @smtp_header, "Reply-To: $replyto" if $replyto; push @smtp_header, "X-Confirm-Reading-To: $reading" if $reading; push @smtp_header, "Disposition-Notification-To: $delivery" if $delivery; my @smtp_body = join("\n", @{ $data->{'Data'} }); push @smtp_body, "\n\n" . "-- \n", $signature; $smtp->data([ join("\n", @smtp_header), "\n", @smtp_body ] ) || die "unable to send message ($!)\n"; ``` This is done in a script without much effort as you can see in line 168. I played with the X-Confirm-Reading-To header as well, but that was never standadized and was used before RFC 3798 was published. Find the complete code of my script below: ```perl #!/usr/local/bin/perl ### mail-disposition.pl --- ## Copyright (C) 2009 Thorsten Klein ## ## Author: cpan [at] perlwizard.de ## Version: $Id: mail-disposition.pl,v 0.0 2009/02/04 13:40:39 grobie Exp $ ## Keywords: Mail,SMTP,disposition,header ## Requirements: ## Status: not intended to be distributed yet ## This program is free software# you can redistribute it and/or modify ## it under the terms of the GNU General Public License as published by ## the Free Software Foundation# either version 2, or (at your option) ## any later version. ## ## This program 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 General Public License for more details. ## ## You should have received a copy of the GNU General Public License ## along with this program# if not, write to the Free Software ## Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. ### Commentary: ## Send an reading and disposition notification for a mail ## This is based on a script I found on the net ## (ToDo: add URL!) ### Code: use strict; use warnings; use Net::SMTP; use Getopt::Long qw(:config no_ignore_case bundling); use Data::Dumper; ############################################################################## #### User Options, Variables ############################################################################## use constant XMAILER => "Net::SMTP"; # UA-String. use constant TIMEOUT => 5; # Timeout. use constant MAILHOST => "mail.example.com"; # SMTP-Server. use constant MAILDOMAIN => "example.com"; # Domain. use constant MAILFROM => 'user@example.com'; # From: use constant MAILBACK => 'user@example.com'; # ReplyTo: my %OPT; # Options # h: help # V: version information # v: verbose # r: request reading confirmation # d: request delivery confirmation GetOptions(\%OPT, 'help|h|?', 'version|V', 'verbose|v+', 'delivery|d:s', 'reading|r:s', 'recipient|to|t=s', 'subject|s=s', 'sender|from|f=s', 'domain|maildomain=s', 'host|mailhost|h=s' ); ############################################################################## #### Main ############################################################################## die "No recipient given\n" unless $OPT{recipient}; my $mail_data = { #a typical mail structure Mailhost => $OPT{'host'}|| MAILHOST, # mail server to use? Maildomain => MAILDOMAIN, # HELO/EHLO mail domain Timeout => TIMEOUT, # server timeout. Mailer => XMAILER, # mail UA From => $OPT{'from'} || MAILFROM, # sender ReplyTo => MAILBACK, # reply-to header To => [ $OPT{'recipient'} ], # recipients list Subject => join(' ', $OPT{'subject'}), # subject Sigfile => $ENV{'HOME'} . "/.signature", # signature (if available) Data => [ contents() ] # read mail body }; $mail_data->{Reading} = $OPT{'reading'} if exists $OPT{'reading'}; # X-Confirm-Reading-To header $mail_data->{Delivery} = $OPT{'delivery'} if exists $OPT{'delivery'}; # Disposition-Notification-To send_mail($mail_data); #Let's go! ############################################################################## #### Subroutines ############################################################################## sub send_mail { my $data = shift; my @smtp_header = (); # check plausibility return unless $data->{'From'}; # no anonymous sender return unless $data->{'To'}->[0]; # no reciepient $data->{'Maildomain'} ||= MAILDOMAIN; # another mail domain? $data->{'Timeout'} ||= TIMEOUT; # different timeout $data->{'Subject'} ||= "(no subject)"; my $rcpt = join(',', @{ $data->{'To'} }); my $xmailer = $data->{'Mailer'} || XMAILER; my $signature = get_signature_from_file($data->{'Sigfile'}); my $replyto = $data->{'ReplyTo'} if $data->{'ReplyTo'} && $data->{'ReplyTo'} ne $data->{'From'}; my ($reading, $delivery); if ( exists $data->{'Reading'} ) { $reading = $data->{'Reading'}; $reading ||= $replyto; $reading ||= $data->{From}; } if ( exists $data->{'Delivery'} ) { $delivery = $data->{'Relivery'}; $delivery ||= $replyto; $delivery ||= $data->{From}; } # verbosity level > 1 will cause SMTP debug my $debug = exists $OPT{verbose} && $OPT{verbose} > 1 ? 1 : 0; print Dumper($data) if $OPT{verbose}; my $smtp = Net::SMTP->new(Host => $data->{'Mailhost'}, Hello => $data->{'Maildomain'}, Timeout => $data->{'Timeout'}, Debug => $debug ) || die "unable to setup SMTP ($!)"; $smtp->mail( $data->{'From'}, ) or return; # SMTP-envelope (not the header!) $smtp->to( @{ $data->{'To'} }, { Notify => ['FAILURE','DELAY', 'SUCCESS'], SkipBad => 1 } ) or return; @smtp_header = ("From: $data->{From}", "To: $rcpt", "X-Mailer: $xmailer", "Subject: $data->{'Subject'}", ); push @smtp_header, "Reply-To: $replyto" if $replyto; push @smtp_header, "X-Confirm-Reading-To: $reading" if $reading; push @smtp_header, "Disposition-Notification-To: $delivery" if $delivery; my @smtp_body = join("\n", @{ $data->{'Data'} }); push @smtp_body, "\n\n" . "-- \n", $signature; $smtp->data([ join("\n", @smtp_header), "\n", @smtp_body ] ) || die "unable to send message ($!)\n"; $smtp->quit() or return; print "mail sent.\n"; } sub contents { my @body; print "Please enter mail body:\n"; while () { chomp; return @body if $_ eq '.'; push(@body, $_); } return @body; } sub get_signature_from_file { my $fname = shift; #filename to find signature local $/ = 1; return unless ( -f $fname ); if ( open (SIGFILE, $fname) ) { my $sig = ; #load file close (SIGFILE); return $sig; } else { warn "can't open sigfile $fname: $!\n"; return; } } ### mail-disposition.pl ends here ```

Categories:

Updated: