| 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 "attachment; 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 70)) |
|---|
| 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)))))))) |
|---|