;;;; simple_list.lisp (in-package #:simple_list) ;;; "simple_list" goes here. Hacks and glory await! (defvar *imap-socket*) (defvar *mailing-list*) (defclass () ((name :initform nil :initarg :name :accessor ml-name) (emails :initform nil :initarg :name :accessor ml-emails))) (manardb:defmmclass () ((name :initform nil :initarg :name :accessor ml-name) (emails :initform nil :initarg :emails :accessor ml-emails)) ) (defmethod print-object ((obj ) s) (print-unreadable-object (obj s :type t :identity t) (format s "~a with ~d emails" (ml-name obj) (length (ml-emails obj))))) (defun make-mailing-list (name &optional persistent) (aprog1 (make-instance ') (setf (slot-value it 'name) (make-keyword (ctypecase name (string (string-to-list-name name)) (symbol name)))))) (defmacro aprogn (&body body) `(let* ,(loop for el in body collect `(it ,el)) it)) (defmacro define-special-variable-wrapper (name var &body value-form) (alexandria:with-gensyms (body) `(defmacro ,name (() &body ,body) (let ((var ',var) (value-form ',value-form)) `(let ((,var ,@value-form)) ,@,body))))) (eval-when (:compile-toplevel :load-toplevel :execute) (defun get-connection-spec () (clonsigna:make-imap :host (ubiquitous:value 'imap 'host) :port (ubiquitous:value 'imap 'port) :ssl-p (ubiquitous:value 'imap 'ssl-p)))) (define-special-variable-wrapper with-connection *imap-socket* (get-connection-spec)) (define-special-variable-wrapper with-mailing-lists *mailing-list* ((manardb:retrieve-all-instances ')) ) (defmacro insert-imap-socket (&body body) (list* 'progn (loop for cmd in body collect (list* (car cmd) '*imap-socket* (cdr cmd))))) (defun connect-and-authorize (auth-info) (clonsigna:cmd-connect *imap-socket*) (clonsigna:cmd-login *imap-socket* (car auth-info) (cdr auth-info))) (defmacro with-authorized-connection ((auth-info) &body body) `(with-connection () (connect-and-authorize ,auth-info) ,@body)) (defun connect-toplevel () (setf *imap-socket* (get-connection-spec))) (defun get-mailbox (name) (cmd-select *imap-socket* name) (mapcar #'princ-to-string (parse-search (cmd-search *imap-socket* :criteria "(recent unseen)" :uid-p t)))) (defun get-inbox () (get-mailbox "inbox")) (defun get-headers (uid) (parse-fetch-fields (cmd-fetch-fields *imap-socket* uid))) (defun get-bodystructures (uid) (let ((headers (get-headers uid))) (mapcar (lambda (x) (make-bodystructure (parse-bodystructure (getf x :bodystructure)))) headers))) (defun get-raw-message (uid) (let* ((message (split-sequence #\Newline (car (cmd-fetch-body *imap-socket* uid :uid-p t)))) (message-without-imap (slice message 0 -1))) (string-join message-without-imap #\Newline))) (defun get-message (uid) (let* ((bodystructures (get-bodystructures uid)) (bodies (flatten (mapcar #'bodystructure-body-list bodystructures))) (sections (flatten (mapcar #'structure-element-section bodies))) (charsets (flatten (mapcar (fw.lu:alambda (cadr (structure-element-body-parameters it))) bodies)))) (parse-fetch-body (car (cmd-fetch-body *imap-socket* uid :section sections :uid-p t)) :charset (car charsets)))) (defun lookup-list (list-name &optional (lookup-table *mailing-list*)) (cdr (assoc list-name lookup-table))) (defun email-to-list (email) (aprogn (car (split-sequence #\@ email :count 1)) (car (split-sequence #\- it :count 1 :from-end t)))) (defun peek-headers (uid) (cmd-fetch *imap-socket* uid :criteria (format nil "(~{~a ~}BODY.PEEK[header.fields (~{~a ~})])" '(uid flags bodystructure) '(date from to cc bcc subject message-id in-reply-to references)) :uid-p t)) (defun send-to-list (list-name from subject message host auth-info) (destructuring-bind (recipients . other-headers) (lookup-list list-name) (destructuring-bind (user . password) auth-info (apply #'cl-smtp:send-email (list* host from recipients subject message :ssl t :port 587 :authentication `(,user ,password) other-headers))))) (defun get-message-envelope-info (uid) (let* ((envelope (car (parse-fetch-fields (peek-headers uid)))) (headers (getf envelope :headers)) ) (values (getf headers :to) ; To (getf headers :from) ; From (getf headers :subject) ; From (getf envelope :flags) ; Message Flags (getf envelope :bodystructure) ; Message bodystructure headers ; Message headers envelope))) ; Envelope info (defun string-to-list-name (name-string) (aprogn name-string (string-upcase it) (make-keyword it))) (defun process-imap-message (uid host auth-info) (multiple-value-bind (to from subject flags) (get-message-envelope-info uid) (unless (member 'seen flags) (let* ((list-name (string-to-list-name (email-to-list to))) (body (get-message uid))) (send-to-list list-name from subject body host auth-info))))) (defun process-imap-mailbox (name host auth-info) (let ((uids (get-mailbox name))) (mapcar (lambda (x) (process-imap-message x host auth-info)) uids))) (defun process-inbox (host auth-info) (process-imap-mailbox "inbox" host auth-info)) (defun get-auth-info () (cons (ubiquitous:value 'auth 'user) (ubiquitous:value 'auth 'password))) (define-condition simple-list-error () () (:documentation "The base condition for exceptions thrown by this utility")) (define-condition no-such-list (simple-list-error) ((list-name :initarg :name :accessor ml-name)) (:report (lambda (c s) (format s "No list named ~a" (ml-name c))))) (defgeneric add-email (mailing-list email) (:method :around (mailing-list email) (let ((result (call-next-method))) (typecase mailing-list (symbol result) (t mailing-list)))) (:method ((mailing-list ) email) (with-slots (emails) mailing-list (unless (member email emails :test #'equalp) (push email emails)))) (:method ((list-name symbol) email) (let ((lists (remove-if-not (fw.lu:alambda (eq list-name (ml-name it))) (manardb:retrieve-all-instances ')))) (if-let ((mailing-list (car lists))) (add-email mailing-list email) (error 'no-such-list :name list-name))))) (defun add-email-to-list (list-name email) (restart-case (add-email list-name email) (make-list () (add-email (make-mailing-list list-name) email)))) (defun main (&optional argv) (ubiquitous:restore 'simple_list) (let ((auth-info (get-auth-info)) (host-name (ubiquitous:value 'imap 'host))) (with-mailing-lists () (format t "Connecting to server ~a" host-name) (with-authorized-connection (auth-info) (setf +debug+ t) (process-inbox host-name auth-info)))))