├── .gitattributes ├── .gitignore ├── README.md └── qmail-aliasfilter.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################# 2 | ## Eclipse 3 | ################# 4 | 5 | *.pydevproject 6 | .project 7 | .metadata 8 | bin/ 9 | tmp/ 10 | *.tmp 11 | *.bak 12 | *.swp 13 | *~.nib 14 | local.properties 15 | .classpath 16 | .settings/ 17 | .loadpath 18 | 19 | # External tool builders 20 | .externalToolBuilders/ 21 | 22 | # Locally stored "Eclipse launch configurations" 23 | *.launch 24 | 25 | # CDT-specific 26 | .cproject 27 | 28 | # PDT-specific 29 | .buildpath 30 | 31 | 32 | ################# 33 | ## Visual Studio 34 | ################# 35 | 36 | ## Ignore Visual Studio temporary files, build results, and 37 | ## files generated by popular Visual Studio add-ons. 38 | 39 | # User-specific files 40 | *.suo 41 | *.user 42 | *.sln.docstates 43 | 44 | # Build results 45 | 46 | [Dd]ebug/ 47 | [Rr]elease/ 48 | x64/ 49 | build/ 50 | [Bb]in/ 51 | [Oo]bj/ 52 | 53 | # MSTest test Results 54 | [Tt]est[Rr]esult*/ 55 | [Bb]uild[Ll]og.* 56 | 57 | *_i.c 58 | *_p.c 59 | *.ilk 60 | *.meta 61 | *.obj 62 | *.pch 63 | *.pdb 64 | *.pgc 65 | *.pgd 66 | *.rsp 67 | *.sbr 68 | *.tlb 69 | *.tli 70 | *.tlh 71 | *.tmp 72 | *.tmp_proj 73 | *.log 74 | *.vspscc 75 | *.vssscc 76 | .builds 77 | *.pidb 78 | *.log 79 | *.scc 80 | 81 | # Visual C++ cache files 82 | ipch/ 83 | *.aps 84 | *.ncb 85 | *.opensdf 86 | *.sdf 87 | *.cachefile 88 | 89 | # Visual Studio profiler 90 | *.psess 91 | *.vsp 92 | *.vspx 93 | 94 | # Guidance Automation Toolkit 95 | *.gpState 96 | 97 | # ReSharper is a .NET coding add-in 98 | _ReSharper*/ 99 | *.[Rr]e[Ss]harper 100 | 101 | # TeamCity is a build add-in 102 | _TeamCity* 103 | 104 | # DotCover is a Code Coverage Tool 105 | *.dotCover 106 | 107 | # NCrunch 108 | *.ncrunch* 109 | .*crunch*.local.xml 110 | 111 | # Installshield output folder 112 | [Ee]xpress/ 113 | 114 | # DocProject is a documentation generator add-in 115 | DocProject/buildhelp/ 116 | DocProject/Help/*.HxT 117 | DocProject/Help/*.HxC 118 | DocProject/Help/*.hhc 119 | DocProject/Help/*.hhk 120 | DocProject/Help/*.hhp 121 | DocProject/Help/Html2 122 | DocProject/Help/html 123 | 124 | # Click-Once directory 125 | publish/ 126 | 127 | # Publish Web Output 128 | *.Publish.xml 129 | *.pubxml 130 | 131 | # NuGet Packages Directory 132 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line 133 | #packages/ 134 | 135 | # Windows Azure Build Output 136 | csx 137 | *.build.csdef 138 | 139 | # Windows Store app package directory 140 | AppPackages/ 141 | 142 | # Others 143 | sql/ 144 | *.Cache 145 | ClientBin/ 146 | [Ss]tyle[Cc]op.* 147 | ~$* 148 | *~ 149 | *.dbmdl 150 | *.[Pp]ublish.xml 151 | *.pfx 152 | *.publishsettings 153 | 154 | # RIA/Silverlight projects 155 | Generated_Code/ 156 | 157 | # Backup & report files from converting an old project file to a newer 158 | # Visual Studio version. Backup files are not needed, because we have git ;-) 159 | _UpgradeReport_Files/ 160 | Backup*/ 161 | UpgradeLog*.XML 162 | UpgradeLog*.htm 163 | 164 | # SQL Server files 165 | App_Data/*.mdf 166 | App_Data/*.ldf 167 | 168 | ############# 169 | ## Windows detritus 170 | ############# 171 | 172 | # Windows image file caches 173 | Thumbs.db 174 | ehthumbs.db 175 | 176 | # Folder config file 177 | Desktop.ini 178 | 179 | # Recycle Bin used on file shares 180 | $RECYCLE.BIN/ 181 | 182 | # Mac crap 183 | .DS_Store 184 | 185 | 186 | ############# 187 | ## Python 188 | ############# 189 | 190 | *.py[co] 191 | 192 | # Packages 193 | *.egg 194 | *.egg-info 195 | dist/ 196 | build/ 197 | eggs/ 198 | parts/ 199 | var/ 200 | sdist/ 201 | develop-eggs/ 202 | .installed.cfg 203 | 204 | # Installer logs 205 | pip-log.txt 206 | 207 | # Unit test / coverage reports 208 | .coverage 209 | .tox 210 | 211 | #Translations 212 | *.mo 213 | 214 | #Mr Developer 215 | .mr.developer.cfg 216 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 'qmail-aliasfilter' is a smart filter script for all qmail lovers. 2 | 3 | It filters aliases by the domain part of the senders email address, 4 | e.g. mail to `nospam-.example.com@hostname.localhost` will only be delivered to maildir 5 | if the mail comes from the `*.example.com` domain, otherwise the mail will be bounced or dropped. 6 | 7 | Here is the example content of the `.qmail-nospam-default` script inside your home directory (`/home/jana`): 8 | 9 | |./qmail-aliasfilter.py 10 | jana 11 | 12 | 13 | From now on, you can easily register new accounts or newsletter subscriptions with your `nospam-...` email address 14 | e.g. `nospam-newsletter.example.com@hostname.localhost` for mailings from `newsletter.example.com` 15 | 16 | Let's see how the wildcard works. Here are some examples: 17 | - `nospam-example.com@...` -> `example.com` (the strictest, but also the best way to suppress spam) 18 | - `nospam-.example.com@...` -> `*.example.com` (and of course: example.com!) 19 | - `nospam-example.@...` -> `example.*` 20 | - `nospam-.example.@...` -> `*.example.*` (the loosest way, use only for debugging!) 21 | - `nospam-example.com+newsletter.example.org@...` -> a combination of the above methods 22 | 23 | There is also the possibility to use qmail-aliasfilter in combination with maildrop 24 | 25 | |./qmail-aliasfilter.py 26 | |maildrop 27 | jana 28 | 29 | ## See also 30 | 31 | - https://uberspace.de/dokuwiki/cool:qmail-aliasfilter 32 | 33 | ## Credits 34 | 35 | This script is originally by wibuni (github@wibuni.de) who seems to has vanished from the Internet. -------------------------------------------------------------------------------- /qmail-aliasfilter.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python2.7 2 | # -*- coding: utf-8 -*- 3 | 4 | # qmail-aliasfilter 5 | 6 | # 'qmail-aliasfilter' is a smart filter script for all qmail lovers. 7 | # It filters aliases by the domain part of the senders email address, 8 | # e.g. mail to nospam-.example.com@hostname.localhost will only be delivered to maildir 9 | # if the mail comes from the *.example.com domain, otherwise the mail will be bounced or dropped. 10 | 11 | # Here is the example content of the .qmail-nospam-default script inside your home directory (/home/jana): 12 | # |./qmail-aliasfilter.py 13 | # jana 14 | 15 | # From now on, you can easily register new accounts or newsletter subscriptions with your 'nospam-...' email address 16 | # e.g. nospam-newsletter.example.com@hostname.localhost for mailings from newsletter.example.com 17 | 18 | # Let's see how the wildcard works. Here are some examples: 19 | # (1) nospam-example.com@... -> example.com (the strictest, but also the best way to suppress spam) 20 | # (2) nospam-.example.com@... -> *.example.com (and of course: example.com!) 21 | # (3) nospam-example.@... -> example.* 22 | # (4) nospam-.example.@... -> *.example.* (the loosest way, use only for debugging!) 23 | # (5) nospam-example.com+newsletter.example.org@... -> a combination of the above methods 24 | 25 | # Since 2.1 there is also the possibility to use qmail-aliasfilter in combination with maildrop 26 | # Please take a look at the documentation (see @link) if you want to use it. 27 | 28 | # @package qmail-aliasfilter 29 | # @author originally by wibuni 30 | # @copyright Copyright (c) 2011-2012, see the @author tags 31 | # @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 32 | # @link originally at https://github.com/wibuni/qmail-aliasfilter 33 | # @link https://github.com/panzerfahrer/qmail-aliasfilter 34 | # @link https://uberspace.de/dokuwiki/cool:qmail-aliasfilter 35 | # @version 2.1.3 36 | 37 | # Let's rock it. 38 | 39 | import os 40 | import sys 41 | import time 42 | import email 43 | 44 | version = '2.1.3' 45 | # see @version 46 | 47 | raw_msg = ''.join(sys.stdin.readlines()) 48 | msg = email.message_from_string(raw_msg) 49 | # get the raw message from stdin and create a message object 50 | 51 | homedir = os.getenv('HOME') 52 | # the $HOME directory of the user, who uses qmail-aliasfilter 53 | 54 | # initializing counters 55 | i = 0 56 | match_found = 0 57 | 58 | # Any arguments passed within the call? 59 | try: 60 | if sys.argv[1] == '--maildrop': 61 | # If you want to use qmail-aliasfilter inside maildrop, use the --maildrop argument 62 | redirect_spam_into_maildir = True 63 | else: 64 | redirect_spam_into_maildir = False 65 | except: 66 | # catch exception if there is no argument passed 67 | redirect_spam_into_maildir = False 68 | 69 | try: 70 | default_alias = os.getenv('DEFAULT') 71 | # $DEFAULT environment variable locally set by qmail, if there is a '.qmail-default', 72 | # so $DEFAULT will be the replacement of the 'default'-part of the '.qmail-default' file name, 73 | # which matches the current recipient address. 74 | 75 | if redirect_spam_into_maildir: 76 | default_alias = msg['X-qmail-default'] 77 | del msg['X-qmail-default'] 78 | 79 | except: 80 | default_alias = '$DEFAULT_not_specified' 81 | 82 | try: 83 | sender_address = os.getenv('SENDER') 84 | # $SENDER environment variable locally set by qmail 85 | except: 86 | sender_address = '$SENDER_not_specified' 87 | 88 | try: 89 | recipient_address = os.getenv('RECIPIENT') 90 | # $RECIPIENT environment variable locally set by qmail 91 | except: 92 | recipient_address = '$RECIPIENT_not_specified' 93 | 94 | 95 | 96 | # debug: 97 | # default_alias = 'test.example.com' 98 | # sender_address = 'no-reply@another.subdomain.whatever.test.example.com' 99 | # recipient_address = 'username@hostname.localhost' 100 | # logfile = open('%s/qmail-aliasfilter.log' %homedir, 'a') 101 | # print >> logfile, '[%s] [DEBUG] $DEFAULT = %s' %(time.asctime(), default_alias) 102 | # logfile.close() 103 | 104 | sender_hostname = str.split(sender_address, '@') # strip the local part of the email address 105 | amount_of_dots = str.count(sender_hostname[1], '.') # counting the dots inside the domain part of the email address 106 | current_alias = str.split(default_alias, '+') # if you use example (5), we need to split and check each of the aliases 107 | 108 | while i <= str.count(default_alias, '+'): 109 | # there is at least one alias, so the while loop will always be entered 110 | # if there are more than one alias (usage like example (5) with the plus sign), each alias will be checked 111 | 112 | amount_of_aliasdots = str.count(current_alias[i], '.') # counting the dots inside the currently checked alias 113 | 114 | if len(str.strip(current_alias[i], '.')) == 0: 115 | # case: the alias contains only dots; no need to do anything else than logging and rejecting the mail. 116 | pass 117 | else: 118 | # case: see examples (1), (2), (3), (4) for further details 119 | # let's check if the hostname matches the given alias 120 | 121 | if sender_hostname[1] == current_alias[i]: 122 | # case (1): nospam-example.com@... -> accepts all incoming mail from example.com 123 | # nothing more to do 124 | match_found = match_found+1 # Yeah! We found a match. 125 | 126 | elif (str.find(current_alias[i], '.') == 0) and (str.rfind(current_alias[i], '.') != len(current_alias[i])-1): 127 | # case (2): nospam-.example.com@... -> accepts all incoming mail from *.example.com 128 | wildcard_alias = sender_hostname[1] 129 | for j in xrange(amount_of_aliasdots, amount_of_dots+1): 130 | # iterate and delete the subdomains until we got exactly as many dots as the alias 131 | wildcard_alias = str.partition(wildcard_alias, '.') 132 | wildcard_alias = wildcard_alias[2] 133 | wildcard_alias = '.'+wildcard_alias 134 | if wildcard_alias == current_alias[i]: 135 | match_found = match_found+1 # Yeah! We found a match. 136 | 137 | elif (str.find(current_alias[i], '.') != 0) and (str.rfind(current_alias[i], '.') == len(current_alias[i])-1): 138 | # case (3): nospam-example.@... -> accepts all incoming mail from example.* 139 | # we have to strip of the TLD part of the hostname 140 | wildcard_alias = str.rpartition(sender_hostname[1], '.') 141 | wildcard_alias = wildcard_alias[0]+wildcard_alias[1] 142 | if wildcard_alias == current_alias[i]: 143 | match_found = match_found+1 # Yeah! We found a match. 144 | 145 | elif (str.find(current_alias[i], '.') == 0) and (str.rfind(current_alias[i], '.') == len(current_alias[i])-1): 146 | # case (4): nospam-.example.@... -> accepts all incoming mail from *.example.* 147 | wildcard_alias = sender_hostname[1] 148 | for j in xrange(amount_of_aliasdots, amount_of_dots+1): 149 | # iterate and delete the subdomains until we got exactly as many dots as the alias 150 | wildcard_alias = str.partition(wildcard_alias, '.') 151 | wildcard_alias = wildcard_alias[2] 152 | wildcard_alias = '.'+wildcard_alias 153 | # and finally we have to strip of the TLD part of the hostname 154 | wildcard_alias = str.rpartition(wildcard_alias, '.') 155 | wildcard_alias = wildcard_alias[0]+wildcard_alias[1] 156 | if wildcard_alias == current_alias[i]: 157 | match_found = match_found+1 # Yeah! We found a match. 158 | 159 | i = i+1 160 | 161 | if match_found >= 1: 162 | # the domain part of the senders email address matches the $DEFAULT alias of qmail 163 | 164 | if redirect_spam_into_maildir: 165 | # Add some identifier headers (to spread qmail-aliasfilter around the world!) 166 | 167 | try: 168 | # and add some identifier headers 169 | msg.replace_header('X-qmail-aliasfilter-Version', 'qmail-aliasfilter %s' %version) 170 | except: 171 | msg.add_header('X-qmail-aliasfilter-Version', 'qmail-aliasfilter %s' %version) 172 | try: 173 | msg.replace_header('X-qmail-aliasfilter-Spam-Status', 'No, tests=FROM_HOSTNAME_IN_DEFAULT_ALIAS version=%s' %version) 174 | except: 175 | msg.add_header('X-qmail-aliasfilter-Spam-Status', 'No, tests=FROM_HOSTNAME_IN_DEFAULT_ALIAS version=%s' %version) 176 | 177 | sys.stdout.writelines(msg.as_string()) # and put message back to stdout 178 | else: 179 | # Let's write this into our logfile (we don't need a logfile with the --maildrop argument, because maildrop uses own logfile) 180 | logfile = open('%s/qmail-aliasfilter.log' %homedir, 'a') 181 | print >> logfile, '[%s] [Accepted] qmail-aliasfilter accepted an email from \'%s\' to \'%s\'' %(time.asctime(), sender_address, recipient_address) 182 | logfile.close() 183 | 184 | sys.exit(0) # everything's fine, let's deliver new mail. 185 | else: 186 | # Oops, we got an error! 187 | 188 | if redirect_spam_into_maildir: 189 | # Don't drop the message, just add some special spam headers 190 | 191 | subj = msg['Subject'] # the original message subject 192 | del msg['Subject'] # now we replace the message subject 193 | msg['Subject'] = '[qmail-aliasfiltered] %s' %subj # with [qmail-aliasfiltered] %Subject 194 | 195 | try: 196 | msg.replace_header('X-qmail-aliasfilter-Version', 'qmail-aliasfilter %s' %version) # and add some identifier headers 197 | except: 198 | msg.add_header('X-qmail-aliasfilter-Version', 'qmail-aliasfilter %s' %version) 199 | try: 200 | msg.replace_header('X-qmail-aliasfilter-Spam-Status', 'Yes, tests=FROM_HOSTNAME_NOT_IN_DEFAULT_ALIAS version=%s' %version) 201 | except: 202 | msg.add_header('X-qmail-aliasfilter-Spam-Status', 'Yes, tests=FROM_HOSTNAME_NOT_IN_DEFAULT_ALIAS version=%s' %version) 203 | try: 204 | msg.replace_header('X-qmail-aliasfilter-Spam-Flag', 'YES') # and the Spam-Flag attribute header 205 | except: 206 | msg.add_header('X-qmail-aliasfilter-Spam-Flag', 'YES') 207 | 208 | sys.stdout.writelines(msg.as_string()) # and put message back to stdout 209 | sys.exit(0) 210 | else: 211 | # Let's write this into our logfile (we don't need a logfile with the --maildrop argument, because maildrop uses own logfile) 212 | logfile = open('%s/qmail-aliasfilter.log' %homedir, 'a') 213 | print >> logfile, '[%s] [Rejected] qmail-aliasfilter rejected an email from \'%s\' to \'%s\'' %(time.asctime(), sender_address, recipient_address) 214 | logfile.close() 215 | 216 | #sys.exit(99) # silently drop message 217 | sys.exit(100) # or bounce message (hard error) and return message from mailer daemon 218 | # sys.exit(111) # or reject message temporarily (soft error). not preferable for most spam but for debbuging! 219 | 220 | # End of file qmail-aliasfilter.py 221 | # Location: ./qmail-aliasfilter.py 222 | --------------------------------------------------------------------------------