| 1 | ;;; -*- mode: Lisp -*- |
|---|
| 2 | |
|---|
| 3 | ;;; This file is part of CL-SMTP, the Lisp SMTP Client |
|---|
| 4 | |
|---|
| 5 | |
|---|
| 6 | ;;; Copyright (C) 2004/2005/2006/2007 Jan Idzikowski |
|---|
| 7 | |
|---|
| 8 | ;;; This library is free software; you can redistribute it and/or |
|---|
| 9 | ;;; modify it under the terms of the Lisp Lesser General Public License |
|---|
| 10 | ;;; (http://opensource.franz.com/preamble.html), known as the LLGPL. |
|---|
| 11 | |
|---|
| 12 | ;;; This library is distributed in the hope that it will be useful, |
|---|
| 13 | ;;; but WITHOUT ANY WARRANTY; without even the implied warranty of |
|---|
| 14 | ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|---|
| 15 | ;;; Lisp Lesser GNU General Public License for more details. |
|---|
| 16 | |
|---|
| 17 | ;;; File: attachments.lisp |
|---|
| 18 | ;;; Description: encoding and transmitting login to include a mime attachment |
|---|
| 19 | |
|---|
| 20 | ;;; |
|---|
| 21 | ;;; Contributed by Brian Sorg |
|---|
| 22 | ;;; |
|---|
| 23 | ;;; Thanks to David Cooper for make-random-boundary |
|---|
| 24 | ;;; |
|---|
| 25 | (in-package :cl-smtp) |
|---|
| 26 | |
|---|
| 27 | ;;; Addition to allow for sending mime attachments along with the smtp message |
|---|
| 28 | |
|---|
| 29 | ;;---- Initialize array of possible boundary characters to make start of attachments |
|---|
| 30 | (defparameter *boundary-chars* |
|---|
| 31 | (let* ((chars (list #\a #\b #\c #\d #\e #\f #\g #\h #\i #\j #\k #\l #\m |
|---|
| 32 | #\n #\o #\p #\q #\r #\s #\t #\u #\v #\w #\x #\y #\z |
|---|
| 33 | #\A #\B #\C #\D #\E #\F #\G #\H #\I #\J #\K #\L #\M |
|---|
| 34 | #\N #\O #\P #\Q #\R #\S #\T #\U #\V #\W #\X #\Y #\Z |
|---|
| 35 | #\0 #\1 #\2 #\3 #\4 #\5 #\6 #\7 #\8 #\9)) |
|---|
| 36 | (arr (make-array (length chars)))) |
|---|
| 37 | (dotimes (i (length chars) arr) |
|---|
| 38 | (setf (aref arr i) (pop chars))))) |
|---|
| 39 | |
|---|
| 40 | (defun make-random-boundary (&optional (length 30) |
|---|
| 41 | (boundary-chars *boundary-chars*)) |
|---|
| 42 | (let ((boundary (make-string length)) |
|---|
| 43 | (prefix "_---------_") |
|---|
| 44 | (chars-length (length boundary-chars))) |
|---|
| 45 | (dotimes (i length (concatenate 'string prefix boundary)) |
|---|
| 46 | (setf (aref boundary i) |
|---|
| 47 | (svref *boundary-chars* (random chars-length)))))) |
|---|
| 48 | |
|---|
| 49 | (defun generate-multipart-header (sock boundary &key (multipart-type "mixed")) |
|---|
| 50 | (write-to-smtp sock |
|---|
| 51 | (format nil "Content-type: multipart/~a;~%~tBoundary=\"~a\"" |
|---|
| 52 | multipart-type boundary))) |
|---|
| 53 | |
|---|
| 54 | (defun generate-message-header (sock |
|---|
| 55 | &key boundary ;; uniques string of character -- see make-random-boundary |
|---|
| 56 | content-type ;; "text/plain; charset=ISO-8859-1" |
|---|
| 57 | content-disposition ;; inline attachment |
|---|
| 58 | content-transfer-encoding ;; 7 bit or 8 bit |
|---|
| 59 | (include-blank-line? t)) |
|---|
| 60 | (when boundary |
|---|
| 61 | (write-to-smtp sock (format nil "--~a" boundary))) |
|---|
| 62 | (when content-type |
|---|
| 63 | (write-to-smtp sock (format nil "Content-type: ~a" content-type))) |
|---|
| 64 | (when content-disposition |
|---|
| 65 | (write-to-smtp sock (format nil "Content-Disposition: ~A" |
|---|
| 66 | content-disposition))) |
|---|
| 67 | (when content-transfer-encoding |
|---|
| 68 | (write-to-smtp sock (format nil "Content-Transfer-Encoding: ~A" |
|---|
| 69 | content-transfer-encoding))) |
|---|
| 70 | (when include-blank-line? (write-blank-line sock))) |
|---|
| 71 | |
|---|
| 72 | (defun send-attachment-header (sock boundary name) |
|---|
| 73 | |
|---|
| 74 | (generate-message-header |
|---|
| 75 | sock |
|---|
| 76 | :boundary boundary |
|---|
| 77 | :content-type (format nil "~a;~%~tname=\"~a\"" (lookup-mime-type name) name) |
|---|
| 78 | :content-transfer-encoding "base64" |
|---|
| 79 | :content-disposition (format nil "inline; filename=\"~a\"" name))) |
|---|
| 80 | |
|---|
| 81 | (defun send-end-marker (sock boundary) |
|---|
| 82 | ;; Note the -- at beginning and end of boundary is required |
|---|
| 83 | (write-to-smtp sock (format nil "~%--~a--~%" boundary))) |
|---|
| 84 | |
|---|
| 85 | (defun send-attachment (sock attachment boundary buffer-size) |
|---|
| 86 | (when (probe-file attachment) |
|---|
| 87 | (let ((name (file-namestring attachment))) |
|---|
| 88 | (send-attachment-header sock boundary name) |
|---|
| 89 | (base64-encode-file attachment sock :buffer-size buffer-size)))) |
|---|
| 90 | |
|---|
| 91 | (defun base64-encode-file (file-in sock |
|---|
| 92 | &key |
|---|
| 93 | (buffer-size 256) ;; in KB |
|---|
| 94 | (wrap-at-column 72)) |
|---|
| 95 | "Encodes the file contents given by file-in, which can be of any form appropriate to with-open-file, and write the base-64 encoded version to sock, which is a socket. |
|---|
| 96 | |
|---|
| 97 | Buffer-size, given in KB, controls how much of the file is processed and written to the socket at one time. A buffer-size of 0, processes the file all at once, regardless of its size. One will have to weigh the speed vs, memory consuption risks when chosing which way is best. |
|---|
| 98 | |
|---|
| 99 | Wrap-at-column controls where the encode string is divided for line breaks." |
|---|
| 100 | (when (probe-file file-in) |
|---|
| 101 | ;;-- open filein --------- |
|---|
| 102 | (with-open-file (strm-in file-in |
|---|
| 103 | :element-type '(unsigned-byte 8)) |
|---|
| 104 | (let* ((;; convert buffer size given to bytes |
|---|
| 105 | ;; or compute bytes based on file |
|---|
| 106 | max-buffer-size |
|---|
| 107 | (if (zerop buffer-size) |
|---|
| 108 | (file-length strm-in) |
|---|
| 109 | ;; Ensures 64 bit encoding is properly |
|---|
| 110 | ;; divided so that filler |
|---|
| 111 | ;; characters are not required between chunks |
|---|
| 112 | (* 24 (truncate (/ (* buffer-size 1024) 24))))) |
|---|
| 113 | (column-count 0) |
|---|
| 114 | (eof? nil) |
|---|
| 115 | (buffer (make-array max-buffer-size |
|---|
| 116 | :element-type '(unsigned-byte 8)))) |
|---|
| 117 | (loop |
|---|
| 118 | (print-debug (format nil "~%Process attachment ~a~%" file-in)) |
|---|
| 119 | (let* ((;; read a portion of the file into the buffer arrary and |
|---|
| 120 | ;; returns the index where it stopped |
|---|
| 121 | byte-count (dotimes (i max-buffer-size max-buffer-size) |
|---|
| 122 | (let ((bchar (read-byte strm-in nil 'EOF))) |
|---|
| 123 | (if (eql bchar 'EOF) |
|---|
| 124 | (progn |
|---|
| 125 | (setq eof? t) |
|---|
| 126 | (return i)) |
|---|
| 127 | (setf (aref buffer i) bchar)))))) |
|---|
| 128 | (if (zerop buffer-size) |
|---|
| 129 | ;; send file all at once to socket. |
|---|
| 130 | #+allegro |
|---|
| 131 | (write-string (excl:usb8-array-to-base64-string |
|---|
| 132 | buffer wrap-at-column) sock) |
|---|
| 133 | #-allegro |
|---|
| 134 | (cl-base64:usb8-array-to-base64-stream |
|---|
| 135 | buffer sock :columns wrap-at-column) |
|---|
| 136 | ;; otherwise process file in chunks. |
|---|
| 137 | ;; The extra encoded-string, |
|---|
| 138 | ;; and its subseq functions are brute force methods |
|---|
| 139 | ;; to properly handle the wrap-at-column feature |
|---|
| 140 | ;; between buffers. |
|---|
| 141 | ;; Not the most efficient way, |
|---|
| 142 | ;; but it works and uses existing functions |
|---|
| 143 | ;; in the cl-base64 package. |
|---|
| 144 | (let* ((;; drops off extra elements that were not filled in in reading, this is important for lisp systems that default a value into |
|---|
| 145 | ;; the array when it is created. -- ie Lispworks, SBCL |
|---|
| 146 | trimmed-buffer (if eof? |
|---|
| 147 | (subseq buffer 0 byte-count) |
|---|
| 148 | buffer)) |
|---|
| 149 | (encoded-string |
|---|
| 150 | #+allegro |
|---|
| 151 | (excl:usb8-array-to-base64-string |
|---|
| 152 | trimmed-buffer) |
|---|
| 153 | #-allegro |
|---|
| 154 | (cl-base64:usb8-array-to-base64-string |
|---|
| 155 | trimmed-buffer))) |
|---|
| 156 | (loop for ch across encoded-string |
|---|
| 157 | do (progn |
|---|
| 158 | (write-char ch sock) |
|---|
| 159 | (incf column-count) |
|---|
| 160 | (when (= column-count wrap-at-column) |
|---|
| 161 | (setq column-count 0) |
|---|
| 162 | (write-char #\Newline sock)))))) |
|---|
| 163 | (force-output sock) |
|---|
| 164 | (print-debug (format nil "~% Eof is ~a~%" eof?)) |
|---|
| 165 | (when (or (zerop buffer-size) |
|---|
| 166 | eof?) |
|---|
| 167 | (return)))))))) |
|---|