diff options
Diffstat (limited to 'mumi/messages.scm')
-rw-r--r-- | mumi/messages.scm | 249 |
1 files changed, 161 insertions, 88 deletions
diff --git a/mumi/messages.scm b/mumi/messages.scm index 258b02d..9614852 100644 --- a/mumi/messages.scm +++ b/mumi/messages.scm @@ -1,5 +1,5 @@ ;;; mumi -- Mediocre, uh, mail interface -;;; Copyright © 2017 Ricardo Wurmus <rekado@elephly.net> +;;; Copyright © 2017, 2018 Ricardo Wurmus <rekado@elephly.net> ;;; ;;; This program is free software: you can redistribute it and/or ;;; modify it under the terms of the GNU Affero General Public License @@ -16,107 +16,180 @@ ;;; <http://www.gnu.org/licenses/>. (define-module (mumi messages) - #:use-module (mu) #:use-module (srfi srfi-1) + #:use-module (srfi srfi-19) #:use-module (srfi srfi-26) #:use-module (ice-9 optargs) #:use-module (ice-9 regex) #:use-module (ice-9 match) - #:use-module (mumi queries)) + #:use-module (debbugs soap) + #:use-module (debbugs operations) + #:use-module (debbugs email) + #:use-module (debbugs bug) + #:use-module (mumi config) + #:use-module (mailutils mailutils) + #:export (search-bugs fetch-bug recent-bugs)) -(define-public (extract-address str) - "Extract an email address from an address string." - (let ((m (string-match ".*<([^@]+@[^>]+)>" str))) - (if m (match:substring m 1) str))) +;; TODO: mu-address-get-personal skips non ASCII characters +;; ex: (mu-address-get-personal "ludo@gnu.org (Ludovic Courtès)") +;; => "Ludovic Courts" +(define-public (extract-name address) + (let ((name (mu-header-decode (mu-address-get-personal address)))) + (if (string-null? name) "Somebody" name))) -(define-public (recipients message) - "Return a list of recipient email addresses for the given MESSAGE." - (append-map (lambda (address-string) - (map (compose extract-address string-trim) - (string-split address-string #\,))) - (filter identity (list (mu:to message) - (mu:cc message) - (mu:bcc message))))) +(define-public extract-email mu-address-get-email) -(define-public sender (compose extract-address mu:from)) +(define (header message key) + (first (assoc-ref (email-headers message) key))) -(define-public (participants messages) - "Return a list of unique email addresses in the conversion." - (apply lset-adjoin string= '() - (map sender messages))) +(define-public (sender message) + (header message "from")) + +(define-public sender-email + (compose mu-address-get-email sender)) + +(define-public (sender-name message) + (extract-name (sender message))) -;; TODO: build a different version of "mu index" to also index -;; X-GNU-PR-* headers? +(define-public (date message) + (header message "date")) -(define-public (action message) - "Return the debbugs action MESSAGE." - (mu:header message "X-GNU-PR-Message")) +(define-public (subject message) + (header message "subject")) -(define-public (report? message) - (let ((action (action message))) - (and action (string-prefix? "report " action)))) +(define-public (message-id message) + (header message "message-id")) + +(define-public (participants messages) + "Return a list of unique senders in the conversion." + (apply lset-adjoin (lambda (a b) + (string= (mu-address-get-email a) + (mu-address-get-email b))) + '() (map sender messages))) + +(define-public (recipients message) + "Return a list of recipient email addresses for the given MESSAGE." + (let ((headers (email-headers message))) + (filter-map (match-lambda + (((or "cc" "bcc" "to") val) val) + (_ #f)) headers))) -;; We cannot rely on the action header alone. -(define-public (closing? message) - (let ((action (action message))) - (or (and action (string-prefix? "cc-closed " action)) - (find (cut string-suffix? "-done@debbugs.gnu.org" <>) - (recipients message))))) +(define-public (closing? message id) + "Is this MESSAGE closing this bug ID?" + (let ((done (string-append (number->string id) + "-done"))) + (string= (header message "x-debbugs-envelope-to") done))) -(define-public (owner? message) - (let ((action (action message))) - (and action (string-prefix? "owner " action)))) +(define-public (bot? address) + (string= "help-debbugs@gnu.org" address)) -(define-public (owner messages) - "Return the owner of this patch or #F if unassigned." - (and=> (find owner? messages) - sender)) +(define-public (internal-message? message) + (bot? (sender-email message))) (define-public (patch-messages id) - "Return list of messages relating to the patch ID." - (let ((address (string-append id "@debbugs.gnu.org")) - (done (string-append id "-done@debbugs.gnu.org"))) - (sort-list (mu:message-list (query-or (string-append "recip:" address) - (string-append "recip:" done))) - (lambda (a b) (< (mu:date a) (mu:date b)))))) - -(define-public (patch-report id) - "Return the original report for the MESSAGE associated with the -given patch ID, or return #F." - (let* ((address (string-append id "@debbugs.gnu.org")) - (reports (filter report? (mu:message-list - (string-append "to:" address))))) - (if (null? reports) #f (car reports)))) - -(define-public (unique-reports messages) - "Return a list of original reports for all given MESSAGES." - (let ((unique-ids (apply lset-adjoin string= '() - (map patch-id messages)))) - (sort-list (filter-map patch-report unique-ids) - ;; Newest first - (lambda (a b) (> (mu:date a) (mu:date b)))))) - -(define-public (patch-id message) - "Return the patch number from the given MESSAGE." - (or (and=> (action message) - (compose number->string string->number last string-tokenize)) - (let ((address (find (cut string-suffix? "@debbugs.gnu.org" <>) - (recipients message)))) - (and=> address - (lambda (address) - (first (string-split (first (string-split address #\@)) #\-))))) - "UNKNOWN")) - -(define*-public (patch-actions messages) - "Return a list of actions for the given patch ID or the set of -MESSAGES. Ignore follow events." - (filter (cut string-prefix? "followup " <>) - (filter-map action messages))) - -;; TODO: can a bug be reopened again? -(define-public (status messages) - (if (find closing? messages) "closed" "open")) - -(define*-public (all-patches #:optional messages) - "Return all messages that are of the report action type." - (filter report? (or messages (mu:message-list)))) + "Return list of messages relating to the bug ID." + ;; TODO: sort by date necessary? + (soap-invoke* (%config 'debbugs) get-bug-log id)) + + +(define* (search-bugs query #:key (attributes '()) (max 100)) + "Return a list of all bugs matching the given QUERY string." + (let* ((matches (soap-invoke* (%config 'debbugs) + search-est + query + #:max max + #:attributes + (append attributes + '((package string-prefix "guix"))))) + (ids (filter-map (lambda (item) + (assoc-ref item "id")) + matches))) + (soap-invoke* (%config 'debbugs) get-status ids))) + +;; TODO: This returns *any* matching debbugs bug, even if it is not +;; part of the default packages. +(define (fetch-bug id) + "Return the bug matching ID or #F." + (match (soap-invoke* (%config 'debbugs) get-status (list id)) + (() #f) + ((bug) bug))) + +(define (recent-bugs amount) + "Return up to AMOUNT bugs with most recent activity." + ;; "search-est" does not return unique items, so we have to take + ;; more and then filter the results. To allow for caching we round + ;; off the current time to the start of the hour. + (let* ((matches + (soap-invoke* (%config 'debbugs) + search-est + "" + #:max 50 + #:attributes + `((package string-prefix "guix") + (@cdate >= ,(let ((this-hour + (date->time-utc (let ((now (current-date))) + (make-date 0 0 0 (date-hour now) + (date-day now) + (date-month now) + (date-year now) 0)))) + (one-month + (make-time time-duration 0 (* 60 60 24 30)))) + (time-second (subtract-duration this-hour one-month))))))) + (ids (take (delete-duplicates + (filter-map (lambda (item) + (assoc-ref item "id")) + matches)) amount))) + (soap-invoke* (%config 'debbugs) get-status ids))) + +(define-public (process-query query) + "Process the QUERY string and return two values: the remaining +unprocessed query string and an alist of search attributes." + (fold (lambda (term acc) + (match acc + ((#:terms terms + #:attributes attrs + #:filters fs) + (match (string-split term #\:) + ;; This is not supported by the Debbugs SOAP service, + ;; so we filter locally. + (("is" (or "done" "closed")) + `(#:terms ,terms + #:attributes ,attrs + #:filters + ,(cons bug-done fs))) + (("is" (or "open" "pending")) + `(#:terms ,terms + #:attributes ,attrs + #:filters + ,(cons (negate bug-done) fs))) + (("title" title) + `(#:terms ,terms + #:attributes ,(cons `(subject string-contains ,title) attrs) + #:filters ,fs)) + (("tag" tag) + `(#:terms ,terms + #:attributes ,(cons `(tags string= ,tag) attrs) + #:filters ,fs)) + (("author" who) + `(#:terms ,terms + #:attributes ,(cons `(@author string-contains ,who) attrs) + #:filters ,fs)) + ;; This is not supported by the Debbugs SOAP service, + ;; so we filter locally. + (("submitter" who) + `(#:terms ,terms + #:attributes ,attrs + #:filters ,(cons (lambda (bug) + (string-contains-ci (bug-originator bug) + who)) + fs))) + (("severity" level) + `(#:terms ,terms + #:attributes ,(cons `(severity string= ,level) attrs) + #:filters ,fs)) + (_ + `(#:terms ,(cons term terms) + #:attributes ,attrs + #:filters ,fs)))))) + '(#:terms () #:attributes () #:filters ()) + (string-tokenize query))) |