1#!/usr/local/bin/perl
2
3#
4# Command line SMTP client with SSL, STARTTLS, SMTP-AUTH and IPv6 support.
5# Michal Ludvig <michal@logix.cz>, 2003-2013
6# See http://smtp-cli.logix.cz for details.
7# Thanks to all contributors for ideas and fixes!
8#
9
10my $version = "3.6";
11
12#
13# ChangeLog:
14# * Version 3.6    (2013-07-11)
15#   - Improved compatibility with perl < 5.10 and perl >= 5.18
16#   - Added support for more chars in user-part of email address.
17#
18# * Version 3.5    (2013-05-08)
19#   - Improved compliance with SMTP RFC 5321
20#   - New parameter --text-encoding
21#
22# * Version 3.4    (2013-02-05)
23#   - Ok, ok, support both File::Type and File::LibMagic
24#
25# * Version 3.3    (2012-07-30)
26#   - Moved from File::Type to File::LibMagic
27#     (File::Type is no longer maintained and not available
28#      in EPEL for RHEL 6)
29#
30# * Version 3.2    (2012-06-26)
31#   - Fixed syntax error
32#
33# * Version 3.1    (2012-06-25)
34#   - New --add-header, --replace-header and --remove-header options.
35#   - Improved compatibility with new IO::Socket::SSL releases.
36#
37# * Version 3.0    (2012-01-24)
38#   - Support for server SSL verification agains CA root cert.
39#   - Use "Content-Disposition: attachment" for all attachments
40#     unless --attach-inline was used.
41#   - No longer default to --server=localhost
42#   - Support for --charset=<charset> affecting all text/* parts.
43#   - Ensure "To: undisclosed-recipients:;" if sending only to Bcc.
44#
45# * Version 2.9    (2011-09-02)
46#   - Fixed problem when using IPv6 addresses with --server.
47#     For example with --server 2001:db8::123 it was connecting
48#     to server 2001:db8:: port 123. Fixed now.
49#
50# * Version 2.8    (2011-01-05)
51#   - Added --ssl to support for SSMTP (SMTP over SSL). This is
52#     turned on by default when --port=465.
53#
54# * Version 2.7    (2010-09-08)
55#   - Added support for Cc header (--cc=...)
56#   - Addressess (From, To, Cc) can now contain a "display name",
57#     for example --from="Michal Ludvig <michal@logix.cz>"
58#   - Support for --mail-from and --rcpt-to addresses independent
59#     on --from, --to, --cc and --bcc
60#   - Fixed warnings in Perl 5.12
61#
62# * Version 2.6    (2009-08-05)
63#   - Message building fixed for plaintext+attachment case.
64#   - Auto-enable AUTH as soon as --user parameter is used.
65#     (previously --enable-auth or --auth-plain had to be used
66#      together with --user, that was confusing).
67#   - New --print-only parameter for displaying the composed
68#     MIME message without sending.
69#   - All(?) non-standard modules are now optional.
70#   - Displays local and remote address on successfull connect.
71#
72# * Version 2.5    (2009-07-21)
73#   - IPv6 support provided the required modules are
74#     available.
75#
76# * Version 2.1    (2008-12-08)
77#   - Make the MIME modules optional. Simply disable
78#     the required functionality if they're not available.
79#
80# * Version 2.0    (2008-11-18)
81#   - Support for message building through MIME::Lite,
82#     including attachments, multipart, etc.
83#
84# * Version 1.1    (2006-08-26)
85#   - STARTTLS and AUTH support
86#
87# * Version 1.0
88#   - First public version
89#
90# This program is licensed under GNU Public License v3 (GPLv3)
91#
92
93## Require Perl 5.8 or higher -> we need open(.., .., \$variable) construct
94require 5.008;
95
96use strict;
97use IO::Socket::INET;
98use MIME::Base64 qw(encode_base64 decode_base64);
99use Getopt::Long;
100use Socket qw(:DEFAULT :crlf);
101
102my @valid_encodings = ("7bit", "8bit", "binary", "base64", "quoted-printable");
103
104my ($user, $pass, $host, $port, $addr_family,
105    $use_login, $use_plain, $use_cram_md5,
106    $ehlo_ok, $auth_ok, $starttls_ok, $ssl, $verbose,
107    $hello_host, $datasrc,
108    $mail_from, @rcpt_to, $from, @to, @cc, @bcc,
109    $missing_modules_ok, $missing_modules_count,
110    $subject, $body_plain, $body_html, $charset, $text_encoding, $print_only,
111    @attachments, @attachments_inline,
112    @add_headers, @replace_headers, @remove_headers,
113    $ssl_ca_file, $ssl_ca_path,
114    $sock, $built_message);
115
116$host = undef;
117$port = 'smtp(25)';
118$addr_family = AF_UNSPEC;
119$hello_host = 'localhost';
120$verbose = 0;
121$use_login = 0;
122$use_plain = 0;
123$use_cram_md5 = 0;
124$starttls_ok = 1;
125$ssl = undef;
126$auth_ok = 0;
127$ehlo_ok = 1;
128$missing_modules_ok = 0;
129$missing_modules_count = 0;
130$charset = undef;
131$text_encoding = "quoted-printable";
132$print_only = 0;
133
134# Get command line options.
135GetOptions (
136	'host|server=s' => \$host,
137	'port=i' => \$port,
138	'4|ipv4' => sub { $addr_family = AF_INET; },
139	'6|ipv6' => sub { $addr_family = AF_INET6; },
140	'user=s' => \$user, 'password=s' => \$pass,
141	'auth-login' => \$use_login,
142	'auth-plain' => \$use_plain,
143	'auth-cram-md5' => \$use_cram_md5,
144	'disable-ehlo' => sub { $ehlo_ok = 0; },
145	'force-ehlo' => sub { $ehlo_ok = 2; },
146	'hello-host|ehlo-host|helo-host=s' => \$hello_host,
147	'auth|enable-auth' => \$auth_ok,
148	'disable-starttls|disable-tls|disable-ssl' =>
149		sub { $starttls_ok = 0; },
150	'ssl' => sub { $ssl = 1 },
151	'disable-ssl' => sub { $ssl = 0 },
152	'mail-from=s' => \$mail_from,
153	'rcpt-to=s' => \@rcpt_to,
154	'from=s' => \$from,
155	'to=s' => \@to,
156	'cc=s' => \@cc,
157	'bcc=s' => \@bcc,
158	'data=s' => \$datasrc,
159	'subject=s' => \$subject,
160	'body|body-plain=s' => \$body_plain,
161	'body-html=s' => \$body_html,
162	'charset=s' => \$charset,
163	'text-encoding=s' => \$text_encoding,
164	'attachment|attach=s' => \@attachments,
165	'attachment-inline|attach-inline=s' => \@attachments_inline,
166	'add-header=s' => \@add_headers,
167	'replace-header=s' => \@replace_headers,
168	'remove-header=s' => \@remove_headers,
169	'print-only' => \$print_only,
170	'missing-modules-ok' => \$missing_modules_ok,
171	'ssl-ca-file=s' => \$ssl_ca_file,
172	'ssl-ca-path=s' => \$ssl_ca_path,
173	'v|verbose+' => \$verbose,
174	'version' => sub { &version() },
175	'help' => sub { &usage() } );
176
177#### Try to load optional modules
178
179## IO::Socket::SSL and Net::SSLeay are optional
180my $have_ssl = eval { require IO::Socket::SSL; require Net::SSLeay; 1; };
181if (not $have_ssl and not $missing_modules_ok) {
182	warn("!!! IO::Socket::SSL and/or Net::SSLeay modules are not found\n");
183	warn("!!! These modules are required for SSL and STARTTLS support\n");
184	$missing_modules_count += 2;
185}
186
187## IO::Socket::INET6 and Socket6 are optional
188my $socket6 = eval { require IO::Socket::INET6; require Socket6; 1; };
189if (not $socket6) {
190	if ($addr_family == AF_INET6) {
191		die("!!! IO::Socket::INET6 and Socket6 modules are not found\nIPv6 support is not available\n");
192	}
193	if (not $missing_modules_ok) {
194		warn("!!! IO::Socket::INET6 -- optional module not found\n");
195		warn("!!! Socket6 -- optional module not found\n");
196		warn("!!! These modules are required for IPv6 support\n\n");
197		$missing_modules_count += 2;
198	}
199}
200
201## MIME::Lite dependency is optional
202my $mime_lite = eval { require MIME::Lite; 1; };
203if (not $mime_lite and not $missing_modules_ok) {
204	warn("!!! MIME::Lite -- optional module not found\n");
205	warn("!!! Used for composing messages from --subject, --body, --attachment, etc.\n\n");
206	$missing_modules_count++;
207}
208
209## File::LibMagic dependency is optional
210my $file_libmagic = eval { require File::LibMagic; File::LibMagic->new(); };
211
212## File::Type dependency is optional
213## Not needed if File::LibMagic is available
214my $file_type = eval { require File::Type; File::Type->new(); };
215
216if (not $file_libmagic and not $file_type and not $missing_modules_ok) {
217	warn("!!! Neither File::LibMagic nor File::Type module found.\n");
218	warn("!!! Used for guessing MIME types of attachments. Optional.\n\n");
219	$missing_modules_count++;
220}
221
222## Term::ReadKey dependency is optional
223my $have_term_readkey = eval { require Term::ReadKey; 1; };
224if (not $have_term_readkey and not $missing_modules_ok) {
225	warn("!!! Term::ReadKey -- optional module not found\n");
226	warn("!!! Used for hidden reading SMTP password from the terminal\n\n");
227	$missing_modules_count++;
228}
229
230my $have_hmac_md5 = eval { require Digest::HMAC_MD5; 1; };
231if (not $have_hmac_md5 and not $missing_modules_ok) {
232	if ($use_cram_md5) {
233		die("!!! CRAM-MD5 authentication is not available because Digest::HMAC_MD5 module is missing\n");
234	}
235	warn("!!! Digest::HMAC_MD5 -- optional module missing\n");
236	warn("!!! Used for CRAM-MD5 authentication method\n");
237	$missing_modules_count++;
238}
239
240## Advise about --missing-modules-ok parameter
241if ($missing_modules_count) {
242	warn("!!! Use --missing-modules-ok if you don't need the above listed modules\n");
243	warn("!!! and don't want to see this message again.\n\n");
244}
245
246## Make sure we've got a server name to connect to
247if (not defined($host)) {
248	if (not $print_only) {
249		die("Error: Specify the SMTP server with --server=hostname[:port]\n");
250	} else {
251		# We're printing to stdout only, let's assign just about any
252		# hostname to satisfy the next few tests.
253		$host = "localhost";
254	}
255}
256
257## Make sure the --text-encoding value is valid
258if (not grep(/^$text_encoding$/, @valid_encodings))
259{
260	die ("The --text-encoding value is invalid: $text_encoding\nMust be one of: " . join(', ', @valid_encodings) . "\n");
261}
262
263## Accept hostname with port number as host:port
264## Either it's a hostname:port or 1.2.3.4:port or [2001:db8::1]:port.
265## Don't parse 2001:db8::1 as $host=2001:db8:: and $port=1!
266if (($host =~ /^([^:]+):([:alnum:]+)$/) or
267    ($host =~ /^\[([[:xdigit:]:]+)\]:([:alnum:]+)$/))
268{
269	$host = $1;
270	$port = $2;
271}
272
273## Automatically start in SSL mode if port == 465 (SSMTP)
274if (not defined($ssl)) {
275	$ssl = ($port == 465);
276}
277
278# Extract $mail_from address from $from
279if (not defined($mail_from) and defined($from)) {
280	$mail_from = &find_email_addr($from) or
281		die ("The --from string does not contain a valid email address: $from\n");
282}
283
284# Extract @rcpt_to list from @to, @cc and @bcc
285if (not @rcpt_to) {
286	foreach my $rcpt (@to, @cc, @bcc) {
287		my $rcpt_addr = &find_email_addr($rcpt);
288		if (not defined($rcpt_addr)) {
289			warn("No valid email address found in: $rcpt\n");
290			next;
291		}
292		push(@rcpt_to, $rcpt_addr);
293	}
294}
295
296# Ensure "To: undisclosed-recipients:;" when sending only to Bcc's
297if (not @to and not @cc) {
298	push(@to, "undisclosed-recipients:;");
299}
300
301# Build the MIME message if required
302if (defined($subject) or defined($body_plain) or defined($body_html) or
303		@attachments or @attachments_inline) {
304	if (not $mime_lite) {
305		die("Module MIME::Lite is not available. Unable to build the message, sorry.\n".
306		    "Use --data and provide a complete email payload including headers instead.\n");
307	}
308	if (defined($datasrc)) {
309		die("Requested building a message and at the same time used --data parameter.\n".
310		    "That's not possible, sorry.\n");
311	}
312	if (defined($body_plain) and -f $body_plain) {
313		local $/=undef;
314		open(FILE, $body_plain);
315		$body_plain = <FILE>;
316		close(FILE);
317	}
318	if (defined($body_html) and -f $body_html) {
319		local $/=undef;
320		open(FILE, $body_html);
321		$body_html = <FILE>;
322		close(FILE);
323	}
324	my $message = &build_message();
325
326	open(BUILT_MESSAGE, "+>", \$built_message);
327	$datasrc = "///built_message";
328	if ($print_only) {
329		$message->print();
330		exit(0);
331	} else {
332		$message->print(\*BUILT_MESSAGE);
333	}
334	seek(BUILT_MESSAGE, 0, 0);
335}
336
337# Username was given -> enable AUTH
338if ($user)
339	{ $auth_ok = 1; }
340
341# If at least one --auth-* option was given, enable AUTH.
342if ($use_login + $use_plain + $use_cram_md5 > 0)
343	{ $auth_ok = 1; }
344
345# If --enable-auth was given, enable all AUTH methods.
346elsif ($auth_ok && ($use_login + $use_plain + $use_cram_md5 == 0))
347{
348	$use_login = 1;
349	$use_plain = 1;
350	$use_cram_md5 = 1 if ($have_hmac_md5);
351}
352
353# Exit if user haven't specified username for AUTH.
354if ($auth_ok && !defined ($user))
355	{ die ("SMTP AUTH support requested without --user\n"); }
356
357# Ask for password if it wasn't supplied on the command line.
358if ($auth_ok && defined ($user) && !defined ($pass))
359{
360	if ($have_term_readkey) {
361		# Set echo off.
362		Term::ReadKey::ReadMode (2);
363	} else {
364		warn ("Module Term::ReadKey not available - password WILL NOT be hidden!!!\n");
365	}
366	printf ("Enter password for %s@%s : ", $user, $host);
367	$pass = <>;
368	if ($have_term_readkey) {
369		# Restore echo.
370		Term::ReadKey::ReadMode (0);
371		printf ("\n");
372	}
373	exit if (! defined ($pass));
374	chop ($pass);
375}
376
377# Connect to the SMTP server.
378my %connect_args = (
379	PeerAddr => $host,
380	PeerPort => $port,
381	Proto => 'tcp',
382	Timeout => 5);
383if ($socket6) {
384	$connect_args{'Domain'} = $addr_family;
385	$sock = IO::Socket::INET6->new(%connect_args) or die ("Connect failed: $@\n");
386} else {
387	$sock = IO::Socket::INET->new(%connect_args) or die ("Connect failed: $@\n");
388}
389
390if ($verbose >= 1) {
391	my $addr_fmt = "%s";
392	$addr_fmt = "[%s]" if ($sock->sockhost() =~ /:/); ## IPv6 connection
393
394	printf ("Connection from $addr_fmt:%s to $addr_fmt:%s\n",
395		$sock->sockhost(), $sock->sockport(),
396		$sock->peerhost(), $sock->peerport());
397}
398
399if ($ssl) {
400	printf ("Starting SMTP/SSL...\n") if ($verbose >= 1);
401	&socket_to_ssl($sock);
402}
403
404my ($code, $text);
405my (%features);
406
407# Wait for the welcome message of the server.
408($code, $text) = &get_line ($sock);
409die ("Unknown welcome string: '$code $text'\n") if ($code != 220);
410$ehlo_ok-- if ($text !~ /ESMTP/);
411
412# Send EHLO
413&say_hello ($sock, $ehlo_ok, $hello_host, \%features) or exit (1);
414
415# Run the SMTP session
416&run_smtp ();
417
418# Good bye...
419&send_line ($sock, "QUIT\n");
420($code, $text) = &get_line ($sock);
421die ("Unknown QUIT response '$code'.\n") if ($code != 221);
422
423exit 0;
424
425# This is the main SMTP "engine".
426sub run_smtp
427{
428	# See if we could start encryption
429	if ((defined ($features{'STARTTLS'}) || defined ($features{'TLS'})) && $starttls_ok && !$have_ssl)
430	{
431		warn ("Module IO::Socket::SSL is missing - STARTTLS support disabled.\n");
432		warn ("Use --disable-starttls or install the modules to avoid this warning.\n");
433		undef ($features{'STARTTLS'});
434		undef ($features{'TLS'});
435	}
436
437	if ((defined ($features{'STARTTLS'}) || defined ($features{'TLS'})) && $starttls_ok)
438	{
439		printf ("Starting TLS...\n") if ($verbose >= 1);
440
441		&send_line ($sock, "STARTTLS\n");
442		($code, $text) = &get_line ($sock);
443		die ("Unknown STARTTLS response '$code'.\n") if ($code != 220);
444
445		&socket_to_ssl($sock);
446
447		# Send EHLO again (required by the SMTP standard).
448		&say_hello ($sock, $ehlo_ok, $hello_host, \%features) or return 0;
449	}
450
451	# See if we should authenticate ourself
452	if (defined ($features{'AUTH'}) && $auth_ok)
453	{
454		printf ("AUTH method (%s): ", $features{'AUTH'}) if ($verbose >= 1);
455
456		## Try DIGEST-MD5 first
457		# Actually we won't. It never worked reliably here.
458		# After all DIGEST-MD5 is on a way to deprecation
459		# see this thread: http://www.imc.org/ietf-sasl/mail-archive/msg02996.html
460
461		# Instead use CRAM-MD5 if supported by the server
462		if ($features{'AUTH'} =~ /CRAM-MD5/i && $use_cram_md5)
463		{
464			printf ("using CRAM-MD5\n") if ($verbose >= 1);
465			&send_line ($sock, "AUTH CRAM-MD5\n");
466			($code, $text) = &get_line ($sock);
467			if ($code != 334)
468				{ die ("AUTH CRAM-MD5 failed: $code $text\n"); }
469
470			my $response = &encode_cram_md5 ($text, $user, $pass);
471			&send_line ($sock, "%s\n", $response);
472			($code, $text) = &get_line ($sock);
473			if ($code != 235)
474				{ die ("AUTH CRAM-MD5 failed: $code $text\n"); }
475		}
476		# Eventually try LOGIN method
477		elsif ($features{'AUTH'} =~ /LOGIN/i && $use_login)
478		{
479			printf ("using LOGIN\n") if ($verbose >= 1);
480			&send_line ($sock, "AUTH LOGIN\n");
481			($code, $text) = &get_line ($sock);
482			if ($code != 334)
483				{ die ("AUTH LOGIN failed: $code $text\n"); }
484
485			&send_line ($sock, "%s\n", encode_base64 ($user, ""));
486
487			($code, $text) = &get_line ($sock);
488			if ($code != 334)
489				{ die ("AUTH LOGIN failed: $code $text\n"); }
490
491			&send_line ($sock, "%s\n", encode_base64 ($pass, ""));
492
493			($code, $text) = &get_line ($sock);
494			if ($code != 235)
495				{ die ("AUTH LOGIN failed: $code $text\n"); }
496		}
497		# Or finally PLAIN if nothing else was supported.
498		elsif ($features{'AUTH'} =~ /PLAIN/i && $use_plain)
499		{
500			printf ("using PLAIN\n") if ($verbose >= 1);
501			&send_line ($sock, "AUTH PLAIN %s\n",
502				encode_base64 ("$user\0$user\0$pass", ""));
503			($code, $text) = &get_line ($sock);
504			if ($code != 235)
505				{ die ("AUTH PLAIN failed: $code $text\n"); }
506		}
507		# Complain otherwise.
508		else
509		{
510			warn ("No supported authentication method\n".
511			      "advertised by the server.\n");
512			return 0;
513		}
514
515		printf ("Authentication of $user\@$host succeeded\n") if ($verbose >= 1);
516	}
517
518	# We can do a relay-test now if a recipient was set.
519	if ($#rcpt_to >= 0)
520	{
521		if (!defined ($mail_from))
522		{
523			warn ("From: address not set. Using empty one.\n");
524			$mail_from = "";
525		}
526		&send_line ($sock, "MAIL FROM: <%s>\n", $mail_from);
527		($code, $text) = &get_line ($sock);
528		if ($code != 250)
529		{
530			warn ("MAIL FROM <$mail_from> failed: '$code $text'\n");
531			return 0;
532		}
533
534		my $i;
535		for ($i=0; $i <= $#rcpt_to; $i++)
536		{
537			&send_line ($sock, "RCPT TO: <%s>\n", $rcpt_to[$i]);
538			($code, $text) = &get_line ($sock);
539			if ($code != 250)
540			{
541				warn ("RCPT TO <".$rcpt_to[$i]."> ".
542				      "failed: '$code $text'\n");
543				return 0;
544			}
545		}
546	}
547
548	# Wow, we should even send something!
549	if (defined ($datasrc))
550	{
551		if ($datasrc eq "///built_message")
552		{
553			*MAIL = *BUILT_MESSAGE;
554		}
555		elsif ($datasrc eq "-")
556		{
557			*MAIL = *STDIN;
558		}
559		elsif (!open (MAIL, $datasrc))
560		{
561			warn ("Can't open file '$datasrc'\n");
562			return 0;
563		}
564
565		&send_line ($sock, "DATA\n");
566		($code, $text) = &get_line ($sock);
567		if ($code != 354)
568		{
569			warn ("DATA failed: '$code $text'\n");
570			return 0;
571		}
572
573		while (<MAIL>)
574		{
575			my $line = $_;
576			# RFC 5321 section 4.5.2 - leading dot must be doubled
577			$line =~ s/^\./\.\./;
578			# RFC 5321 section 2.3.8 - ensure CR-LF line ending
579			$line =~ s/[\r\n]+$/$CRLF/;
580			$sock->print ($line);
581		}
582
583		close (MAIL);
584
585		$sock->printf ("$CRLF.$CRLF");
586
587		($code, $text) = &get_line ($sock);
588		if ($code != 250)
589		{
590			warn ("DATA not send: '$code $text'\n");
591			return 0;
592		}
593	}
594
595	# Perfect. Everything succeeded!
596	return 1;
597}
598
599# Get one line of response from the server.
600sub get_one_line ($)
601{
602	my $sock = shift;
603	my ($code, $sep, $text) = ($sock->getline() =~ /(\d+)(.)([^\r]*)/);
604	my $more;
605	$more = ($sep eq "-");
606	if ($verbose)
607		{ printf ("[%d] '%s'\n", $code, $text); }
608	return ($code, $text, $more);
609}
610
611# Get concatenated lines of response from the server.
612sub get_line ($)
613{
614	my $sock = shift;
615	my ($code, $text, $more) = &get_one_line ($sock);
616	while ($more) {
617		my ($code2, $line);
618		($code2, $line, $more) = &get_one_line ($sock);
619		$text .= " $line";
620		die ("Error code changed from $code to $code2. That's illegal.\n") if ($code ne $code2);
621	}
622	return ($code, $text);
623}
624
625# Send one line back to the server
626sub send_line ($@)
627{
628	my $socket = shift;
629	my @args = @_;
630
631	if ($verbose)
632		{ printf ("> "); printf (@args); }
633	$args[0] =~ s/\n/$CRLF/g;
634	$socket->printf (@args);
635}
636
637sub socket_to_ssl($)
638{
639	if (!$have_ssl) {
640		die ("SSL/TLS support is not available due to missing modules. Sorry.\n");
641	}
642
643	# Do Net::SSLeay initialization
644	Net::SSLeay::load_error_strings();
645	Net::SSLeay::SSLeay_add_ssl_algorithms();
646	Net::SSLeay::randomize();
647
648	if (! IO::Socket::SSL->start_SSL($sock, {
649		SSL_ca_file => $ssl_ca_file,
650		SSL_ca_path => $ssl_ca_path,
651		SSL_verify_mode => (defined($ssl_ca_file) or defined($ssl_ca_path)) ? 0x01 : 0x00,
652	}))
653	{
654		die ("SSL/TLS: ".IO::Socket::SSL::errstr()."\n");
655	}
656
657	if ($verbose >= 1)
658	{
659		printf ("Using cipher: %s\n", $sock->get_cipher ());
660		printf ("%s", $sock->dump_peer_certificate());
661	}
662}
663
664# Helper function to encode CRAM-MD5 challenge
665sub encode_cram_md5 ($$$)
666{
667	my ($ticket64, $username, $password) = @_;
668	my $ticket = decode_base64($ticket64) or
669		die ("Unable to decode Base64 encoded string '$ticket64'\n");
670
671	print "Decoded CRAM-MD5 challenge: $ticket\n" if ($verbose > 1);
672	my $password_md5 = Digest::HMAC_MD5::hmac_md5_hex($ticket, $password);
673	return encode_base64 ("$username $password_md5", "");
674}
675
676# Store all server's ESMTP features to a hash.
677sub say_hello ($$$$)
678{
679	my ($sock, $ehlo_ok, $hello_host, $featref) = @_;
680	my ($feat, $param);
681	my $hello_cmd = $ehlo_ok > 0 ? "EHLO" : "HELO";
682
683	&send_line ($sock, "$hello_cmd $hello_host\n");
684	my ($code, $text, $more) = &get_one_line ($sock);
685
686	if ($code != 250)
687	{
688		warn ("$hello_cmd failed: '$code $text'\n");
689		return 0;
690	}
691
692	# Empty the hash
693	%{$featref} = ();
694
695	($feat, $param) = ($text =~ /^(\w+)[= ]*(.*)$/);
696	$featref->{$feat} = $param;
697
698	# Load all features presented by the server into the hash
699	while ($more == 1)
700	{
701		($code, $text, $more) = &get_one_line ($sock);
702		($feat, $param) = ($text =~ /^(\w+)[= ]*(.*)$/);
703		$featref->{$feat} = $param;
704	}
705
706	return 1;
707}
708
709sub find_email_addr($)
710{
711	my $addr = shift;
712	if ($addr =~ /([A-Z0-9._%=+-]+@(?:[A-Z0-9-]+\.)+[A-Z]+)\b/i) {
713		return $1;
714	}
715	return undef;
716}
717
718sub guess_mime_type($)
719{
720	my $filename = shift;
721	if (defined($file_libmagic)) {
722		## Use File::LibMagic if possible
723		return $file_libmagic->checktype_filename($filename);
724	} elsif (defined($file_type)) {
725		## Use File::Type if possible
726		return $file_type->mime_type($filename);
727	} else {
728		## Module File::LibMagic is not available
729		## Still recognise some common extensions
730		return "image/jpeg" if ($filename =~ /\.jpe?g/i);
731		return "image/gif" if ($filename =~ /\.gif/i);
732		return "image/png" if ($filename =~ /\.png/i);
733		return "text/plain" if ($filename =~ /\.txt/i);
734		return "application/zip" if ($filename =~ /\.zip/i);
735		return "application/x-gzip" if ($filename =~ /\.t?gz/i);
736		return "application/x-bzip" if ($filename =~ /\.t?bz2?/i);
737	}
738	return "application/octet-stream";
739}
740
741sub basename($)
742{
743	my $path = shift;
744	my @parts = split(/\//, $path);
745	return $parts[$#parts];
746}
747
748sub prepare_attachment($)
749{
750	my $attachment = shift;
751	my ($path, $mime_type);
752
753	if (-f $attachment) {
754		$path = $attachment;
755		$mime_type = guess_mime_type($attachment);
756	} elsif ($attachment =~ /(.*)@([^@]*)$/ and -f $1) {
757		$path = $1;
758		$mime_type = $2;
759	}
760	return ($path, $mime_type);
761}
762
763sub attach_attachments($$@)
764{
765	my $message = shift;
766	my $disposition = shift;
767	my @attachments = @_;
768
769	foreach my $attachment (@attachments) {
770		my ($path, $mime_type) = prepare_attachment($attachment);
771		if (not defined($path)) {
772			warn("$attachment: File not found. Ignoring.\n");
773			next;
774		}
775		$message->attach(
776			Type => $mime_type,
777			Path => $path,
778			Id   => basename($path),
779			Disposition => $disposition,
780		);
781	}
782}
783
784sub safe_attach($$)
785{
786	my ($message, $part) = @_;
787	## Remove some headers when $part is becoming a subpart of $message
788	$part->delete("Date");
789	$part->delete("X-Mailer");
790	$part->attr("MIME-Version" => undef);
791	$message->attach($part);
792	return $message;
793}
794
795sub mime_message($$)
796{
797	my ($type, $data) = @_;
798
799	## Set QP encoding for text/* types, let MIME::Lite decide for all other types.
800	my $encoding = $type =~ /^text\// ? $text_encoding : undef;
801	my $message = MIME::Lite->new(
802		Type	=> $type,
803		Encoding=> $encoding,
804		Data	=> $data);
805	$message->attr('content-type.charset' => $charset) if (($type =~ /^text\//i) and defined($charset));
806	return $message;
807}
808
809sub build_message
810{
811	my ($part_plain, $part_html, $part_body, $message);
812
813	if (@attachments_inline) {
814		if (not defined($body_html)) {
815			die("Inline attachments (--attach-inline) must be used with --body-html\n");
816		}
817		$part_html = MIME::Lite->new(Type => 'multipart/related');
818		$part_html->attach(Type => 'text/html', Data => $body_html);
819		attach_attachments($part_html, "inline", @attachments_inline);
820		$message = $part_html;
821		# undefine $body_html to prevent confusion in the next if()
822		undef($body_html);
823	}
824
825	if (defined($body_html)) {
826		$part_html = mime_message('text/html', $body_html);
827		$message = $part_html;
828	}
829
830	if (defined($body_plain)) {
831		$part_plain = mime_message('text/plain', $body_plain);
832		$message = $part_plain;
833	}
834
835	if (defined($part_plain) and defined($part_html)) {
836		$part_body = mime_message("multipart/alternative", undef);
837		safe_attach($part_body, $part_plain);
838		safe_attach($part_body, $part_html);
839		$message = $part_body;
840	}
841
842	if (@attachments) {
843		if (defined($message)) {
844			# We already have some plaintext and/or html content built
845			# => make it the first part of multipart/mixed
846			my $message_body = $message;
847			$message = mime_message("multipart/mixed", undef);
848			safe_attach($message, $message_body);
849			attach_attachments($message, "attachment", @attachments);
850		} elsif ($#attachments == 0) {
851			# Only one single attachment - let it be the body
852			my ($path, $mime_type) = prepare_attachment($attachments[0]);
853			if (not defined($path)) {
854				die($attachments[0].": File not found. No other message parts defined. Aborting.\n");
855			}
856			$message = MIME::Lite->new(
857				Type => $mime_type,
858				Path => $path);
859		} else {
860			# Message consisting only of attachments
861			$message = mime_message("multipart/mixed", undef);
862			attach_attachments($message, "attachment", @attachments);
863		}
864	}
865
866	# Last resort - empty plaintext message
867	if (!defined($message)) {
868		$message = mime_message("TEXT", "");
869	}
870
871	$message->replace("From" => $from);
872	$message->replace("To" => join(", ", @to));
873	$message->replace("Cc" => join(", ", @cc));
874	$message->replace("Subject" => $subject);
875	$message->replace("X-Mailer" => "smtp-cli $version, see http://smtp-cli.logix.cz");
876	$message->replace("Message-ID" => time()."-".int(rand(999999))."\@smtp-cli");
877
878	for my $header (@add_headers) {
879		my ($hdr, $val) = ($header =~ /^([^:]+):\s*(.*)$/);
880		die("Not a valid header format: ${header}\n") if (not $hdr or not $val);
881		$message->add($hdr => $val);
882	}
883	for my $header (@replace_headers) {
884		my ($hdr, $val) = ($header =~ /^([^:]+):\s*(.*)$/);
885		die("Not a valid header format: ${header}\n") if (not $hdr or not $val);
886		$message->replace($hdr => $val);
887	}
888	for my $header (@remove_headers) {
889		my ($hdr) = ($header =~ /^([^:\s]+)/);
890		$message->replace($header => "");
891	}
892
893	return $message;
894}
895
896sub version ()
897{
898	print "smtp-cli version $version\n";
899	exit (0);
900}
901
902sub usage ()
903{
904	printf (
905"Simple SMTP client written in Perl that supports advanced
906features like STARTTLS and SMTP-AUTH and IPv6. It can also
907create messages from components (files, text snippets) and
908attach files.
909
910Version: smtp-cli v$version
911
912Author: Michal Ludvig <mludvig\@logix.net.nz> (c) 2003-2011
913        http://smtp-cli.logix.cz
914
915Usage: smtp-cli [--options]
916
917        --server=<hostname>[:<port>]
918                                Host name or IP address of the SMTP server.
919                                May include the port after colon, alternatively
920                                use --port.
921        --port=<number>         Port where the SMTP server is listening.
922                                (default: 25)
923
924        -4 or --ipv4            Use standard IP (IPv4) protocol.
925        -6 or --ipv6            Use IPv6 protocol. For hosts that have
926                                both IPv6 and IPv4 addresses the IPv6
927                                connection is tried first.
928
929        --hello-host=<string>   String to use in the EHLO/HELO command.
930        --disable-ehlo          Don't use ESMTP EHLO command, only HELO.
931        --force-ehlo            Use EHLO even if server doesn't say ESMTP.
932
933        Transport encryption (TLS)
934        --disable-starttls      Don't use encryption even if the remote
935                                host offers it.
936        --ssl                   Start in SMTP/SSL mode (aka SSMTP).
937                                Default when --port=465
938        --disable-ssl           Don't start SSMTP even if --port=465
939        --ssl-ca-file=<filename>
940                                Verify the server's SSL certificate against
941                                a trusted CA root certificate file.
942        --ssl-ca-path=<dirname> Similar to --ssl-ca-file but will look for
943                                the appropriate root certificate file in
944                                the given directory. The certificates must
945                                must be stored one per file with hash-links
946                                generated by, for example, c_rehash script
947                                from OpenSSL.
948
949        Authentication options (AUTH)
950        --user=<username>       Username for SMTP authentication.
951        --pass=<password>       Corresponding password.
952        --auth-login            Enable only AUTH LOGIN method.
953        --auth-plain            Enable only AUTH PLAIN method.
954        --auth-cram-md5         Enable only AUTH CRAM-MD5 method.
955        --auth                  Enable all supported methods. This is
956                                normally not needed, --user enables
957                                everything as well.
958
959        Sender / recipient
960        --from=\"Display Name <add\@re.ss>\"
961                                Sender's name address (or address only).
962        --to=\"Display Name <add\@re.ss>\"
963        --cc=\"Display Name <add\@re.ss>\"
964        --bcc=\"Display Name <add\@re.ss>\"
965                                Message recipients. Each parameter can be
966                                used multiple times.
967                                The --bcc addresses won't apprear in
968                                the composed message.
969
970        SMTP Envelope sender / recipient
971        (rarely needed, use --from, --to, --cc and --bcc instead)
972        --mail-from=<address>   Address to use in MAIL FROM command.
973                                Use --from instead, unless you want
974                                a different address in the envelope and
975                                in the headers.
976        --rcpt-to=<address>     Address to use in RCPT TO command. Can be
977                                used multiple times. Normally not needed,
978                                use --to, --cc and --bcc instead.
979                                If set the --to, --cc and --bcc will only
980                                be used for composing the message body and
981                                not for delivering the messages.
982
983        Send a complete RFC822-compliant email message:
984        --data=<filename>       Name of file to send after DATA command.
985                                With \"--data=-\" the script will read
986                                standard input (useful e.g. for pipes).
987
988        Alternatively build email a message from provided components:
989        --subject=<subject>     Subject of the message
990        --body-plain=<text|filename>
991        --body-html=<text|filename>
992                                Plaintext and/or HTML body of the message
993                                If both are provided the message is sent
994                                as multipart.
995        --charset=<charset>     Character set used for Subject and Body,
996                                for example UTF-8, ISO-8859-2, KOI8-R, etc.
997        --text-encoding=<encoding>
998                                Enforce Content-Transfer-Encoding for text
999                                parts of the email, including body and
1000                                attachments. Must be one of:
1001                                ".join(", ", @valid_encodings)."
1002                                The default is: quoted-printable
1003        --attach=<filename>[\@<MIME/Type>]
1004                                Attach a given filename.
1005                                MIME-Type of the attachment is guessed
1006                                by default guessed but can optionally
1007                                be specified after '\@' delimiter.
1008                                For instance: --attach mail.log\@text/plain
1009                                Parameter can be used multiple times.
1010        --attach-inline=<filename>[\@<MIME/Type>]
1011                                Attach a given filename (typically a picture)
1012                                as a 'related' part to the above 'body-html'.
1013                                Refer to these pictures as <img src='cid:filename'>
1014                                in the 'body-html' contents.
1015                                See --attach for details about MIME-Type.
1016                                Can be used multiple times.
1017        --add-header=\"Header: value\"
1018        --replace-header=\"Header: value\"
1019        --remove-header=\"Header\"
1020                                Add, Replace or Remove pretty much any header
1021                                in the email. For example to set a different
1022                                Mailer use --replace-header=\"X-Mailer: Blah\",
1023                                to remove it altogether --remove-header=X-Mailer
1024                                or to add a completely custom header use
1025                                --add-header=\"X-Something: foo bar\".
1026        --print-only            Dump the composed MIME message to standard
1027                                output. This is useful mainly for debugging
1028                                or in the case you need to run the message
1029                                through some filter before sending.
1030
1031        Other options
1032        --verbose[=<number>]    Be more verbose, print the SMTP session.
1033        --missing-modules-ok    Don't complain about missing optional modules.
1034        --version               Print: smtp-cli version $version
1035        --help                  Guess what is this option for ;-)
1036
1037PayPal donations: http://smtp-cli.logix.cz/donate
1038                  Thanks in advance for your support!
1039
1040");
1041	exit (0);
1042}
1043
1044