From mboxrd@z Thu Jan 1 00:00:00 1970 X-Msuck: nntp://news.gmane.io/gmane.emacs.gnus.general/46384 Path: main.gmane.org!not-for-mail From: Ted Zlatanov Newsgroups: gmane.emacs.gnus.general Subject: new spam.el (alpha code) Date: Tue, 03 Sep 2002 23:56:02 -0400 Organization: =?koi8-r?q?=F4=C5=CF=C4=CF=D2=20=FA=CC=C1=D4=C1=CE=CF=D7?= @ Cienfuegos Sender: owner-ding@hpc.uh.edu Message-ID: NNTP-Posting-Host: localhost.gmane.org Mime-Version: 1.0 Content-Type: multipart/mixed; boundary="=-=-=" X-Trace: main.gmane.org 1031111625 29067 127.0.0.1 (4 Sep 2002 03:53:45 GMT) X-Complaints-To: usenet@main.gmane.org NNTP-Posting-Date: Wed, 4 Sep 2002 03:53:45 +0000 (UTC) Cc: Alexander Kotelnikov , =?iso-8859-1?q?Fran=E7ois?= Pinard Return-path: Original-Received: from malifon.math.uh.edu ([129.7.128.13]) by main.gmane.org with esmtp (Exim 3.35 #1 (Debian)) id 17mREe-0007Yh-00 for ; Wed, 04 Sep 2002 05:53:44 +0200 Original-Received: from sina.hpc.uh.edu ([129.7.128.10] ident=lists) by malifon.math.uh.edu with esmtp (Exim 3.20 #1) id 17mREe-000555-00; Tue, 03 Sep 2002 22:53:44 -0500 Original-Received: by sina.hpc.uh.edu (TLB v0.09a (1.20 tibbs 1996/10/09 22:03:07)); Tue, 03 Sep 2002 22:54:19 -0500 (CDT) Original-Received: from sclp3.sclp.com (qmailr@sclp3.sclp.com [209.196.61.66]) by sina.hpc.uh.edu (8.9.3/8.9.3) with SMTP id WAA26943 for ; Tue, 3 Sep 2002 22:53:56 -0500 (CDT) Original-Received: (qmail 25359 invoked by alias); 4 Sep 2002 03:53:16 -0000 Original-Received: (qmail 25354 invoked from network); 4 Sep 2002 03:53:16 -0000 Original-Received: from ns2.beld.net (208.229.215.82) by gnus.org with SMTP; 4 Sep 2002 03:53:16 -0000 Original-Received: from heechee.beld.net (dhcp-0-50-8b-df-51-5e.cpe.beld.net [65.202.179.253]) by ns2.beld.net (Postfix) with ESMTP id 173EE3B8F6; Tue, 3 Sep 2002 23:53:11 -0400 (EDT) Original-To: ding X-Face: bd.DQ~'29fIs`T_%O%C\g%6jW)yi[zuz6;d4V0`@y-~$#3P_Ng{@m+e4o<4P'#(_GJQ%TT= D}[Ep*b!\e,fBZ'j_+#"Ps?s2!4H2-Y"sx" Mail-Followup-To: ding , Alexander Kotelnikov , =?iso-8859-1?q?Fran=E7ois?= Pinard Original-Lines: 46 User-Agent: Gnus/5.090008 (Oort Gnus v0.08) Emacs/21.2 (i386-redhat-linux-gnu) Precedence: list X-Majordomo: 1.94.jlt7 Xref: main.gmane.org gmane.emacs.gnus.general:46384 X-Report-Spam: http://spam.gmane.org/gmane.emacs.gnus.general:46384 --=-=-= Content-Type: text/plain; charset=iso-8859-1 Content-Transfer-Encoding: quoted-printable Hello, I am attaching the latest spam.el code. 90% of the work was done by Fran=E7ois Pinard, who deserves the credit for not only adding an interface to bogofilter, but also implementing a lot of new and necessary features. There's also summary exit functionality to process articles marked as spam. There's lots of FIXME comments, and instructions on installing bogofilter and a procmail recipe too. The goal is to add more spam checks as time goes on. (bogofilter is a program that implements statistical analysis in an external process from Emacs. See the comments inside spam.el) I also added a BBDB check, which uses the BBDB like a whitelist. Credit goes to Alexander Kotelnikov. Alexander, please let me know if it's OK to use your code :) The code assumes bbdb.el is loaded - that's probably a bad idea, so I marked it with a FIXME. I'm also not sure about the whitelist/blacklist logic, that may be a little bogus. I have not committed to CVS yet, because all the code is in alpha. The CVS spam.el is fairly stable (well, it's broken, and that's a sort of stability, isn't it? :) so I'd rather let people test the code and fix bugs outside CVS for the first few days. The spam-split function, especially, has completely changed, and the new spam.el may break code that uses it. The work currently in spam.el can use RBL, whitelist/blacklist/BBDB, and bogofilter. We can add other Bayesian analyzers; several external ones were discussed already. The spam-stats.el Lisp statistical analysis could be included as well. I think 95% of the work for spam-stats.el is done by the author of the library already, only the final glue in spam.el needs to be put together. The documentation for the spam.el library is in the Lisp comments right now. I will write real docs as soon as we have a stable user-side interface. I know they are necessary. I will be gone from tomorrow until Sept. 25 on vacation, so feel free to kick spam.el around until then. Someone else can commit to CVS if I don't respond quickly. Thanks Ted --=-=-= Content-Type: application/emacs-lisp Content-Disposition: attachment; filename=spam.el Content-Transfer-Encoding: 8bit ;;; spam.el --- Identifying spam ;; Copyright (C) 2002 Free Software Foundation, Inc. ;; Author: Lars Magne Ingebrigtsen ;; Keywords: network ;; This file is part of GNU Emacs. ;; GNU Emacs is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation; either version 2, or (at your option) ;; any later version. ;; GNU Emacs is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; You should have received a copy of the GNU General Public License ;; along with GNU Emacs; see the file COPYING. If not, write to the ;; Free Software Foundation, Inc., 59 Temple Place - Suite 330, ;; Boston, MA 02111-1307, USA. ;;; Commentary: ;;; This module addresses a few aspects of spam control under Gnus. Page ;;; breaks are used for grouping declarations and documentation relating to ;;; each particular aspect. ;;; The integration with Gnus is not yet complete. See various `FIXME' ;;; comments, below, for supplementary explanations or discussions. ;;; Code: (require 'gnus-sum) ;; FIXME! We should not require `dns' nor `message' until we actually ;; need them. Best would be to declare needed functions as auto-loadable. (require 'dns) (require 'message) ;;; Main parameters. (defvar spam-use-blacklist t "True if blacklist should be used.") (defvar spam-use-whitelist t "True if blacklist should be used.") (defvar spam-use-blackholes nil ;; FIXME! Turned off for now. The DNS routines are said to be flaky. "True if blackholes should be used.") (defvar spam-use-bogofilter t "True if bogofilter should be used.") (defvar spam-split-group "spam" "Usual group name where spam should be split.") (defvar spam-junk-mailgroups ;; FIXME! The mailgroup list evidently depends on other choices made by the ;; user, so the built-in default below is not likely to be appropriate. (cons spam-split-group '("mail.junk" "poste.pourriel")) "Mailgroups which are dedicated by splitting to receive various junk. All unmarked article in such group receive the spam mark on group entry.") ;; FIXME! For `spam-ham-marks' and `spam-spam-marks', I wonder if it would ;; not be easier for the user to just accept a string of mark letters, instead ;; of a list of Gnus variable names. In such case, the stunt of deferred ;; evaluation would not be useful anymore. Lars?? :-) ;; FIXME! It is rather questionable to see `K', `X' and `Y' as indicating ;; positive ham. It much depends on how and why people use kill files, score ;; files, and the kill command. Maybe it would be better, by default, to not ;; process a message neither as ham nor spam, that is, just ignore it for ;; learning purposes, when we are not sure of how the user sees it. ;; But `r' and `R' should undoubtedly be seen as ham. ;; FIXME! Some might consider overkill to define a list of spam marks. On ;; the other hand, who knows, some users might for example like that ;; explicitly `E'xpired articles be processed as positive spam. (defvar spam-ham-marks (list gnus-del-mark gnus-read-mark gnus-killed-mark gnus-kill-file-mark gnus-low-score-mark) "Marks considered as being ham (positively not spam). Such articles will be transmitted to `bogofilter -n' on group exit.") (defvar spam-spam-marks (list gnus-spam-mark) "Marks considered as being spam (positively spam). Such articles will be transmitted to `bogofilter -s' on group exit.") ;; FIXME! Ideally, the remainder of this page should be fully integrated ;; within `gnus-sum.el'. ;;; Key bindings for spam control. ;; FIXME! The justification for `M-d' is that this is what Paul Graham ;; suggests in his original article, and what Eric Raymond's patch for Mutt ;; uses. But more importantly, that binding was still free in Summary mode! ;; FIXME! Lars has not blessed the following key bindings yet. It looks ;; convenient that the score analysis command uses a sequence ending with the ;; letter `t', so it nicely parallels `B t' or `V t'. `M-d' is a kind of ;; "alternate" `d', it is also the sequence suggested in Paul Graham article, ;; and also in Eric Raymond's patch for Mutt. `S u' might be the more ;; official key binding for `M-d' (but it is in use right now). (gnus-define-keys gnus-summary-mode-map "St" spam-bogofilter-score "Su" gnus-summary-mark-as-spam "\M-d" gnus-summary-mark-as-spam) ;;; How to highlight a spam summary line. ;; FIXME! Of course, `gnus-splash-face' has another purpose. Maybe a ;; special face should be created, named and used instead, for spam lines. (push '((eq mark gnus-spam-mark) . gnus-splash-face) gnus-summary-highlight) ;;; Hooks dispatching. A bit raw for now. (defun spam-summary-prepare () (spam-mark-junk-as-spam-routine)) (defun spam-summary-prepare-exit () (spam-bogofilter-register-routine)) (add-hook 'gnus-summary-prepare-hook 'spam-summary-prepare) (add-hook 'gnus-summary-prepare-exit-hook 'spam-summary-prepare-exit) (defun spam-mark-junk-as-spam-routine () (when (member gnus-newsgroup-name spam-junk-mailgroups) (let ((articles gnus-newsgroup-articles) article) (while articles (setq article (pop articles)) (when (eq (gnus-summary-article-mark article) gnus-unread-mark) (gnus-summary-mark-article article gnus-spam-mark)))))) ;;;; Spam determination. ;; The following list contains pairs associating a parameter variable with a ;; spam checking function. If the parameter variable is true, then the ;; checking function is called, and its value decides what happens. Each ;; individual check may return `nil', `t', or a mailgroup name. The value ;; `nil' means that the check does not yield a decision, and so, that further ;; checks are needed. The value `t' means that the message is definitely not ;; spam, and that further spam checks should be inhibited. Otherwise, a ;; mailgroup name is returned where the mail should go, and further checks are ;; also inhibited. The usual mailgroup name is the value of ;; `spam-split-group', meaning that the message is definitely a spam. (defvar spam-list-of-checks '((spam-use-blacklist . spam-check-blacklist) (spam-use-whitelist . spam-check-whitelist) (spam-use-bbdb . spam-check-bbdb) (spam-use-blackholes . spam-check-blackholes) (spam-use-bogofilter . spam-check-bogofilter))) (defun spam-split () "Split this message into the `spam' group if it is spam. This function can be used as an entry in `nnmail-split-fancy', for example like this: (: spam-split) See the Info node `(gnus)Fancy Mail Splitting' for more details." (interactive) (let ((list-of-checks spam-list-of-checks) decision) (while (and list-of-checks (not decision)) (let ((pair (pop list-of-checks))) (when (eval (car pair)) (setq decision (apply (cdr pair)))))) (if (eq decision t) nil decision))) ;;;; Blackholes. (defvar spam-blackhole-servers '("bl.spamcop.net" "relays.ordb.org" "dev.null.dk" "relays.visi.com" "rbl.maps.vix.com") "List of blackhole servers.") (defun spam-check-blackholes () "Check the Receieved headers for blackholed relays." (let ((headers (message-fetch-field "received")) ips matches) (when headers (with-temp-buffer (insert headers) (goto-char (point-min)) (while (re-search-forward "\\[\\([0-9]+.[0-9]+.[0-9]+.[0-9]+\\)\\]" nil t) (message "Blackhole search found host IP %s." (match-string 1)) (push (mapconcat 'identity (nreverse (split-string (match-string 1) "\\.")) ".") ips))) (dolist (server spam-blackhole-servers) (dolist (ip ips) (when (query-dns (concat ip "." server)) (push (list ip server (query-dns (concat ip "." server) 'TXT)) matches))))) (when matches spam-split-group))) ;;;; Blacklists and whitelists. (defvar spam-directory "~/News/spam/" "When spam files are kept.") (defvar spam-whitelist (expand-file-name "whitelist" spam-directory) "The location of the whitelist. The file format is one regular expression per line. The regular expression is matched against the address.") (defvar spam-blacklist (expand-file-name "blacklist" spam-directory) "The location of the blacklist. The file format is one regular expression per line. The regular expression is matched against the address.") (defvar spam-whitelist-cache nil) (defvar spam-blacklist-cache nil) (defun spam-enter-whitelist (address) "Enter ADDRESS into the whitelist." (interactive "sAddress: ") (spam-enter-list address spam-whitelist) (setq spam-whitelist-cache nil)) (defun spam-enter-blacklist (address) "Enter ADDRESS into the blacklist." (interactive "sAddress: ") (spam-enter-list address spam-blacklist) (setq spam-blacklist-cache nil)) (defun spam-enter-list (address file) "Enter ADDRESS into the given FILE, either the whitelist or the blacklist." (unless (file-exists-p (file-name-directory file)) (make-directory (file-name-directory file) t)) (save-excursion (set-buffer (find-file-noselect file)) (goto-char (point-max)) (unless (bobp) (insert "\n")) (insert address "\n") (save-buffer))) (defun spam-check-whitelist () ;; FIXME! Should it detect when file timestamps change? (unless spam-whitelist-cache (setq spam-whitelist-cache (spam-parse-list spam-whitelist))) (and (spam-from-listed-p spam-whitelist-cache) t)) ;;; copied from code by Alexander Kotelnikov ;; FIXME: assumes that bbdb.el is loaded ;; FIXME: not sure about the logic... (defun spam-check-bbdb () "We want people, who are in bbdb not to be splitted to spam" (let ((who (cadr (gnus-extract-address-components (message-fetch-field "from")) ))) (bbdb-search (bbdb-records) nil nil (regexp-quote who)))) (defun spam-check-blacklist () ;; FIXME! Should it detect when file timestamps change? (unless spam-blacklist-cache (setq spam-blacklist-cache (spam-parse-list spam-blacklist))) (and (spam-from-listed-p spam-blacklist-cache) spam-split-group)) (eval-and-compile (defalias 'spam-point-at-eol (if (fboundp 'point-at-eol) 'point-at-eol 'line-end-position))) (defun spam-parse-list (file) (when (file-readable-p file) (let (contents address) (with-temp-buffer (insert-file-contents file) (while (not (eobp)) (setq address (buffer-substring (point) (spam-point-at-eol))) (forward-line 1) (unless (zerop (length address)) (setq address (regexp-quote address)) (while (string-match "\\\\\\*" address) (setq address (replace-match ".*" t t address))) (push address contents)))) (nreverse contents)))) (defun spam-from-listed-p (cache) (let ((from (message-fetch-field "from")) found) (while cache (when (string-match (pop cache) from) (setq found t cache nil))) found)) ;;;; Training via Bogofilter. Last updated 2002-09-02. ;;; See Paul Graham article, at `http://www.paulgraham.com/spam.html'. ;;; This page is for those wanting to control spam with the help of Eric ;;; Raymond's speedy Bogofilter, see http://www.tuxedo.org/~esr/bogofilter. ;;; This has been tested with a locally patched copy of version 0.4. ;;; Make sure Bogofilter is installed. Bogofilter internally uses Judy fast ;;; associative arrays, so you need to install Judy first, and Bogofilter ;;; next. Fetch both distributions by visiting the following links and ;;; downloading the latest version of each: ;;; ;;; http://sourceforge.net/projects/judy/ ;;; http://www.tuxedo.org/~esr/bogofilter/ ;;; ;;; Unpack the Judy distribution and enter its main directory. Then do: ;;; ;;; ./configure ;;; make ;;; make install ;;; ;;; You will likely need to become super-user for the last step. Then, unpack ;;; the Bogofilter distribution and enter its main directory: ;;; ;;; make ;;; make install ;;; ;;; Here as well, you need to become super-user for the last step. Now, ;;; initialises your word lists by doing, under your own identity: ;;; ;;; mkdir ~/.bogofilter ;;; touch ~/.bogofilter/badlist ;;; touch ~/.bogofilter/goodlist ;;; ;;; These two files are text files you may edit, but you normally don't! ;;; The `M-d' command gets added to Gnus summary mode, marking current article ;;; as spam, showing it with the `H' mark. Whenever you see a spam article, ;;; make sure to mark its summary line with `M-d' before leaving the group. ;;; Some groups, as per variable `spam-junk-mailgroups' below, receive articles ;;; from Gnus splitting on clues added by spam recognisers, so for these ;;; groups, we tack an `H' mark at group entry for all summary lines which ;;; would otherwise have no other mark. Make sure to _remove_ `H' marks for ;;; any article which is _not_ genuine spam, before leaving such groups: you ;;; may use `M-u' to "unread" the article, or `d' for declaring it read the ;;; non-spam way. When you leave a group, all `H' marked articles, saved or ;;; unsaved, are sent to Bogofilter which will study them as spam samples. ;;; Messages may also be deleted in various other ways, and unless ;;; `spam-ham-marks-form' gets overridden below, marks `R' and `r' for default ;;; read or explicit delete, marks `X' and 'K' for automatic or explicit ;;; kills, as well as mark `Y' for low scores, are all considered to be ;;; associated with articles which are not spam. This assumption might be ;;; false, in particular if you use kill files or score files as means for ;;; detecting genuine spam, you should then adjust `spam-ham-marks-form'. When ;;; you leave a group, all _unsaved_ articles bearing any the above marks are ;;; sent to Bogofilter which will study these as not-spam samples. If you ;;; explicit kill a lot, you might sometimes end up with articles marked `K' ;;; which you never saw, and which might accidentally contain spam. Best is ;;; to make sure that real spam is marked with `H', and nothing else. ;;; All other marks do not contribute to Bogofilter pre-conditioning. In ;;; particular, ticked, dormant or souped articles are likely to contribute ;;; later, when they will get deleted for real, so there is no need to use ;;; them prematurely. Explicitly expired articles do not contribute, command ;;; `E' is a way to get rid of an article without Bogofilter ever seeing it. ;;; In a word, with a minimum of care for associating the `H' mark for spam ;;; articles only, Bogofilter training all gets fairly automatic. You should ;;; do this until you get a few hundreds of articles in each category, spam ;;; or not. The shell command `head -1 ~/.bogofilter/*' shows both article ;;; counts. The command `S S' in summary mode, either for debugging or for ;;; curiosity, triggers Bogofilter into displaying in another buffer the ;;; "spamicity" score of the current article (between 0.0 and 1.0), together ;;; with the article words which most significantly contribute to the score. ;;; The real way for using Bogofilter, however, is to have some use tool like ;;; `procmail' for invoking it on message reception, then adding some ;;; recognisable header in case of detected spam. Gnus splitting rules might ;;; later trip on these added headers and react by sorting such articles into ;;; specific junk folders as per `spam-junk-mailgroups'. Here is a possible ;;; `.procmailrc' contents (still untested -- please tell me how it goes): ;;; ;;; :0HBf: ;;; * ? bogofilter ;;; | formail -bfI "X-Spam-Status: Yes" (defvar spam-output-buffer-name "*Bogofilter Output*" "Name of buffer when displaying `bogofilter -v' output.") (defvar spam-spaminfo-header-regexp ;; FIXME! In the following regexp, we should explain which tool produces ;; which kind of header. I do not even remember them all by now. X-Junk ;; (and previously X-NoSpam) are produced by the `NoSpam' tool, which has ;; never been published, so it might not be reasonable leaving it in the ;; list. "^X-\\(jf\\|Junk\\|NoSpam\\|Spam\\|SB\\)[^:]*:" "Regexp for spam markups in headers. Markup from spam recognisers, as well as `Xref', are to be removed from articles before they get registered by Bogofilter.") ;; FIXME! I do not know if Gnus has a compatibility function for ;; `executable-find'. Here is a possible mantra for portability, ;; until Lars decides how we really should do it. (unless (fboundp 'executable-find) (if (fboundp 'locate-file) (defun executable-find (command) (locate-file command exec-path)) (autoload 'executable-find "executable"))) ;; End of portability mantra for `executable-find'. (defvar spam-bogofilter-path (executable-find "bogofilter") "File path of the Bogofilter executable program. Force this variable to nil if you want to inhibit the functionality.") (defun spam-check-bogofilter () ;; Dynamic spam check. I do not know how to check the exit status, ;; so instead, read `bogofilter -v' output. (when (and spam-use-bogofilter spam-bogofilter-path) (spam-bogofilter-articles nil "-v" (list (gnus-summary-article-number))) (when (save-excursion (set-buffer spam-output-buffer-name) (goto-char (point-min)) (re-search-forward "Spamicity: \\(0\\.9\\|1\\.0\\)" nil t)) spam-split-group))) (defun spam-bogofilter-score () "Use `bogofilter -v' on the current article. This yields the 15 most discriminant words for this article and the spamicity coefficient of each, and the overall article spamicity." (interactive) (when (and spam-use-bogofilter spam-bogofilter-path) (spam-bogofilter-articles nil "-v" (list (gnus-summary-article-number))) (save-excursion (set-buffer spam-output-buffer-name) (unless (= (point-min) (point-max)) (display-message-or-buffer (current-buffer) spam-output-buffer-name))))) (defun spam-bogofilter-register-routine () (when (and spam-use-bogofilter spam-bogofilter-path) (let ((articles gnus-newsgroup-articles) article mark ham-articles spam-articles) (while articles (setq article (pop articles) mark (gnus-summary-article-mark article)) (cond ((memq mark spam-spam-marks) (push article spam-articles)) ((memq article gnus-newsgroup-saved)) ((memq mark spam-ham-marks) (push article ham-articles)))) (when ham-articles (spam-bogofilter-articles "ham" "-n" ham-articles)) (when spam-articles (spam-bogofilter-articles "spam" "-s" spam-articles))))) (defun spam-bogofilter-articles (type option articles) (let* ((output-buffer (get-buffer-create spam-output-buffer-name)) (article-copy (get-buffer-create " *Bogofilter Article Copy*")) (remove-regexp (concat spam-spaminfo-header-regexp "\\|Xref:")) (prefix (and type (format "Studying %d articles as `%s'..." (length articles) type))) (counter 0) process article) (when prefix (message "%s" prefix)) (save-excursion (set-buffer output-buffer) (erase-buffer)) (setq process (start-process "bogofilter" output-buffer spam-bogofilter-path "-F" option)) (process-kill-without-query process t) (unwind-protect (save-window-excursion (while articles (gnus-summary-goto-subject (pop articles)) (gnus-summary-select-article) (gnus-eval-in-buffer-window article-copy (insert-buffer-substring gnus-original-article-buffer) ;; Remove spam classification redundant headers: they may induce ;; unwanted biases in later analysis. (goto-char (point-min)) (while (not (or (eobp) (= (following-char) ?\n))) (if (looking-at remove-regexp) (delete-region (point) (save-excursion (forward-line 1) (point))) (forward-line 1))) (goto-char (point-min)) ;; Bogofilter really wants From envelopes for counting articles. ;; Fake one at the beginning, make sure there will be no other. (if (looking-at "From ") (forward-line 1) (insert "From nobody " (current-time-string) "\n")) (let (case-fold-search) (while (re-search-forward "^From " nil t) (beginning-of-line) (insert ">"))) (process-send-region process (point-min) (point-max)) (erase-buffer)) (setq counter (1+ counter)) (when (and prefix (zerop (% counter 10))) (message "%s %d" prefix counter)))) ;; Sending the EOF is unwind-protected. This is to prevent lost copies ;; of `bogofilter', hung on reading their standard input, in case the ;; whole registering process gets interrupted by the user. (process-send-eof process)) (when prefix (message "%s %d" prefix counter)) (kill-buffer article-copy) (while (memq (process-status process) '(run stop)) (accept-process-output process)) (when prefix (message "%s done!" prefix)))) (provide 'spam) ;;; spam.el ends here. --=-=-=--