]> git.sur5r.net Git - bacula/bacula/blob - bacula/src/tools/bsmtp.c
4ca89a22da0adb20108d87f11480365296021041
[bacula/bacula] / bacula / src / tools / bsmtp.c
1 /*
2    Bacula® - The Network Backup Solution
3
4    Copyright (C) 2001-2009 Free Software Foundation Europe e.V.
5
6    The main author of Bacula is Kern Sibbald, with contributions from
7    many others, a complete list can be found in the file AUTHORS.
8    This program is Free Software; you can redistribute it and/or
9    modify it under the terms of version three of the GNU Affero General Public
10    License as published by the Free Software Foundation and included
11    in the file LICENSE.
12
13    This program is distributed in the hope that it will be useful, but
14    WITHOUT ANY WARRANTY; without even the implied warranty of
15    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16    General Public License for more details.
17
18    You should have received a copy of the GNU Affero General Public License
19    along with this program; if not, write to the Free Software
20    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
21    02110-1301, USA.
22
23    Bacula® is a registered trademark of Kern Sibbald.
24    The licensor of Bacula is the Free Software Foundation Europe
25    (FSFE), Fiduciary Program, Sumatrastrasse 25, 8006 Zürich,
26    Switzerland, email:ftf@fsfeurope.org.
27 */
28 /*
29    Derived from a SMTPclient:
30
31   ======== Original copyrights ==========  
32
33        SMTPclient -- simple SMTP client
34
35        Copyright (c) 1997 Ralf S. Engelschall, All rights reserved.
36
37        This program is free software; it may be redistributed and/or modified
38        only under the terms of either the Artistic License or the GNU General
39        Public License, which may be found in the SMTP source distribution.
40        Look at the file COPYING.
41
42        This program is distributed in the hope that it will be useful, but
43        WITHOUT ANY WARRANTY; without even the implied warranty of
44        MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
45        GNU General Public License for more details.
46
47        ======================================================================
48
49        smtpclient_main.c -- program source
50
51        Based on smtp.c as of August 11, 1995 from
52            W.Z. Venema,
53            Eindhoven University of Technology,
54            Department of Mathematics and Computer Science,
55            Den Dolech 2, P.O. Box 513, 5600 MB Eindhoven, The Netherlands.
56
57    =========
58
59
60    Kern Sibbald, July 2001
61
62      Note, the original W.Z. Venema smtp.c had no license and no
63      copyright.  See:
64         http://archives.neohapsis.com/archives/postfix/2000-05/1520.html
65  
66  */
67
68 #include "bacula.h"
69 #include "jcr.h"
70 #define MY_NAME "bsmtp"
71
72 #if defined(HAVE_WIN32)
73 #include <lmcons.h>
74 #endif
75
76 /*
77  * Dummy functions
78  */
79 int generate_daemon_event(JCR *jcr, const char *event) 
80 {
81    return 1;
82 }
83
84 #ifndef MAXSTRING
85 #define MAXSTRING 254
86 #endif
87
88 enum resolv_type {
89    RESOLV_PROTO_ANY,
90    RESOLV_PROTO_IPV4,
91    RESOLV_PROTO_IPV6
92 };
93
94 static FILE *sfp;
95 static FILE *rfp;
96
97 static char *from_addr = NULL;
98 static char *cc_addr = NULL;
99 static char *subject = NULL;
100 static char *err_addr = NULL;
101 static const char *mailhost = NULL;
102 static char *reply_addr = NULL;
103 static int mailport = 25;
104 static char my_hostname[MAXSTRING];
105 static bool content_utf8 = false;
106 static resolv_type default_resolv_type = RESOLV_PROTO_IPV4;
107
108 /* 
109  * Take input that may have names and other stuff and strip
110  *  it down to the mail box address ... i.e. what is enclosed
111  *  in < >.  Otherwise add < >.
112  */
113 static char *cleanup_addr(char *addr, char *buf, int buf_len)
114 {
115    char *p, *q;
116
117    if ((p = strchr(addr, '<')) == NULL) {
118       snprintf(buf, buf_len, "<%s>", addr);
119    } else {
120       /* Copy <addr> */
121       for (q=buf; *p && *p!='>'; ) {
122          *q++ = *p++;
123       }
124       if (*p) {
125          *q++ = *p;
126       }
127       *q = 0;
128   }
129   Dmsg2(100, "cleanup in=%s out=%s\n", addr, buf);
130   return buf;    
131 }
132
133 /*
134  *  examine message from server
135  */
136 static void get_response(void)
137 {
138     char buf[1000];
139
140     Dmsg0(50, "Calling fgets on read socket rfp.\n");
141     buf[3] = 0;
142     while (fgets(buf, sizeof(buf), rfp)) {
143         int len = strlen(buf);
144         if (len > 0) {
145            buf[len-1] = 0;
146         }
147         if (debug_level >= 10) {
148             fprintf(stderr, "%s <-- %s\n", mailhost, buf);
149         }
150         Dmsg2(10, "%s --> %s\n", mailhost, buf);
151         if (!isdigit((int)buf[0]) || buf[0] > '3') {
152             Pmsg2(0, _("Fatal malformed reply from %s: %s\n"), mailhost, buf);
153             exit(1);
154         }
155         if (buf[3] != '-') {
156             break;
157         }
158     }
159     if (ferror(rfp)) {
160         fprintf(stderr, _("Fatal fgets error: ERR=%s\n"), strerror(errno));
161     }
162     return;
163 }
164
165 /*
166  *  say something to server and check the response
167  */
168 static void chat(const char *fmt, ...)
169 {
170     va_list ap;
171
172     va_start(ap, fmt);
173     vfprintf(sfp, fmt, ap);
174     va_end(ap);
175     if (debug_level >= 10) {
176        fprintf(stdout, "%s --> ", my_hostname);
177        va_start(ap, fmt);
178        vfprintf(stdout, fmt, ap);
179        va_end(ap);
180     }
181
182     fflush(sfp);
183     if (debug_level >= 10) {
184        fflush(stdout);
185     }
186     get_response();
187 }
188
189
190 static void usage()
191 {
192    fprintf(stderr,
193 _("\n"
194 "Usage: %s [-f from] [-h mailhost] [-s subject] [-c copy] [recipient ...]\n"
195 "       -4          forces bsmtp to use IPv4 addresses only.\n"
196 #ifdef HAVE_IPV6
197 "       -6          forces bsmtp to use IPv6 addresses only.\n"
198 #endif
199 "       -8          set charset to UTF-8\n"
200 "       -a          use any ip protocol for address resolution\n"
201 "       -c          set the Cc: field\n"
202 "       -d <nn>     set debug level to <nn>\n"
203 "       -dt         print a timestamp in debug output\n"
204 "       -f          set the From: field\n"
205 "       -h          use mailhost:port as the SMTP server\n"
206 "       -s          set the Subject: field\n"
207 "       -r          set the Reply-To: field\n"
208 "       -l          set the maximum number of lines to send (default: unlimited)\n"
209 "       -?          print this message.\n"
210 "\n"), MY_NAME);
211
212    exit(1);
213 }
214
215 /*
216  * Return the offset west from localtime to UTC in minutes
217  * Same as timezone.tz_minuteswest
218  *   Unix tz_offset coded by:  Attila Fülöp
219  */
220 static long tz_offset(time_t lnow, struct tm &tm)
221 {
222 #if defined(HAVE_WIN32)
223 #if defined(HAVE_MINGW)
224 __MINGW_IMPORT long     _dstbias;
225 #endif
226 #if defined(MINGW64)
227 # define _tzset tzset
228 #endif
229    /* Win32 code */
230    long offset;
231    _tzset();
232    offset = _timezone;
233    if (tm.tm_isdst) {
234       offset += _dstbias;
235    }
236    return offset /= 60;
237 #else
238
239    /* Unix/Linux code */
240    struct tm tm_utc;
241    time_t now = lnow;
242
243    (void)gmtime_r(&now, &tm_utc);
244    tm_utc.tm_isdst = tm.tm_isdst;
245    return (long)difftime(mktime(&tm_utc), now) / 60;
246 #endif
247 }
248
249 static void get_date_string(char *buf, int buf_len)
250 {
251    time_t now = time(NULL);
252    struct tm tm;
253    char tzbuf[MAXSTRING];
254    long my_timezone;
255
256    /* Add RFC822 date */
257    (void)localtime_r(&now, &tm);
258
259    my_timezone = tz_offset(now, tm);
260    strftime(buf, buf_len, "%a, %d %b %Y %H:%M:%S", &tm);
261    snprintf(tzbuf, sizeof(tzbuf), " %+2.2ld%2.2u", -my_timezone / 60, abs(my_timezone) % 60);
262    strcat(buf, tzbuf);              /* add +0100 */
263    strftime(tzbuf, sizeof(tzbuf), " (%Z)", &tm);
264    strcat(buf, tzbuf);              /* add (CEST) */
265 }
266
267 /*********************************************************************
268  *
269  *  Program to send email
270  */
271 int main (int argc, char *argv[])
272 {
273    char buf[1000];
274    int i, ch;
275    unsigned long maxlines, lines;
276 #if defined(HAVE_WIN32)
277    SOCKET s;
278 #else
279    int s, r;
280    struct passwd *pwd;
281 #endif
282    char *cp, *p;
283 #ifdef HAVE_GETADDRINFO
284    int res;
285    struct addrinfo hints;
286    struct addrinfo *ai, *rp;
287    char mail_port[10];
288 #else
289    struct hostent *hp;
290    struct sockaddr_in sin;
291 #endif
292 #ifdef HAVE_IPV6
293    const char *options = "468ac:d:f:h:r:s:l:?";
294 #else
295    const char *options = "48ac:d:f:h:r:s:l:?";
296 #endif
297     
298    setlocale(LC_ALL, "en_US");
299    bindtextdomain("bacula", LOCALEDIR);
300    textdomain("bacula");
301
302    my_name_is(argc, argv, "bsmtp");
303    maxlines = 0;
304
305    while ((ch = getopt(argc, argv, options)) != -1) {
306       switch (ch) {
307       case '4':
308          default_resolv_type = RESOLV_PROTO_IPV4;
309          break;
310
311 #ifdef HAVE_IPV6
312       case '6':
313          default_resolv_type = RESOLV_PROTO_IPV6;
314          break;
315 #endif
316
317       case '8':
318          content_utf8 = true;
319          break;
320
321       case 'a':
322          default_resolv_type = RESOLV_PROTO_ANY;
323          break;
324
325       case 'c':
326          Dmsg1(20, "cc=%s\n", optarg);
327          cc_addr = optarg;
328          break;
329
330       case 'd':                    /* set debug level */
331          if (*optarg == 't') {
332             dbg_timestamp = true;
333          } else {
334             debug_level = atoi(optarg);
335             if (debug_level <= 0) {
336                debug_level = 1;
337             }
338          }
339          Dmsg1(20, "Debug level = %d\n", debug_level);
340          break;
341
342       case 'f':                    /* from */
343          from_addr = optarg;
344          break;
345
346       case 'h':                    /* smtp host */
347          Dmsg1(20, "host=%s\n", optarg);
348          p = strchr(optarg, ':');
349          if (p) {
350             *p++ = 0;
351             mailport = atoi(p);
352          }
353          mailhost = optarg;
354          break;
355
356       case 's':                    /* subject */
357          Dmsg1(20, "subject=%s\n", optarg);
358          subject = optarg;
359          break;
360
361       case 'r':                    /* reply address */
362          reply_addr = optarg;
363          break;
364
365       case 'l':
366          Dmsg1(20, "maxlines=%s\n", optarg);
367          maxlines = (unsigned long) atol(optarg);
368          break;
369
370       case '?':
371       default:
372          usage();
373
374       }
375    }
376    argc -= optind;
377    argv += optind;
378
379    if (argc < 1) {
380       Pmsg0(0, _("Fatal error: no recipient given.\n"));
381       usage();
382       exit(1);
383    }
384
385    /*
386     *  Determine SMTP server
387     */
388    if (mailhost == NULL) {
389       if ((cp = getenv("SMTPSERVER")) != NULL) {
390          mailhost = cp;
391       } else {
392          mailhost = "localhost";
393       }
394    }
395
396 #if defined(HAVE_WIN32)
397    WSADATA  wsaData;
398
399    _setmode(0, _O_BINARY);
400    WSAStartup(MAKEWORD(2,2), &wsaData);
401 #endif
402
403    /*
404     *  Find out my own host name for HELO;
405     *  if possible, get the fully qualified domain name
406     */
407    if (gethostname(my_hostname, sizeof(my_hostname) - 1) < 0) {
408       Pmsg1(0, _("Fatal gethostname error: ERR=%s\n"), strerror(errno));
409       exit(1);
410    }
411 #ifdef HAVE_GETADDRINFO
412    memset(&hints, 0, sizeof(struct addrinfo));
413    hints.ai_family = AF_UNSPEC;
414    hints.ai_socktype = 0;
415    hints.ai_protocol = 0;
416    hints.ai_flags = AI_CANONNAME;
417
418    if ((res = getaddrinfo(my_hostname, NULL, &hints, &ai)) != 0) {
419       Pmsg2(0, _("Fatal getaddrinfo for myself failed \"%s\": ERR=%s\n"),
420             my_hostname, gai_strerror(res));
421       exit(1);
422    }
423    strcpy(my_hostname, ai->ai_canonname);
424    freeaddrinfo(ai);
425 #else
426    if ((hp = gethostbyname(my_hostname)) == NULL) {
427       Pmsg2(0, _("Fatal gethostbyname for myself failed \"%s\": ERR=%s\n"),
428             my_hostname, strerror(errno));
429       exit(1);
430    }
431    strcpy(my_hostname, hp->h_name);
432 #endif
433    Dmsg1(20, "My hostname is: %s\n", my_hostname);
434
435    /*
436     *  Determine from address.
437     */
438    if (from_addr == NULL) {
439 #if defined(HAVE_WIN32)
440       DWORD dwSize = UNLEN + 1;
441       LPSTR lpszBuffer = (LPSTR)alloca(dwSize);
442
443       if (GetUserName(lpszBuffer, &dwSize)) {
444          snprintf(buf, sizeof(buf), "%s@%s", lpszBuffer, my_hostname);
445       } else {
446          snprintf(buf, sizeof(buf), "unknown-user@%s", my_hostname);
447       }
448 #else
449       if ((pwd = getpwuid(getuid())) == 0) {
450          snprintf(buf, sizeof(buf), "userid-%d@%s", (int)getuid(), my_hostname);
451       } else {
452          snprintf(buf, sizeof(buf), "%s@%s", pwd->pw_name, my_hostname);
453       }
454 #endif
455       from_addr = bstrdup(buf);
456    }
457    Dmsg1(20, "From addr=%s\n", from_addr);
458
459    /*
460     *  Connect to smtp daemon on mailhost.
461     */
462 lookup_host:
463 #ifdef HAVE_GETADDRINFO
464    memset(&hints, 0, sizeof(struct addrinfo));
465    switch (default_resolv_type) {
466    case RESOLV_PROTO_ANY:
467       hints.ai_family = AF_UNSPEC;
468       break;
469    case RESOLV_PROTO_IPV4:
470       hints.ai_family = AF_INET;
471       break;
472 #ifdef HAVE_IPV6
473    case RESOLV_PROTO_IPV6:
474       hints.ai_family = AF_INET6;
475       break;
476 #endif
477    default:
478       hints.ai_family = AF_UNSPEC;
479       break;
480    }
481    hints.ai_socktype = SOCK_STREAM;
482    hints.ai_protocol = 0;
483    hints.ai_flags = 0;
484    snprintf(mail_port, sizeof(mail_port), "%d", mailport);
485
486    if ((res = getaddrinfo(mailhost, mail_port, &hints, &ai)) != 0) {
487       Pmsg2(0, _("Error unknown mail host \"%s\": ERR=%s\n"),
488             mailhost, gai_strerror(res));
489       if (strcasecmp(mailhost, "localhost")) {
490          Pmsg0(0, _("Retrying connection using \"localhost\".\n"));
491          mailhost = "localhost";
492          goto lookup_host;
493       }
494       exit(1);
495    }
496
497    for (rp = ai; rp != NULL; rp = rp->ai_next) {
498 #if defined(HAVE_WIN32)
499       s = WSASocket(rp->ai_family, rp->ai_socktype, rp->ai_protocol, NULL, 0, 0);
500 #else
501       s = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
502 #endif
503       if (s < 0) {
504          continue;
505       }
506
507       if (connect(s, rp->ai_addr, rp->ai_addrlen) != -1) {
508          break;
509       }
510
511       close(s);
512    }
513
514    if (!rp) {
515       Pmsg1(0, _("Failed to connect to mailhost %s\n"), mailhost);
516       exit(1);
517    }
518
519    freeaddrinfo(ai);
520 #else
521    if ((hp = gethostbyname(mailhost)) == NULL) {
522       Pmsg2(0, _("Error unknown mail host \"%s\": ERR=%s\n"), mailhost,
523          strerror(errno));
524       if (strcasecmp(mailhost, "localhost") != 0) {
525          Pmsg0(0, _("Retrying connection using \"localhost\".\n"));
526          mailhost = "localhost";
527          goto lookup_host;
528       }
529       exit(1);
530    }
531
532    if (hp->h_addrtype != AF_INET) {
533       Pmsg1(0, _("Fatal error: Unknown address family for smtp host: %d\n"), hp->h_addrtype);
534       exit(1);
535    }
536    memset((char *)&sin, 0, sizeof(sin));
537    memcpy((char *)&sin.sin_addr, hp->h_addr, hp->h_length);
538    sin.sin_family = hp->h_addrtype;
539    sin.sin_port = htons(mailport);
540 #if defined(HAVE_WIN32)
541    if ((s = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, 0)) < 0) {
542       Pmsg1(0, _("Fatal socket error: ERR=%s\n"), strerror(errno));
543       exit(1);
544    }
545 #else
546    if ((s = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
547       Pmsg1(0, _("Fatal socket error: ERR=%s\n"), strerror(errno));
548       exit(1);
549    }
550 #endif
551    if (connect(s, (struct sockaddr *)&sin, sizeof(sin)) < 0) {
552       Pmsg2(0, _("Fatal connect error to %s: ERR=%s\n"), mailhost, strerror(errno));
553       exit(1);
554    }
555    Dmsg0(20, "Connected\n");
556 #endif
557
558 #if defined(HAVE_WIN32)
559    int fdSocket = _open_osfhandle(s, _O_RDWR | _O_BINARY);
560    if (fdSocket == -1) {
561       Pmsg1(0, _("Fatal _open_osfhandle error: ERR=%s\n"), strerror(errno));
562       exit(1);
563    }
564
565    int fdSocket2 = dup(fdSocket);
566
567    if ((sfp = fdopen(fdSocket, "wb")) == NULL) {
568       Pmsg1(0, _("Fatal fdopen error: ERR=%s\n"), strerror(errno));
569       exit(1);
570    }
571    if ((rfp = fdopen(fdSocket2, "rb")) == NULL) {
572       Pmsg1(0, _("Fatal fdopen error: ERR=%s\n"), strerror(errno));
573       exit(1);
574    }
575 #else
576    if ((r = dup(s)) < 0) {
577       Pmsg1(0, _("Fatal dup error: ERR=%s\n"), strerror(errno));
578       exit(1);
579    }
580    if ((sfp = fdopen(s, "w")) == 0) {
581       Pmsg1(0, _("Fatal fdopen error: ERR=%s\n"), strerror(errno));
582       exit(1);
583    }
584    if ((rfp = fdopen(r, "r")) == 0) {
585       Pmsg1(0, _("Fatal fdopen error: ERR=%s\n"), strerror(errno));
586       exit(1);
587    }
588 #endif
589
590    /*
591     *  Send SMTP headers.  Note, if any of the strings have a <
592     *   in them already, we do not enclose the string in < >, otherwise
593     *   we do.
594     */
595    get_response(); /* banner */
596    chat("HELO %s\r\n", my_hostname);
597    chat("MAIL FROM:%s\r\n", cleanup_addr(from_addr, buf, sizeof(buf)));
598    
599    for (i = 0; i < argc; i++) {
600       Dmsg1(20, "rcpt to: %s\n", argv[i]);
601       chat("RCPT TO:%s\r\n", cleanup_addr(argv[i], buf, sizeof(buf)));
602    }
603
604    if (cc_addr) {
605       chat("RCPT TO:%s\r\n", cleanup_addr(cc_addr, buf, sizeof(buf)));
606    }
607    Dmsg0(20, "Data\n");
608    chat("DATA\r\n");
609
610    /*
611     *  Send message header
612     */
613    fprintf(sfp, "From: %s\r\n", from_addr);
614    Dmsg1(10, "From: %s\r\n", from_addr);
615    if (subject) {
616       fprintf(sfp, "Subject: %s\r\n", subject);
617       Dmsg1(10, "Subject: %s\r\n", subject);
618    }
619    if (reply_addr) {
620       fprintf(sfp, "Reply-To: %s\r\n", reply_addr);
621       Dmsg1(10, "Reply-To: %s\r\n", reply_addr);
622    }
623    if (err_addr) {
624       fprintf(sfp, "Errors-To: %s\r\n", err_addr);
625       Dmsg1(10, "Errors-To: %s\r\n", err_addr);
626    }
627
628 #if defined(HAVE_WIN32)
629    DWORD dwSize = UNLEN + 1;
630    LPSTR lpszBuffer = (LPSTR)alloca(dwSize);
631
632    if (GetUserName(lpszBuffer, &dwSize)) {
633       fprintf(sfp, "Sender: %s@%s\r\n", lpszBuffer, my_hostname);
634       Dmsg2(10, "Sender: %s@%s\r\n", lpszBuffer, my_hostname);
635    } else {
636       fprintf(sfp, "Sender: unknown-user@%s\r\n", my_hostname);
637       Dmsg1(10, "Sender: unknown-user@%s\r\n", my_hostname);
638    }
639 #else
640    if ((pwd = getpwuid(getuid())) == 0) {
641       fprintf(sfp, "Sender: userid-%d@%s\r\n", (int)getuid(), my_hostname);
642       Dmsg2(10, "Sender: userid-%d@%s\r\n", (int)getuid(), my_hostname);
643    } else {
644       fprintf(sfp, "Sender: %s@%s\r\n", pwd->pw_name, my_hostname);
645       Dmsg2(10, "Sender: %s@%s\r\n", pwd->pw_name, my_hostname);
646    }
647 #endif
648
649    fprintf(sfp, "To: %s", argv[0]);
650    Dmsg1(10, "To: %s", argv[0]);
651    for (i = 1; i < argc; i++) {
652       fprintf(sfp, ",%s", argv[i]);
653       Dmsg1(10, ",%s", argv[i]);
654    }
655
656    fprintf(sfp, "\r\n");
657    Dmsg0(10, "\r\n");
658    if (cc_addr) {
659       fprintf(sfp, "Cc: %s\r\n", cc_addr);
660       Dmsg1(10, "Cc: %s\r\n", cc_addr);
661    }
662
663    if (content_utf8) {
664       fprintf(sfp, "Content-Type: text/plain; charset=UTF-8\r\n");
665       Dmsg0(10, "Content-Type: text/plain; charset=UTF-8\r\n");
666    }
667
668    get_date_string(buf, sizeof(buf));
669    fprintf(sfp, "Date: %s\r\n", buf);
670    Dmsg1(10, "Date: %s\r\n", buf);
671
672    fprintf(sfp, "\r\n");
673
674    /*
675     *  Send message body
676     */
677    lines = 0;
678    while (fgets(buf, sizeof(buf), stdin)) {
679       if (maxlines > 0 && ++lines > maxlines) {
680          Dmsg1(20, "skip line because of maxlines limit: %lu\n", maxlines);
681          while (fgets(buf, sizeof(buf), stdin)) {
682             ++lines;
683          }
684          break;
685       }
686       buf[sizeof(buf)-1] = '\0';
687       buf[strlen(buf)-1] = '\0';
688       if (buf[0] == '.') {         /* add extra . see RFC 2821 4.5.2 */
689          fputs(".", sfp);
690       }
691       fputs(buf, sfp);
692       fputs("\r\n", sfp);
693    }
694
695    if (lines > maxlines) {
696       Dmsg1(10, "hit maxlines limit: %lu\n", maxlines);
697       fprintf(sfp, "\r\n\r\n[maximum of %lu lines exceeded, skipped %lu lines of output]\r\n", maxlines, lines-maxlines);
698    }
699
700    /*
701     *  Send SMTP quit command
702     */
703    chat(".\r\n");
704    chat("QUIT\r\n");
705
706    /*
707     *  Go away gracefully ...
708     */
709    exit(0);
710 }