]> code.delx.au - gnu-emacs-elpa/blobdiff - packages/debbugs/debbugs.el
Fix some quoting problems in doc strings
[gnu-emacs-elpa] / packages / debbugs / debbugs.el
index 4bfbb903c65695d2b7eb2debaa8f83b074b8582b..c3d230732a3ba9fd6dc8c7f8222534a91b9606fe 100644 (file)
@@ -1,11 +1,12 @@
-;;; debbugs.el --- SOAP library to access debbugs servers
+;;; debbugs.el --- SOAP library to access debbugs servers  -*- lexical-binding:t -*-
 
 
-;; Copyright (C) 2011-2015 Free Software Foundation, Inc.
+;; Copyright (C) 2011-2016 Free Software Foundation, Inc.
 
 ;; Author: Michael Albinus <michael.albinus@gmx.de>
 ;; Keywords: comm, hypermedia
 ;; Package: debbugs
 
 ;; Author: Michael Albinus <michael.albinus@gmx.de>
 ;; Keywords: comm, hypermedia
 ;; Package: debbugs
-;; Version: 0.8
+;; Version: 0.9.7
+;; Package-Requires: ((soap-client "3.1.1") (cl-lib "0.5"))
 
 ;; This file is not part of GNU Emacs.
 
 
 ;; This file is not part of GNU Emacs.
 
@@ -34,7 +35,7 @@
 
 ;(setq soap-debug t message-log-max t)
 (require 'soap-client)
 
 ;(setq soap-debug t message-log-max t)
 (require 'soap-client)
-(eval-when-compile (require 'cl))
+(eval-when-compile (require 'cl-lib))
 
 (defgroup debbugs nil
   "Debbugs library"
 
 (defgroup debbugs nil
   "Debbugs library"
@@ -95,6 +96,36 @@ This corresponds to the Debbugs server to be accessed, either
       default-directory)))
   "The WSDL object to be used describing the SOAP interface.")
 
       default-directory)))
   "The WSDL object to be used describing the SOAP interface.")
 
