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
```