- (while headers
- (setq header (car headers)
- headers (cdr headers))
- (insert (int-to-string (mail-header-number header)) "\t"
- (or (mail-header-subject header) "") "\t"
- (or (mail-header-from header) "") "\t"
- (or (mail-header-date header) "") "\t"
- (or (mail-header-id header) "") "\t"
- (or (mail-header-references header) "") "\t"
- (int-to-string (or (mail-header-chars header) 0)) "\t"
- (int-to-string (or (mail-header-lines header) 0)) "\t"
- (if (mail-header-xref header)
- (concat "Xref: " (mail-header-xref header) "\t")
- "") "\n")))))
-
-(defun nnvirtual-possibly-change-newsgroups (group regexp &optional check)
- (let ((inf t))
- (or (not group)
- (and nnvirtual-current-group
- (string= group nnvirtual-current-group))
- (and (setq inf (assoc group nnvirtual-group-alist))
- (string= (nth 3 inf) regexp)
- (progn
- (setq nnvirtual-current-group (car inf))
- (setq nnvirtual-current-groups (nth 1 inf))
- (setq nnvirtual-current-mapping (nth 2 inf)))))
- (if (or (not check) (not inf))
- (progn
- (and inf (setq nnvirtual-group-alist
- (delq inf nnvirtual-group-alist)))
- (setq nnvirtual-current-mapping nil)
- (setq nnvirtual-current-group group)
- (let ((newsrc gnus-newsrc-alist)
- (virt-group (gnus-group-prefixed-name
- nnvirtual-current-group '(nnvirtual ""))))
- (setq nnvirtual-current-groups nil)
- (while newsrc
- (and (string-match regexp (car (car newsrc)))
- (not (string= (car (car newsrc)) virt-group))
- (setq nnvirtual-current-groups
- (cons (car (car newsrc)) nnvirtual-current-groups)))
- (setq newsrc (cdr newsrc))))
- (if nnvirtual-current-groups
- (progn
- (nnvirtual-create-mapping group)
- (setq nnvirtual-group-alist
- (cons (list group nnvirtual-current-groups
- nnvirtual-current-mapping regexp)
- nnvirtual-group-alist)))
- (setq nnvirtual-status-string
- (format
- "nnvirtual: No newsgroups for this virtual newsgroup"))))))
- nnvirtual-current-groups)
-
-(defun nnvirtual-create-mapping (group)
- (let* ((group (gnus-group-prefixed-name group (list 'nnvirtual "")))
- (info (nth 2 (gnus-gethash group gnus-newsrc-hashtb)))
- (groups nnvirtual-current-groups)
- (offset 0)
- reads unread igroup itotal ireads)
- ;; The virtual group doesn't exist. (?)
- (or info (error "No such group: %s" group))
- (setq nnvirtual-current-mapping nil)
- (while groups
- ;; Added by Sudish Joseph <joseph@cis.ohio-state.edu>.
- (setq igroup (car groups))
- (let ((info (nth 2 (gnus-gethash igroup gnus-newsrc-hashtb)))
- (active (gnus-gethash igroup gnus-active-hashtb)))
- ;; See if the group has had its active list read this session
- ;; if not, we do it now.
- (if (null active)
- (if (gnus-activate-group igroup)
- (progn
- (gnus-get-unread-articles-in-group
- info (gnus-gethash igroup gnus-active-hashtb))
- (setq active (gnus-gethash igroup gnus-active-hashtb)))
- (message "Couldn't open component group %s" igroup)))
- (if (null active)
- ()
- ;; And then we do the mapping for this component group. If
- ;; you feel tempted to cast your eyes to the soup below -
- ;; don't. It'll hurt your soul. Suffice to say that it
- ;; assigns ranges of nnvirtual article numbers to the
- ;; different component groups. To get the article number
- ;; from the nnvirtual number, one does something like
- ;; (+ (- number offset) (car active)), where `offset' is the
- ;; slice the mess below assigns, and active is the lowest
- ;; active article in the component group.
- (setq itotal (1+ (- (cdr active) (car active))))
- (if (setq ireads (nth 2 info))
- (let ((itreads
- (if (not (listp (cdr ireads)))
- (setq ireads (list (cons (car ireads) (cdr ireads))))
- (setq ireads (copy-alist ireads)))))
- (if (< (or (and (numberp (car ireads)) (car ireads))
- (cdr (car ireads))) (car active))
- (setq ireads (setq itreads (cdr ireads))))
- (if (and ireads (< (or (and (numberp (car ireads))
- (car ireads))
- (car (car ireads))) (car active)))
- (setcar (or (and (numberp (car ireads)) ireads)
- (car ireads)) (1+ (car active))))
- (while itreads
- (setcar (or (and (numberp (car itreads)) itreads)
- (car itreads))
- (+ (max
- 1 (- (if (numberp (car itreads))
- (car itreads)
- (car (car itreads)))
- (car active)))
- offset))
- (if (not (numberp (car itreads)))
- (setcdr (car itreads)
- (+ (- (cdr (car itreads)) (car active)) offset)))
- (setq itreads (cdr itreads)))
- (setq reads (nconc reads ireads))))
- (setq offset (+ offset (1- itotal)))
- (setq nnvirtual-current-mapping
- (cons (list offset igroup (car active))
- nnvirtual-current-mapping)))
- (setq groups (cdr groups))))
- (setq nnvirtual-current-mapping
- (nreverse nnvirtual-current-mapping))
- ;; Set Gnus active info.
- (gnus-sethash group (cons 1 (1- offset)) gnus-active-hashtb)
- ;; Set Gnus read info.
- (setcar (nthcdr 2 info) reads)
-
- ;; Then we deal with the marks.
- (let ((map nnvirtual-current-mapping)
- (marks '(tick dormant reply expire score))
- (offset 0)
- tick dormant reply expire score marked active)
- (while map
- (setq igroup (nth 1 (car map)))
- (setq active (nth 2 (car map)))
- (setq marked (nth 3 (nth 2 (gnus-gethash igroup gnus-newsrc-hashtb))))
- (let ((m marks))
- (while m
- (and (assq (car m) marked)
- (set (car m)
- (nconc (mapcar
- (lambda (art)
- (if (numberp art)
- (if (< art active)
- 0 (+ (- art active) offset))
- (cons (+ (- (car art) active) offset)
- (cdr art))))
- (cdr (assq (car m) marked)))
- (symbol-value (car m)))))
- (setq m (cdr m))))
- (setq offset (car (car map)))
- (setq map (cdr map)))
- ;; Put the list of marked articles in the info of the virtual group.
- (let ((m marks)
- marked)
- (while m
- (and (symbol-value (car m))
- (setq marked (cons (cons (car m) (symbol-value (car m)))
- marked)))
- (setq m (cdr m)))
- (if (nthcdr 3 info)
- (setcar (nthcdr 3 info) marked)
- (setcdr (nthcdr 2 info) (list marked)))))))
-
-(defun nnvirtual-update-marked ()
- (let ((mark-lists '((gnus-newsgroup-marked . tick)
- (gnus-newsgroup-dormant . dormant)
- (gnus-newsgroup-expirable . expire)
- (gnus-newsgroup-replied . reply)))
- marks art-group group-alist g)
- (while mark-lists
- (setq marks (symbol-value (car (car mark-lists))))
- ;; Find out what groups the mark belong to.
- (while marks
- (setq art-group (nnvirtual-art-group (car marks)))
- (if (setq g (assoc (car art-group) group-alist))
- (nconc g (list (cdr art-group)))
- (setq group-alist (cons (list (car art-group) (cdr art-group))
- group-alist)))
- (setq marks (cdr marks)))
- ;; The groups that don't have marks must have no marks. (Yup.)
- (let ((groups nnvirtual-current-groups))
- (while groups
- (or (assoc (car groups) group-alist)
- (setq group-alist (cons (list (car groups)) group-alist)))
- (setq groups (cdr groups))))
- ;; The we update the list of marks.
- (while group-alist
- (gnus-add-marked-articles
- (car (car group-alist)) (cdr (car mark-lists))
- (cdr (car group-alist)) nil t)
- (gnus-group-update-group (car (car group-alist)) t)
- (setq group-alist (cdr group-alist)))
- (setq mark-lists (cdr mark-lists)))))
-
-(defun nnvirtual-art-group (article)
- (let ((map nnvirtual-current-mapping)
- (offset 0))
- (while (< (car (car map)) (if (numberp article) article (car article)))
- (setq offset (car (car map))
- map (cdr map)))
- (cons (nth 1 (car map))
- (if (numberp article)
- (- (+ article (nth 2 (car map))) offset)
- (cons (- (+ (car article) (nth 2 (car map))) offset)
- (cdr article))))))
-
-(defun nnvirtual-catchup-group (group &optional server all)
- (nnvirtual-possibly-change-newsgroups group server)
- (let ((gnus-group-marked nnvirtual-current-groups)
- (gnus-expert-user t))
- (save-excursion
- (set-buffer gnus-group-buffer)
- (gnus-group-catchup-current nil all))))
+ (mapc 'nnheader-insert-nov headers))))
+
+
+(defun nnvirtual-update-xref-header (group article prefix system-name)
+ "Edit current NOV header in current buffer to have an xref to the component group, and also server prefix any existing xref lines."
+ ;; Move to beginning of Xref field, creating a slot if needed.
+ (beginning-of-line)
+ (looking-at
+ "[^\t]*\t[^\t]*\t[^\t]*\t[^\t]*\t[^\t]*\t[^\t]*\t[^\t]*\t")
+ (goto-char (match-end 0))
+ (unless (search-forward "\t" (point-at-eol) 'move)
+ (insert "\t"))
+
+ ;; Remove any spaces at the beginning of the Xref field.
+ (while (eq (char-after (1- (point))) ? )
+ (forward-char -1)
+ (delete-char 1))
+
+ (insert "Xref: " system-name " " group ":")
+ (princ article (current-buffer))
+ (insert " ")
+
+ ;; If there were existing xref lines, clean them up to have the correct
+ ;; component server prefix.
+ (save-restriction
+ (narrow-to-region (point)
+ (or (search-forward "\t" (point-at-eol) t)
+ (point-at-eol)))
+ (goto-char (point-min))
+ (when (re-search-forward "Xref: *[^\n:0-9 ]+ *" nil t)
+ (replace-match "" t t))
+ (goto-char (point-min))
+ (when (re-search-forward
+ (concat (regexp-quote (gnus-group-real-name group)) ":[0-9]+")
+ nil t)
+ (replace-match "" t t))
+ (unless (eobp)
+ (insert " ")
+ (when (not (string= "" prefix))
+ (while (re-search-forward "[^ ]+:[0-9]+" nil t)
+ (save-excursion
+ (goto-char (match-beginning 0))
+ (insert prefix))))))
+
+ ;; Ensure a trailing \t.
+ (end-of-line)
+ (or (eq (char-after (1- (point))) ?\t)
+ (insert ?\t)))
+
+
+(defun nnvirtual-possibly-change-server (server)
+ (or (not server)
+ (nnoo-current-server-p 'nnvirtual server)
+ (nnvirtual-open-server server)))
+
+
+(defun nnvirtual-update-read-and-marked (read-p update-p)
+ "Copy marks from the virtual group to the component groups.
+If READ-P is not nil, update the (un)read status of the components.
+If UPDATE-P is not nil, call gnus-group-update-group on the components."
+ (when nnvirtual-current-group
+ (let ((unreads (and read-p
+ (nnvirtual-partition-sequence
+ (gnus-list-of-unread-articles
+ (nnvirtual-current-group)))))
+ (type-marks
+ (delq nil
+ (mapcar (lambda (ml)
+ (if (eq (car ml) 'score)
+ nil
+ (cons (car ml)
+ (nnvirtual-partition-sequence (cdr ml)))))
+ (gnus-info-marks (gnus-get-info
+ (nnvirtual-current-group))))))
+ type groups info)
+
+ ;; Ok, atomically move all of the (un)read info, clear any old
+ ;; marks, and move all of the current marks. This way if someone
+ ;; hits C-g, you won't leave the component groups in a half-way state.
+ (progn
+ ;; move (un)read
+ ;; bind for workaround guns-update-read-articles
+ (let ((gnus-newsgroup-active nil))
+ (dolist (entry unreads)
+ (gnus-update-read-articles (car entry) (cdr entry))))
+
+ ;; clear all existing marks on the component groups
+ (dolist (group nnvirtual-component-groups)
+ (when (and (setq info (gnus-get-info group))
+ (gnus-info-marks info))
+ (gnus-info-set-marks
+ info
+ (if (assq 'score (gnus-info-marks info))
+ (list (assq 'score (gnus-info-marks info)))
+ nil))))
+
+ ;; Ok, currently type-marks is an assq list with keys of a mark type,
+ ;; with data of an assq list with keys of component group names
+ ;; and the articles which correspond to that key/group pair.
+ (dolist (mark type-marks)
+ (setq type (car mark))
+ (setq groups (cdr mark))
+ (dolist (carticles groups)
+ (gnus-add-marked-articles (car carticles) type (cdr carticles)
+ nil t))))
+
+ ;; possibly update the display, it is really slow
+ (when update-p
+ (dolist (group nnvirtual-component-groups)
+ (gnus-group-update-group group t))))))
+
+
+(defun nnvirtual-current-group ()
+ "Return the prefixed name of the current nnvirtual group."
+ (concat "nnvirtual:" nnvirtual-current-group))
+
+
+
+;;; This is currently O(kn^2) to merge n lists of length k.
+;;; You could do it in O(knlogn), but we have a small n, and the
+;;; overhead of the other approach is probably greater.
+(defun nnvirtual-merge-sorted-lists (&rest lists)
+ "Merge many sorted lists of numbers."
+ (if (null (cdr lists))
+ (car lists)
+ (sort (apply 'nconc lists) '<)))
+
+
+;;; We map between virtual articles and real articles in a manner
+;;; which keeps the size of the virtual active list the same as the
+;;; sum of the component active lists.
+
+;;; To achieve fair mixing of the groups, the last article in each of
+;;; N component groups will be in the last N articles in the virtual
+;;; group.
+
+;;; If you have 3 components A, B and C, with articles 1-8, 1-5, and
+;;; 6-7 respectively, then the virtual article numbers look like:
+;;;
+;;; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
+;;; A1 A2 A3 A4 B1 A5 B2 A6 B3 A7 B4 C6 A8 B5 C7
+
+;;; To compute these mappings we generate a couple tables and then
+;;; do some fast operations on them. Tables for the example above:
+;;;
+;;; Offsets - [(A 0) (B -3) (C -1)]
+;;;
+;;; a b c d e
+;;; Mapping - ([ 3 0 1 3 0 ]
+;;; [ 6 3 2 9 3 ]
+;;; [ 8 6 3 15 9 ])
+;;;
+;;; (note column 'e' is different in real algorithm, which is slightly
+;;; different than described here, but this gives you the methodology.)
+;;;
+;;; The basic idea is this, when going from component->virtual, apply
+;;; the appropriate offset to the article number. Then search the first
+;;; column of the table for a row where 'a' is less than or equal to the
+;;; modified number. You can see that only group A can therefore go to
+;;; the first row, groups A and B to the second, and all to the last.
+;;; The third column of the table is telling us the number of groups
+;;; which might be able to reach that row (it might increase by more than
+;;; 1 if several groups have the same size).
+;;; Then column 'b' provides an additional offset you apply when you have
+;;; found the correct row. You then multiply by 'c' and add on the groups
+;;; _position_ in the offset table. The basic idea here is that on
+;;; any given row we are going to map back and forth using X'=X*c+Y and
+;;; X=(X'/c), Y=(X' mod c). Then once you've done this transformation,
+;;; you apply a final offset from column 'e' to give the virtual article.
+;;;
+;;; Going the other direction, you instead search on column 'd' instead
+;;; of 'a', and apply everything in reverse order.
+
+;;; Convert component -> virtual:
+;;; set num = num - Offset(group)
+;;; find first row in Mapping where num <= 'a'
+;;; num = (num-'b')*c + Position(group) + 'e'
+
+;;; Convert virtual -> component:
+;;; find first row in Mapping where num <= 'd'
+;;; num = num - 'e'
+;;; group_pos = num mod 'c'
+;;; num = (num / 'c') + 'b' + Offset(group_pos)
+
+;;; Easy no? :)
+;;;
+;;; Well actually, you need to keep column e offset smaller by the 'c'
+;;; column for that line, and always add 1 more when going from
+;;; component -> virtual. Otherwise you run into a problem with
+;;; unique reverse mapping.
+
+(defun nnvirtual-map-article (article)
+ "Return a cons of the component group and article corresponding to the given virtual ARTICLE."
+ (let ((table nnvirtual-mapping-table)
+ entry group-pos)
+ (while (and table
+ (> article (aref (car table) 3)))
+ (setq table (cdr table)))
+ (when (and table
+ (> article 0))
+ (setq entry (car table))
+ (setq article (- article (aref entry 4) 1))
+ (setq group-pos (mod article (aref entry 2)))
+ (cons (car (aref nnvirtual-mapping-offsets group-pos))
+ (+ (/ article (aref entry 2))
+ (aref entry 1)
+ (cdr (aref nnvirtual-mapping-offsets group-pos)))
+ ))
+ ))
+
+
+
+(defun nnvirtual-reverse-map-article (group article)
+ "Return the virtual article number corresponding to the given component GROUP and ARTICLE."
+ (when (numberp article)
+ (let ((table nnvirtual-mapping-table)
+ (group-pos 0)
+ entry)
+ (while (not (string= group (car (aref nnvirtual-mapping-offsets
+ group-pos))))
+ (setq group-pos (1+ group-pos)))
+ (setq article (- article (cdr (aref nnvirtual-mapping-offsets
+ group-pos))))
+ (while (and table
+ (> article (aref (car table) 0)))
+ (setq table (cdr table)))
+ (setq entry (car table))
+ (when (and entry
+ (> article 0)
+ (< group-pos (aref entry 2))) ; article not out of range below
+ (+ (aref entry 4)
+ group-pos
+ (* (- article (aref entry 1))
+ (aref entry 2))
+ 1))
+ )))
+
+
+(defsubst nnvirtual-reverse-map-sequence (group articles)
+ "Return list of virtual article numbers for all ARTICLES in GROUP.
+The ARTICLES should be sorted, and can be a compressed sequence.
+If any of the article numbers has no corresponding virtual article,
+then it is left out of the result."
+ (when (numberp (cdr-safe articles))
+ (setq articles (list articles)))
+ (let (result a i j new-a)
+ (while (setq a (pop articles))
+ (if (atom a)
+ (setq i a
+ j a)
+ (setq i (car a)
+ j (cdr a)))
+ (while (<= i j)
+ ;; If this is slow, you can optimize by moving article checking
+ ;; into here. You don't have to recompute the group-pos,
+ ;; nor scan the table every time.
+ (when (setq new-a (nnvirtual-reverse-map-article group i))
+ (push new-a result))
+ (setq i (1+ i))))
+ (nreverse result)))
+
+
+(defun nnvirtual-partition-sequence (articles)
+ "Return an association list of component article numbers.
+These are indexed by elements of nnvirtual-component-groups, based on
+the sequence ARTICLES of virtual article numbers. ARTICLES should be
+sorted, and can be a compressed sequence. If any of the article
+numbers has no corresponding component article, then it is left out of
+the result."
+ (when (numberp (cdr-safe articles))
+ (setq articles (list articles)))
+ (let ((carticles (mapcar 'list nnvirtual-component-groups))
+ a i j article entry)
+ (while (setq a (pop articles))
+ (if (atom a)
+ (setq i a
+ j a)
+ (setq i (car a)
+ j (cdr a)))
+ (while (<= i j)
+ (when (setq article (nnvirtual-map-article i))
+ (setq entry (assoc (car article) carticles))
+ (setcdr entry (cons (cdr article) (cdr entry))))
+ (setq i (1+ i))))
+ (mapc (lambda (x) (setcdr x (nreverse (cdr x))))
+ carticles)
+ carticles))
+
+
+(defun nnvirtual-create-mapping ()
+ "Build the tables necessary to map between component (group, article) to virtual article.
+Generate the set of read messages and marks for the virtual group
+based on the marks on the component groups."
+ (let ((cnt 0)
+ (tot 0)
+ (M 0)
+ (i 0)
+ actives all-unreads all-marks
+ active min max size unreads marks
+ next-M next-tot
+ reads beg)
+ ;; Ok, we loop over all component groups and collect a lot of
+ ;; information:
+ ;; Into actives we place (g size max), where size is max-min+1.
+ ;; Into all-unreads we put (g unreads).
+ ;; Into all-marks we put (g marks).
+ ;; We also increment cnt and tot here, and compute M (max of sizes).
+ (mapc (lambda (g)
+ (setq active (gnus-activate-group g)
+ min (car active)
+ max (cdr active))
+ (when (and active (>= max min) (not (zerop max)))
+ ;; store active information
+ (push (list g (- max min -1) max) actives)
+ ;; collect unread/mark info for later
+ (setq unreads (gnus-list-of-unread-articles g))
+ (setq marks (gnus-info-marks (gnus-get-info g)))
+ (when gnus-use-cache
+ (push (cons 'cache
+ (gnus-cache-articles-in-group g))
+ marks))
+ (push (cons g unreads) all-unreads)
+ (push (cons g marks) all-marks)
+ ;; count groups, total #articles, and max size
+ (setq size (- max min -1))
+ (setq cnt (1+ cnt)
+ tot (+ tot size)
+ M (max M size))))
+ nnvirtual-component-groups)
+
+ ;; Number of articles in the virtual group.
+ (setq nnvirtual-mapping-len tot)
+
+
+ ;; We want the actives list sorted by size, to build the tables.
+ (setq actives (sort actives (lambda (g1 g2) (< (nth 1 g1) (nth 1 g2)))))
+
+ ;; Build the offset table. Largest sized groups are at the front.
+ (setq nnvirtual-mapping-offsets
+ (vconcat
+ (nreverse
+ (mapcar (lambda (entry)
+ (cons (nth 0 entry)
+ (- (nth 2 entry) M)))
+ actives))))
+
+ ;; Build the mapping table.
+ (setq nnvirtual-mapping-table nil)
+ (setq actives (mapcar (lambda (entry) (nth 1 entry)) actives))
+ (while actives
+ (setq size (car actives))
+ (setq next-M (- M size))
+ (setq next-tot (- tot (* cnt size)))
+ ;; make current row in table
+ (push (vector M next-M cnt tot (- next-tot cnt))
+ nnvirtual-mapping-table)
+ ;; update M and tot
+ (setq M next-M)
+ (setq tot next-tot)
+ ;; subtract the current size from all entries.
+ (setq actives (mapcar (lambda (x) (- x size)) actives))
+ ;; remove anything that went to 0.
+ (while (and actives
+ (= (car actives) 0))
+ (pop actives)
+ (setq cnt (- cnt 1))))
+
+
+ ;; Now that the mapping tables are generated, we can convert
+ ;; and combine the separate component unreads and marks lists
+ ;; into single lists of virtual article numbers.
+ (setq unreads (apply 'nnvirtual-merge-sorted-lists
+ (mapcar (lambda (x)
+ (nnvirtual-reverse-map-sequence
+ (car x) (cdr x)))
+ all-unreads)))
+ (setq marks (mapcar
+ (lambda (type)
+ (cons (cdr type)
+ (gnus-compress-sequence
+ (apply
+ 'nnvirtual-merge-sorted-lists
+ (mapcar (lambda (x)
+ (nnvirtual-reverse-map-sequence
+ (car x)
+ (cdr (assq (cdr type) (cdr x)))))
+ all-marks)))))
+ gnus-article-mark-lists))
+
+ ;; Remove any empty marks lists, and store.
+ (setq nnvirtual-mapping-marks nil)
+ (dolist (mark marks)
+ (when (cdr mark)
+ (push mark nnvirtual-mapping-marks)))
+
+ ;; We need to convert the unreads to reads. We compress the
+ ;; sequence as we go, otherwise it could be huge.
+ (while (and (<= (incf i) nnvirtual-mapping-len)
+ unreads)
+ (if (= i (car unreads))
+ (setq unreads (cdr unreads))
+ ;; try to get a range.
+ (setq beg i)
+ (while (and (<= (incf i) nnvirtual-mapping-len)
+ (not (= i (car unreads)))))
+ (setq i (- i 1))
+ (if (= i beg)
+ (push i reads)
+ (push (cons beg i) reads))
+ ))
+ (when (<= i nnvirtual-mapping-len)
+ (if (= i nnvirtual-mapping-len)
+ (push i reads)
+ (push (cons i nnvirtual-mapping-len) reads)))
+
+ ;; Store the reads list for later use.
+ (setq nnvirtual-mapping-reads (nreverse reads))
+
+ ;; Throw flag to show we changed the info.
+ (setq nnvirtual-info-installed nil)
+ ))