+;; Please do not increase this value, otherwise we would run into
+;; performance problems on the server.  Maybe we need to change this a
+;; server specific value.
+(defconst debbugs-max-hits-per-request 500
+  "The max number of bugs or results per soap invocation.")
+
+(defvar debbugs-cache-data
+  (make-hash-table :test 'equal :size debbugs-max-hits-per-request)
+  "Hash table of retrieved bugs.")
+
+(defcustom debbugs-cache-expiry (* 60 60)
+  "How many seconds debbugs query results are cached.
+t or 0 disables caching, nil disables expiring."
+  :group 'debbugs
+  :type '(choice (const :tag "Always" t)
+                (const :tag "Never" nil)
+                (integer :tag "Seconds")))
+
+(defvar debbugs-soap-invoke-async-object nil
+  "The object manipulated by `debbugs-soap-invoke-async'.")
+
+(defun debbugs-soap-invoke-async (operation-name &rest parameters)
+  "Invoke the SOAP connection asynchronously."
+  (apply
+   #'soap-invoke-async
+   (lambda (response &rest _args)
+     (setq debbugs-soap-invoke-async-object
+          (append debbugs-soap-invoke-async-object (car response))))
+   nil debbugs-wsdl debbugs-port operation-name parameters))
+
 (defun debbugs-get-bugs (&rest query)
   "Return a list of bug numbers which match QUERY.
 
 (defun debbugs-get-bugs (&rest query)
   "Return a list of bug numbers which match QUERY.
 
@@ -150,8 +181,8 @@ Valid keywords are:
   the value of field `affects' in bug's status.  The returned bugs
   do not necessary belong to this package.
 
   the value of field `affects' in bug's status.  The returned bugs
   do not necessary belong to this package.
 
-  :status -- Status of bug.  Valid values are \"done\",
-  \"forwarded\" and \"open\".
+  :status -- Status of bug.  Valid values are \"open\",
+  \"forwarded\" and \"done\".
 
   :archive -- A keyword to filter for bugs which are already
   archived, or not.  Valid values are \"0\" (not archived),
 
   :archive -- A keyword to filter for bugs which are already
   archived, or not.  Valid values are \"0\" (not archived),
@@ -177,7 +208,7 @@ patch:
       (unless (and (keywordp kw) (stringp val))
        (error "Wrong query: %s %s" kw val))
       (setq key (substring (symbol-name kw) 1))
       (unless (and (keywordp kw) (stringp val))
        (error "Wrong query: %s %s" kw val))
       (setq key (substring (symbol-name kw) 1))
-      (case kw
+      (cl-case kw
        ((:package :severity :tag :src :affects)
         ;; Value shall be one word.
         (if (string-match "\\`\\S-+\\'" val)
        ((:package :severity :tag :src :affects)
         ;; Value shall be one word.
         (if (string-match "\\`\\S-+\\'" val)
@@ -194,8 +225,8 @@ patch:
               (setq vec (vconcat vec (list key val))))
           (error "Wrong %s: %s" key val)))
        (:status
               (setq vec (vconcat vec (list key val))))
           (error "Wrong %s: %s" key val)))
        (:status
-        ;; Possible values: "done", "forwarded" and "open"
-        (if (string-match "\\`\\(done\\|forwarded\\|open\\)\\'" val)
+        ;; Possible values: "open", "forwarded" and "done".
+        (if (string-match "\\`\\(open\\|forwarded\\|done\\)\\'" val)
             (setq vec (vconcat vec (list key val)))
           (error "Wrong %s: %s" key val)))
        (:archive
             (setq vec (vconcat vec (list key val)))
           (error "Wrong %s: %s" key val)))
        (:archive
@@ -213,6 +244,27 @@ patch:
   "Return the list of bug numbers, according to AMOUNT (a number) latest bugs."
   (sort (car (soap-invoke debbugs-wsdl debbugs-port "newest_bugs" amount)) '<))
 
   "Return the list of bug numbers, according to AMOUNT (a number) latest bugs."
   (sort (car (soap-invoke debbugs-wsdl debbugs-port "newest_bugs" amount)) '<))
 
+(defun debbugs-convert-soap-value-to-string (string-value)
+  "If STRING-VALUE is unibyte, decode its contents as a UTF-8 string.
+If STRING-VALUE is a multibyte string, then `soap-client'
+received an xsd:string for this value, and will have decoded it
+already.
+
+If STRING-VALUE is a unibyte string, then `soap-client' received
+an xsd:base64Binary, and ran `base64-decode-string' on it to
+produce a unibyte string of bytes.
+
+For some reason, the Debbugs server code base64-encodes strings
+that contain UTF-8 characters, and returns them as
+xsd:base64Binary, instead of just returning them as xsd:string.
+Therefore, when STRING-VALUE is a unibyte string, we assume its
+bytes represent a UTF-8 string and decode them accordingly."
+  (if (stringp string-value)
+      (if (not (multibyte-string-p string-value))
+         (decode-coding-string string-value 'utf-8)
+       string-value)
+    (error "Invalid string value")))
+
 (defun debbugs-get-status (&rest bug-numbers)
   "Return a list of status entries for the bugs identified by BUG-NUMBERS.
 
 (defun debbugs-get-status (&rest bug-numbers)
   "Return a list of status entries for the bugs identified by BUG-NUMBERS.
 
@@ -223,13 +275,14 @@ Every returned entry is an association list with the following attributes:
   `package': A list of package names the bug belongs to.
 
   `severity': The severity of the bug report. This can be
   `package': A list of package names the bug belongs to.
 
   `severity': The severity of the bug report. This can be
-  \"important\", \"grave\", \"normal\", \"minor\" or \"wishlist\".
+  \"critical\", \"grave\", \"serious\", \"important\",
+  \"normal\", \"minor\" or \"wishlist\".
 
   `tags': The status of the bug report, a list of strings.  This
   can be \"fixed\", \"notabug\", \"wontfix\", \"unreproducible\",
   \"moreinfo\" or \"patch\".
 
 
   `tags': The status of the bug report, a list of strings.  This
   can be \"fixed\", \"notabug\", \"wontfix\", \"unreproducible\",
   \"moreinfo\" or \"patch\".
 
-  `pending': The string \"pending\", \"forwarded\" or \"done\".
+  `pending': The string \"pending\", \"forwarded\", \"fixed\" or \"done\".
 
   `subject': Subject/Title of the bugreport.
 
 
   `subject': Subject/Title of the bugreport.
 
@@ -250,7 +303,7 @@ Every returned entry is an association list with the following attributes:
 
   `done': The email address of the worker who has closed the bug (if done).
 
 
   `done': The email address of the worker who has closed the bug (if done).
 
-  `archived': `t' if the bug is archived, `nil' otherwise.
+  `archived': t if the bug is archived, nil otherwise.
 
   `unarchived': The date the bug has been unarchived, if ever.
 
 
   `unarchived': The date the bug has been unarchived, if ever.
 
@@ -290,41 +343,103 @@ Example:
        \(last_modified . 1271200046.0)
        \(pending . \"pending\")
        \(package \"emacs\")))"
        \(last_modified . 1271200046.0)
        \(pending . \"pending\")
        \(package \"emacs\")))"
-  (when bug-numbers
-    (let ((object
-          (car
-           (soap-invoke
-            debbugs-wsdl debbugs-port "get_status"
-            (apply 'vector bug-numbers)))))
-      (mapcar
-       (lambda (x)
-        (let (y)
-          ;; "archived" is the number 1 or 0.
-          (setq y (assoc 'archived (cdr (assoc 'value x))))
-          (setcdr y (= (cdr y) 1))
-          ;; "found_versions" and "fixed_versions" are lists,
-          ;; containing strings or numbers.
-          (dolist (attribute '(found_versions fixed_versions))
-            (setq y (assoc attribute (cdr (assoc 'value x))))
-            (setcdr y (mapcar
-                       (lambda (z) (if (numberp z) (number-to-string z) z))
-                       (cdr y))))
-          ;; "mergedwith", "blocks" and "blockedby are strings,
-          ;; containing blank separated bug numbers.
-          (dolist (attribute '(mergedwith blocks blockedby))
-            (setq y (assoc attribute (cdr (assoc 'value x))))
-            (when (stringp (cdr y))
-              (setcdr y (mapcar
-                         'string-to-number (split-string (cdr y) " " t)))))
-          ;; "package" is a string, containing comma separated
-          ;; package names.  "keywords" and "tags" are strings,
-          ;; containing blank separated package names.
-          (dolist (attribute '(package keywords tags))
-            (setq y (assoc attribute (cdr (assoc 'value x))))
-            (when (stringp (cdr y))
-              (setcdr y (split-string (cdr y) ",\\| " t))))
-          (cdr (assoc 'value x))))
-       object))))
+  (let (cached-bugs)
+    ;; Check for cached bugs.
+    (setq bug-numbers (delete-dups bug-numbers)
+         bug-numbers
+         (delete
+          nil
+          (mapcar
+           (lambda (bug)
+             (let ((status (gethash bug debbugs-cache-data)))
+               (if (and
+                    status
+                    (or
+                     (null debbugs-cache-expiry)
+                     (and
+                      (natnump debbugs-cache-expiry)
+                      (> (cdr (assoc 'cache_time status))
+                         (- (float-time)) debbugs-cache-expiry))))
+                   (progn
+                     (setq cached-bugs (append cached-bugs (list status)))
+                     nil)
+                 bug)))
+           bug-numbers)))
+
+    ;; Retrieve the data.
+    (setq debbugs-soap-invoke-async-object nil)
+    (when bug-numbers
+      ;; Retrieve bugs asynchronously.
+      (let ((bug-ids bug-numbers)
+           results)
+       (while bug-ids
+         (setq results
+               (append
+                results
+                (list
+                 (debbugs-soap-invoke-async
+                  "get_status"
+                  (apply
+                   #'vector
+                   (butlast
+                    bug-ids (- (length bug-ids)
+                               debbugs-max-hits-per-request))))))
+
+               bug-ids
+               (last bug-ids (- (length bug-ids)
+                                debbugs-max-hits-per-request))))
+
+       (dolist (res results)
+         (while (buffer-live-p res)
+           (accept-process-output (get-buffer-process res) 0.1)))))
+
+    (append
+     cached-bugs
+     ;; Massage results.
+     (mapcar
+      (lambda (x)
+       (let (y)
+         ;; "archived" is the number 1 or 0.
+         (setq y (assoc 'archived (cdr (assoc 'value x))))
+         (setcdr y (= (cdr y) 1))
+         ;; "found_versions" and "fixed_versions" are lists,
+         ;; containing strings or numbers.
+         (dolist (attribute '(found_versions fixed_versions))
+           (setq y (assoc attribute (cdr (assoc 'value x))))
+           (setcdr y (mapcar
+                      (lambda (z) (if (numberp z) (number-to-string z) z))
+                      (cdr y))))
+         ;; "mergedwith", "blocks" and "blockedby are strings,
+         ;; containing blank separated bug numbers.
+         (dolist (attribute '(mergedwith blocks blockedby))
+           (setq y (assoc attribute (cdr (assoc 'value x))))
+           (when (stringp (cdr y))
+             (setcdr y (mapcar
+                        #'string-to-number (split-string (cdr y) " " t)))))
+         ;; "subject", "originator", "owner" and "summary" may be an
+         ;; xsd:base64Binary value containing a UTF-8-encoded string.
+         (dolist (attribute '(subject originator owner summary))
+           (setq y (assoc attribute (cdr (assoc 'value x))))
+           (when (stringp (cdr y))
+             (setcdr y (debbugs-convert-soap-value-to-string (cdr y)))))
+         ;; "package" is a string, containing comma separated
+         ;; package names.  "keywords" and "tags" are strings,
+         ;; containing blank separated package names.
+         (dolist (attribute '(package keywords tags))
+           (setq y (assoc attribute (cdr (assoc 'value x))))
+           (when (stringp (cdr y))
+             (setcdr y (split-string (cdr y) ",\\| " t))))
+         ;; Cache the result, and return.
+         (if (and debbugs-cache-expiry (natnump debbugs-cache-expiry))
+             (puthash
+              (cdr (assoc 'key x))
+              ;; Put also a time stamp.
+              (cons (cons 'cache_time (float-time))
+                    (cdr (assoc 'value x)))
+              debbugs-cache-data)
+           ;; Don't cache.
+           (cdr (assoc 'value x)))))
+      debbugs-soap-invoke-async-object))))
 
 (defun debbugs-get-usertag (&rest query)
   "Return a list of bug numbers which match QUERY.
 
 (defun debbugs-get-usertag (&rest query)
   "Return a list of bug numbers which match QUERY.
@@ -366,7 +481,7 @@ Example:
       (unless (and (keywordp kw) (stringp val))
        (error "Wrong query: %s %s" kw val))
       (setq key (substring (symbol-name kw) 1))
       (unless (and (keywordp kw) (stringp val))
        (error "Wrong query: %s %s" kw val))
       (setq key (substring (symbol-name kw) 1))
-      (case kw
+      (cl-case kw
        ((:user)
         ;; Value shall be one word.  Extract email address, if existing.
         (if (string-match "\\`\\S-+\\'" val)
        ((:user)
         ;; Value shall be one word.  Extract email address, if existing.
         (if (string-match "\\`\\S-+\\'" val)
@@ -375,12 +490,12 @@ Example:
                 (setq val user-mail-address))
               (when (string-match "<\\(.+\\)>" val)
                 (setq val (match-string 1 val)))
                 (setq val user-mail-address))
               (when (string-match "<\\(.+\\)>" val)
                 (setq val (match-string 1 val)))
-              (pushnew val user :test #'equal))
+              (cl-pushnew val user :test #'equal))
           (error "Wrong %s: %s" key val)))
        ((:tag)
         ;; Value shall be one word.
         (if (string-match "\\`\\S-+\\'" val)
           (error "Wrong %s: %s" key val)))
        ((:tag)
         ;; Value shall be one word.
         (if (string-match "\\`\\S-+\\'" val)
-            (pushnew val tags :test #'equal)
+            (cl-pushnew val tags :test #'equal)
           (error "Wrong %s: %s" key val)))
        (t (error "Unknown key: %s" kw))))
 
           (error "Wrong %s: %s" key val)))
        (t (error "Unknown key: %s" kw))))
 
@@ -414,7 +529,7 @@ Every message is an association list with the following attributes:
 
   `body': The message body.
 
 
   `body': The message body.
 
-  `attachments' A list of possible attachments, or `nil'.  Not
+  `attachments' A list of possible attachments, or nil.  Not
   implemented yet server side."
   (car (soap-invoke debbugs-wsdl debbugs-port "get_bug_log" bug-number)))
 
   implemented yet server side."
   (car (soap-invoke debbugs-wsdl debbugs-port "get_bug_log" bug-number)))
 
@@ -441,7 +556,8 @@ The following conditions are possible:
 
   :skip and :max are optional.  They specify, how many hits are
   skipped, and how many maximal hits are returned.  This can be
 
   :skip and :max are optional.  They specify, how many hits are
   skipped, and how many maximal hits are returned.  This can be
-  used for paged results.  Per default, :skip is 0 and :max is 10.
+  used for paged results.  Per default, :skip is 0 and all
+  possible hits are returned.
 
   There must be exactly one such condition.
 
 
   There must be exactly one such condition.
 
@@ -449,17 +565,15 @@ The following conditions are possible:
 
   ATTRIBUTE is one of the following keywords:
 
 
   ATTRIBUTE is one of the following keywords:
 
-  :status --  Status of bug.  Valid values are \"done\",
-  \"forwarded\" and \"open\".
-
   :subject, :@title -- The subject of a message or the title of
   the bug, a string.
 
   :date, :@cdate -- The submission or modification dates of a
   message, a number.
 
   :subject, :@title -- The subject of a message or the title of
   the bug, a string.
 
   :date, :@cdate -- The submission or modification dates of a
   message, a number.
 
-  :submitter, :@author -- The email address of the submitter of a
-  bug or the author of a message belonging to this bug, a string.
+  :@author -- The email address of the author of a message
+  belonging to this bug, a string.  It may be different than
+  the email of the person submitting the bug.
   The special email address \"me\" is used as pattern, replaced
   with `user-mail-address'.
 
   The special email address \"me\" is used as pattern, replaced
   with `user-mail-address'.
 
@@ -518,9 +632,9 @@ same attributes as in the conditions.  Additional attributes are
 Examples:
 
   \(debbugs-search-est
 Examples:
 
   \(debbugs-search-est
-    '\(:phrase \"armstrong AND debbugs\" :skip 10 :max 2)
-    '\(:severity \"normal\" :operator \"STRINC\")
-    '\(:date :order \"NUMA\"))
+    \\='\(:phrase \"armstrong AND debbugs\" :skip 10 :max 2)
+    \\='\(:severity \"normal\" :operator \"STRINC\")
+    \\='\(:date :order \"NUMA\"))
 
   => \(\(\(msg_num . 21)
        \(date . 1229208302)
 
   => \(\(\(msg_num . 21)
        \(date . 1229208302)
@@ -536,141 +650,170 @@ Examples:
 
   ;; Show all messages from me between 2011-08-01 and 2011-08-31.
   \(debbugs-search-est
 
   ;; Show all messages from me between 2011-08-01 and 2011-08-31.
   \(debbugs-search-est
-    '\(:max 20)
-    '\(:@author \"me\" :operator \"ISTRINC\")
-    `\(:date
+    \\='\(:max 20)
+    \\='\(:@author \"me\" :operator \"ISTRINC\")
+    \\=`\(:date
       ,\(floor \(float-time \(encode-time 0 0 0  1 8 2011)))
       ,\(floor \(float-time \(encode-time 0 0 0 31 8 2011)))
       :operator \"NUMBT\"))"
 
       ,\(floor \(float-time \(encode-time 0 0 0  1 8 2011)))
       ,\(floor \(float-time \(encode-time 0 0 0 31 8 2011)))
       :operator \"NUMBT\"))"
 
-  (let (args result)
-    ;; Compile search arguments.
-    (dolist (elt query)
-      (let (vec kw key val
-           phrase-cond attr-cond)
-
-       ;; Phrase is mandatory, even if empty.
-       (when (and (or  (member :skip elt) (member :max elt))
-                  (not (member :phrase elt)))
-         (setq vec (vector "phrase" "")))
-
-       ;; Parse condition.
-       (while (consp elt)
-         (setq kw (pop elt))
-         (unless (keywordp kw)
-           (error "Wrong keyword: %s" kw))
-         (setq key (substring (symbol-name kw) 1))
-         (cl-case kw
-           ;; Phrase condition.
-           (:phrase
-            ;; It shouldn't happen in an attribute condition.
-            (if attr-cond
-                (error "Wrong keyword: %s" kw))
-            (setq phrase-cond t val (pop elt))
-            ;; Value is a string.
-            (if (stringp val)
-                (setq vec (vconcat vec (list key val)))
-              (error "Wrong %s: %s" key val)))
-
-           ((:skip :max)
-            ;; It shouldn't happen in an attribute condition.
-            (if attr-cond
-                (error "Wrong keyword: %s" kw))
-            (setq phrase-cond t val (pop elt))
-            ;; Value is a number.
-            (if (numberp val)
-                (setq vec (vconcat vec (list key (number-to-string val))))
-              (error "Wrong %s: %s" key val)))
-
-           ;; Attribute condition.
-           ((:submitter :@author)
-            ;; It shouldn't happen in a phrase condition.
-            (if phrase-cond
-                (error "Wrong keyword: %s" kw))
-            (if (not (stringp (car elt)))
-                (setq vec (vconcat vec (list key "")))
-              ;; Value is an email address.
-              (while (and (stringp (car elt))
-                          (string-match "\\`\\S-+\\'" (car elt)))
-                (when (string-equal "me" (car elt))
-                  (setcar elt user-mail-address))
-                (when (string-match "<\\(.+\\)>" (car elt))
-                  (setcar elt (match-string 1 (car elt))))
-                 (let ((x (pop elt)))
-                   (unless (member x val)
-                     (setq val (append val (list x))))))
-              (setq vec
-                    (vconcat vec (list key (mapconcat 'identity val " "))))))
-
-           (:status
-            ;; It shouldn't happen in a phrase condition.
-            (if phrase-cond
-                (error "Wrong keyword: %s" kw))
-            (setq attr-cond t)
-            (if (not (stringp (car elt)))
-                (setq vec (vconcat vec (list key "")))
-              ;; Possible values: "done", "forwarded" and "open"
-              (while  (and (stringp (car elt))
-                           (string-match
-                            "\\`\\(done\\|forwarded\\|open\\)\\'" (car elt)))
-                (let ((x (pop elt)))
-                   (unless (member x val)
-                     (setq val (append val (list x))))))
-              (setq vec
-                    (vconcat vec (list key (mapconcat 'identity val " "))))))
-
-           ((:subject :package :tags :severity :@title)
-            ;; It shouldn't happen in a phrase condition.
-            (if phrase-cond
-                (error "Wrong keyword: %s" kw))
-            (setq attr-cond t)
-            (if (not (stringp (car elt)))
-                (setq vec (vconcat vec (list key "")))
-              ;; Just a string.
-              (while (stringp (car elt))
-                (let ((x (pop elt)))
-                   (unless (member x val)
-                     (setq val (append val (list x))))))
-              (setq vec
-                    (vconcat vec (list key (mapconcat 'identity val " "))))))
-
-           ((:date :@cdate)
-            ;; It shouldn't happen in a phrase condition.
-            (if phrase-cond
-                (error "Wrong keyword: %s" kw))
-            (setq attr-cond t)
-            (if (not (numberp (car elt)))
-                (setq vec (vconcat vec (list key "")))
-              ;; Just a number.
-              (while (numberp (car elt))
-                 (let ((x (pop elt)))
-                   (unless (member x val)
-                     (setq val (append val (list x))))))
-              (setq vec
-                    (vconcat
-                     vec (list key (mapconcat 'number-to-string val " "))))))
-
-           ((:operator :order)
-            ;; It shouldn't happen in a phrase condition.
-            (if phrase-cond
-                (error "Wrong keyword: %s" kw))
-            (setq attr-cond t val (pop elt))
-            ;; Value is a number.
-            (if (stringp val)
-                (setq vec (vconcat vec (list key val)))
-              (error "Wrong %s: %s" key val)))
-
-           (t (error "Unknown key: %s" kw))))
-
-       (setq args (vconcat args (list vec)))))
-
-    (setq result
-         (car (soap-invoke debbugs-wsdl debbugs-port "search_est" args)))
-    ;; The result contains lists (key value).  We transform it into
-    ;; cons cells (key . value).
-    (dolist (elt1 result result)
-      (dolist (elt2 elt1)
-       (setcdr elt2 (cadr elt2))))))
+  (let ((phrase (assoc :phrase query))
+       args result)
+    (if (and phrase (not (member :skip phrase)) (not (member :max phrase)))
+       ;; We loop, until we have all results.
+       (let ((skip 0)
+             (query (delete phrase query))
+             result1)
+         (while skip
+           (setq result1
+                 (apply
+                  #'debbugs-search-est
+                  (append
+                   (list
+                    (append
+                     phrase `(:skip ,skip)
+                     `(:max ,debbugs-max-hits-per-request)))
+                   query))
+                 skip (and (= (length result1) debbugs-max-hits-per-request)
+                           (+ skip debbugs-max-hits-per-request))
+                 result (append result result1)))
+         result)
+
+      ;; Compile search arguments.
+      (dolist (elt query)
+        ;; FIXME: `vec' is used in an O(N²) way.  It should be a list instead,
+        ;; on which we push elements, and we only convert it to a vector at
+        ;; the end.
+       (let (vec kw key val
+                 phrase-cond attr-cond)
+
+         ;; Phrase is mandatory, even if empty.
+         (when (and (or  (member :skip elt) (member :max elt))
+                    (not (member :phrase elt)))
+           (setq vec (vector "phrase" "")))
+
+         ;; Parse condition.
+         (while (consp elt)
+           (setq kw (pop elt))
+           (unless (keywordp kw)
+             (error "Wrong keyword: %s" kw))
+           (setq key (substring (symbol-name kw) 1))
+           (cl-case kw
+             ;; Phrase condition.
+             (:phrase
+              ;; It shouldn't happen in an attribute condition.
+              (if attr-cond
+                  (error "Wrong keyword: %s" kw))
+              (setq phrase-cond t val (pop elt))
+              ;; Value is a string.
+              (if (stringp val)
+                  (setq vec (vconcat vec (list key val)))
+                (error "Wrong %s: %s" key val)))
+
+             ((:skip :max)
+              ;; It shouldn't happen in an attribute condition.
+              (if attr-cond
+                  (error "Wrong keyword: %s" kw))
+              (setq phrase-cond t val (pop elt))
+              ;; Value is a number.
+              (if (numberp val)
+                  (setq vec (vconcat vec (list key (number-to-string val))))
+                (error "Wrong %s: %s" key val)))
+
+             ;; Attribute condition.
+             ((:submitter :@author)
+              ;; It shouldn't happen.
+              (if (or (and (eq kw :submitter) phrase-cond)
+                      (and (eq kw :@author) attr-cond))
+                  (error "Wrong keyword: %s" kw))
+              (if (not (stringp (car elt)))
+                  (setq vec (vconcat vec (list key "")))
+                ;; Value is an email address.
+                (while (and (stringp (car elt))
+                            (string-match "\\`\\S-+\\'" (car elt)))
+                  (when (string-equal "me" (car elt))
+                    (setcar elt user-mail-address))
+                  (when (string-match "<\\(.+\\)>" (car elt))
+                    (setcar elt (match-string 1 (car elt))))
+                  (let ((x (pop elt)))
+                    (unless (member x val)
+                      (setq val (append val (list x))))))
+                (setq vec
+                      (vconcat
+                       vec (list key (mapconcat #'identity val " "))))))
+
+             (:status
+              ;; It shouldn't happen in a phrase condition.
+              (if phrase-cond
+                  (error "Wrong keyword: %s" kw))
+              (setq attr-cond t)
+              (if (not (stringp (car elt)))
+                  (setq vec (vconcat vec (list key "")))
+                ;; Possible values: "open", "forwarded" and "done".
+                (while (and (stringp (car elt))
+                            (string-match
+                             "\\`\\(open\\|forwarded\\|done\\)\\'" (car elt)))
+                  (let ((x (pop elt)))
+                    (unless (member x val)
+                      (setq val (append val (list x))))))
+                (setq vec
+                      (vconcat
+                       vec (list key (mapconcat #'identity val " "))))))
+
+             ((:subject :package :tags :severity :@title)
+              ;; It shouldn't happen in a phrase condition.
+              (if phrase-cond
+                  (error "Wrong keyword: %s" kw))
+              (setq attr-cond t)
+              (if (not (stringp (car elt)))
+                  (setq vec (vconcat vec (list key "")))
+                ;; Just a string.
+                (while (stringp (car elt))
+                  (let ((x (pop elt)))
+                    (unless (member x val)
+                      (setq val (append val (list x))))))
+                (setq vec
+                      (vconcat
+                       vec (list key (mapconcat #'identity val " "))))))
+
+             ((:date :@cdate)
+              ;; It shouldn't happen in a phrase condition.
+              (if phrase-cond
+                  (error "Wrong keyword: %s" kw))
+              (setq attr-cond t)
+              (if (not (numberp (car elt)))
+                  (setq vec (vconcat vec (list key "")))
+                ;; Just a number.
+                (while (numberp (car elt))
+                  (let ((x (pop elt)))
+                    (unless (member x val)
+                      (setq val (append val (list x))))))
+                (setq vec
+                      (vconcat
+                       vec
+                       (list key (mapconcat #'number-to-string val " "))))))
+
+             ((:operator :order)
+              ;; It shouldn't happen in a phrase condition.
+              (if phrase-cond
+                  (error "Wrong keyword: %s" kw))
+              (setq attr-cond t val (pop elt))
+              ;; Value is a number.
+              (if (stringp val)
+                  (setq vec (vconcat vec (list key val)))
+                (error "Wrong %s: %s" key val)))
+
+             (t (error "Unknown key: %s" kw))))
+
+         (setq args (vconcat args (list vec)))))
+
+      (setq result
+           (car (soap-invoke debbugs-wsdl debbugs-port "search_est" args)))
+      ;; The result contains lists (key value).  We transform it into
+      ;; cons cells (key . value).
+      (dolist (elt1 result result)
+       (dolist (elt2 elt1)
+         (setcdr elt2 (cadr elt2)))))))
 
 (defun debbugs-get-attribute (bug-or-message attribute)
   "Return the value of key ATTRIBUTE.
 
 (defun debbugs-get-attribute (bug-or-message attribute)
   "Return the value of key ATTRIBUTE.
@@ -681,7 +824,7 @@ BUG-OR-MESSAGE must be list element returned by either
 Example: Return the originator of last submitted bug.
 
 \(debbugs-get-attribute
 Example: Return the originator of last submitted bug.
 
 \(debbugs-get-attribute
-  \(car \(apply 'debbugs-get-status \(debbugs-newest-bugs 1))) 'originator)"
+  \(car \(apply #\\='debbugs-get-status \(debbugs-newest-bugs 1))) \\='originator)"
   (cdr (assoc attribute bug-or-message)))
 
 (defun debbugs-get-message-numbers (messages)
   (cdr (assoc attribute bug-or-message)))
 
 (defun debbugs-get-message-numbers (messages)
@@ -698,11 +841,11 @@ the header lines of the message, the second element is the body
 of the message.  Further elements of the list, if any, are
 attachments of the message.
 
 of the message.  Further elements of the list, if any, are
 attachments of the message.
 
-If there is no message with MESSAGE-NUMBER, the function returns `nil'.
+If there is no message with MESSAGE-NUMBER, the function returns nil.
 
 Example: Return the first message of last submitted bug.
 
 
 Example: Return the first message of last submitted bug.
 
-\(let \(\(messages \(apply 'debbugs-get-bug-log \(debbugs-newest-bugs 1))))
+\(let \(\(messages \(apply #\\='debbugs-get-bug-log \(debbugs-newest-bugs 1))))
   \(debbugs-get-message messages
                       \(car \(debbugs-get-message-numbers messages))))"
   (while (and messages
   \(debbugs-get-message messages
                       \(car \(debbugs-get-message-numbers messages))))"
   (while (and messages
@@ -729,8 +872,8 @@ following symbols:
    either symbol depends on actual Debbugs server configuration.
    For gnu.org, use the former; for debian.org - the latter.
 
    either symbol depends on actual Debbugs server configuration.
    For gnu.org, use the former; for debian.org - the latter.
 
-FILENAME, if non-`nil', is the name of file to store mbox.  If
-FILENAME is `nil', the downloaded mbox is inserted into the
+FILENAME, if non-nil, is the name of file to store mbox.  If
+FILENAME is nil, the downloaded mbox is inserted into the
 current buffer."
   (let (url (mt "") bn)
     (unless (setq url (plist-get
 current buffer."
   (let (url (mt "") bn)
     (unless (setq url (plist-get