├── .gitignore ├── LICENSE ├── README.md ├── bitmessage-gateway.py ├── bitmessage-lmtpd.py ├── lib ├── __init__.py ├── badparsers.py ├── bmapi.py ├── bmfolder.py ├── bminbox.py ├── bmlogging.py ├── bmmega.py ├── bmmessage.py ├── bmsignal.py ├── charset.py ├── config.py ├── gpg.py ├── lmtpd.py ├── maintenance.py ├── milter.py ├── msgtemplate.py ├── mysql.py ├── netstring.py ├── parsers.py ├── payment.py ├── sendbm.py ├── singleton.py ├── socketmap.py ├── unicode.py └── user.py ├── templates ├── accountexpired.txt ├── command-invalid.txt ├── configchange.txt ├── confignochange.txt ├── deregistration-confirmed.txt ├── invoice.txt ├── registration-confirmed.txt ├── registration-duplicate.txt ├── registration-invalid.txt ├── relay-missing-recipient.txt ├── relay-throttle.txt ├── smtperror.txt └── status.txt └── walletnotify.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | lib/*.pyc 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | bitmessage-email-gateway 2 | ======================== 3 | 4 | This software is a bi-directional email gateway implementation for the Bitmessage network. 5 | 6 | This software will take a standard email MTA setup using a catchall address and MailDir as its storage type and will act as a email <-> bitmessage gateway. 7 | 8 | For more information about Bitmessage, please visit https://www.bitmessage.org 9 | 10 | 11 | 12 | ## Features 13 | * Allows users to register for and deregister from the service via Bitmessage 14 | * Drops message requests from unregistered users 15 | * Allows registered users to send emails from their registered account to the Internet using their Bitmessage client software 16 | * Allows Internet users to send messages to registered users using the user's @domain.com email address 17 | * Auto purges bitmessages and emails immediately after sending 18 | * Transparently encrypts emails destined for addresses with published PGP keys (multiple key servers supported) 19 | 20 | ## System Requirements 21 | * Debian 7 (other Linux distributions will also work) 22 | * Python 2.7.5 (most stable version for the Bitmessage application) 23 | * Bitmessage with API enabled (see https://bitmessage.org/wiki/Compiling_instructions and https://bitmessage.org/wiki/API_Reference) 24 | * Postfix (or similar MTA) setup with a catchall-address @your-domain.com that forwards all messages to the user 'bitmessage' and uses the MailDir storage method 25 | 26 | ## Python Dependencies 27 | * Gnupg 28 | * BeautifulSoup3 29 | * Possibly others that I have forgotten about 30 | 31 | ## Installation Instructions 32 | * Download and install Bitmessage. Make sure it is running and listening via it's API port (https://bitmessage.org/wiki/API_Reference) 33 | * Add a linux user for the software 34 | ``` 35 | useradd bitmessage 36 | passwd bitmessage 37 | su bitmessage 38 | ``` 39 | * Install Postfix (or other MTA) for your domain name and make sure a catch-all address is set up to forward all emails to bitmessage@your-domain.com 40 | * Make sure Postfix (or other MTA) is configured to use MailDir as its storage method! You can send a test email to catchall@your-domain.com to see if it's delivered to /home/bitmessage/MailDir/new 41 | * Download the newest version of this software and unzip 42 | ``` 43 | wget https://github.com/darkVPN/bitmessage-email-gateway/archive/master.zip 44 | unzip master.zip 45 | rm -rf ./master.zip 46 | cd bitmessage-email-gateway/ 47 | ``` 48 | * Open bitmessage-gateway.py and edit the Bitmessage API connection settings: 49 | ``` 50 | ## API connection information 51 | api = { 52 | 'conn' : '', 53 | 'username' : 'your-bitmessage-api-user', 54 | 'password' : 'your-bitmessage-api-password', 55 | 'host' : '127.0.0.1', 56 | 'port' : '8442' 57 | 58 | } 59 | ``` 60 | * Next, modify the application's general settings: 61 | ``` 62 | ## system configuration details 63 | config = { 64 | 'domain_name' : 'your-domain.com', 65 | 'mail_folder' : '/home/bitmessage/MailDir/new/', 66 | 'log_filename' : '/var/log/bitmessage-gateway.log', 67 | 'process_interval' : 10, 68 | 'receiving_address_label' : 'your-domain.com Generic Receive Address', 69 | 'sending_address_label' : 'your-domain.com Generic Sender Address', 70 | 'registration_address_label' : 'your-domain.com Registration Address', 71 | 'deregistration_address_label' : 'your-domain.com Deregistration Address', 72 | 'bug_report_address_bitmessage' : 'you-bitmessage-address', 73 | 'bug_report_address_email' : 'your-email', 74 | 'debug' : True, 75 | 'respond_to_invalid' : True 76 | } 77 | ``` 78 | * Setup the log file for use: 79 | ``` 80 | touch /var/log/bitmessage-gateway.log 81 | chown bitmessage:bitmessage /var/log/bitmessage-gateway.log 82 | ``` 83 | * Add the required bitmessage addresses for your service (registration, deregistration, sender, and receiver'): 84 | ``` 85 | chmod +x bitmessage-gateway.py 86 | ./bitmessage-gateway.py -a 'your-domain.com Generic Receive Address' 87 | ./bitmessage-gateway.py -a 'your-domain.com Generic Sender Address' 88 | ./bitmessage-gateway.py -a 'your-domain.com Registration Address' 89 | ./bitmessage-gateway.py -a 'your-domain.com Deregistration Address' 90 | ``` 91 | * Check to make sure all required addresses have been added successfully. Note: you should see the four labels (and corresponding addresses) that you set in your config! 92 | ``` 93 | ./bitmessage-gateway.py -l 94 | ``` 95 | An example response: 96 | ``` 97 | #################################### 98 | Internal Address List 99 | #################################### 100 | goDark Deregistration Address BM-2cTDKufxNFY6iAafxartJUHodHDQ8BabNR 101 | goDark Registration Address BM-2cX1rp2LmTxn2yZERVuMGqCNuTbBwqLA4e 102 | goDark Generic Receive Address BM-2cW5Yvp5x9mL8gwjGdm65H9ombKG6JvRHg 103 | goDark Generic Sender Address BM-2cWPQvSfwEzDnG8xd8DGwz1p3Lj8FGk3tT 104 | ``` 105 | * Now it's time to distribute these addresses to your users via a website, Twitter, etc! 106 | 107 | When users send a message to your 'domain.com Registration Address' bitmessage address with their desired username in the subject field, the system will automatically register them and send a welcome email. This welcome email describes how to use the system and how to contact you about bugs/comments. You can change the welcome email content by editing the bitmessage-gateway.py script. 108 | 109 | Users can deregister by sending a message to the 'domain.com Deregistration Address' bitmessage address listed in the last step. 110 | 111 | Users can send outbound emails to the Internet by sending a message to the 'domain.com Receive Address' bitmessage address with the destination email in the subject field. Outbound emails destined for addresses with known public PGP keys will be encrypted automatically. Email responses will automatically be forwarded to your users. 112 | 113 | People of the Internets can send emails to your users by simply emailing their username@domain.com address. 114 | 115 | * Run the application! 116 | ``` 117 | ./bitmessage-gateway.py 118 | ``` 119 | 120 | ## Comments / Bug Reports 121 | Fork away if you're a developer ;) 122 | 123 | -------------------------------------------------------------------------------- /bitmessage-gateway.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | ## imports 4 | import os 5 | import re 6 | import time 7 | import datetime 8 | import argparse 9 | import logging 10 | import signal 11 | import sys 12 | import threading 13 | import xmlrpclib 14 | import json 15 | import smtplib 16 | import base64 17 | import email 18 | import fcntl 19 | import html2text 20 | import lib.gpg 21 | import lib.bminbox 22 | import lib.bmsignal 23 | import lib.bmmega 24 | #import lib.sendingtemplates 25 | import lib.payment 26 | import lib.milter 27 | import Milter 28 | from lib.config import BMConfig 29 | from lib.mysql import BMMySQL 30 | from lib.bmlogging import BMLogging 31 | from lib.bmapi import BMAPI 32 | from lib.sendbm import SendBMTemplate, SendBM 33 | from lib.bmmessage import BMMessage 34 | import lib.maintenance 35 | import lib.charset 36 | import lib.user 37 | import random 38 | from subprocess import call 39 | 40 | from BeautifulSoup import BeautifulSoup 41 | from email.parser import Parser 42 | from email.header import decode_header 43 | from email.MIMEMultipart import MIMEMultipart 44 | from email.MIMEText import MIMEText 45 | 46 | from email.mime.multipart import MIMEMultipart 47 | from email.mime.text import MIMEText 48 | from email.header import Header 49 | from email import Charset 50 | from email.generator import Generator 51 | 52 | try: 53 | import pyinotify 54 | have_inotify = True 55 | except ImportError: 56 | have_inotify = False 57 | 58 | import pprint 59 | 60 | ## setup logging 61 | BMLogging() 62 | 63 | ## check if username is banned 64 | def is_banned_username(username): 65 | if username in BMConfig().get("bmgateway", "banned_usernames"): 66 | return True 67 | else: 68 | return False 69 | 70 | ## read email from file 71 | def read_email(k): 72 | try: 73 | f = open(BMConfig().get("bmgateway", "bmgateway", "mail_folder") + k, 'r') 74 | message = f.read() 75 | return message 76 | except IOError: 77 | logging.error('Could not read email: ' + BMConfig().get("bmgateway", "bmgateway", "mail_folder") + k) 78 | return 79 | 80 | 81 | ## delete email from file 82 | def delete_email(k): 83 | try: 84 | os.remove(BMConfig().get("bmgateway", "bmgateway", "mail_folder") + k) 85 | except OSError: 86 | logging.error('Could not delete email: ' + BMConfig().get("bmgateway", "bmgateway", "mail_folder") + k) 87 | 88 | ## save email into another directory 89 | def save_email(k): 90 | savedir = BMConfig().get("bmgateway", "bmgateway", "mail_folder") + "../cur/" 91 | try: 92 | os.rename(BMConfig().get("bmgateway", "bmgateway", "mail_folder") + k, 93 | savedir + k) 94 | except OSError: 95 | logging.error('Could not save email: ' + BMConfig().get("bmgateway", "bmgateway", "mail_folder") + k + " to " + savedir) 96 | 97 | 98 | ## generate a bitmessage address for an incoming email adress 99 | def generate_sender_address(email): 100 | ## generate random address 101 | time_start = time.time() 102 | address = BMAPI().conn().createRandomAddress(base64.b64encode(email)) 103 | time_stop = time.time() 104 | time_total = int(time_stop - time_start) 105 | logging.info('Generated sender address for ' + email + ' in ' + str(time_total) + ' seconds') 106 | 107 | return address 108 | 109 | ## check for new bitmessages 110 | def get_inbox(): 111 | while True: 112 | try: 113 | messages = json.loads(BMAPI().conn().getAllInboxMessages())['inboxMessages'] 114 | except: 115 | logging.warn('Could not read inbox messages via API %s. Retrying...', sys.exc_info()[0]) 116 | BMAPI().disconnect() 117 | time.sleep(random.random()+0.5) 118 | continue 119 | break 120 | return messages 121 | 122 | def get_outbox(): 123 | while True: 124 | try: 125 | messages = json.loads(BMAPI().conn().getAllSentMessages())['sentMessages'] 126 | except: 127 | logging.warn('Could not read outbox messages via API %s. Retrying...', sys.exc_info()[0]) 128 | BMAPI().disconnect() 129 | time.sleep(random.random()+0.5) 130 | continue 131 | break 132 | return messages 133 | 134 | ## send outbound email 135 | def send_email(recipient, sender, subject, body, bm_id, userdata = None): 136 | ## open connection 137 | server = smtplib.SMTP('localhost') 138 | server.set_debuglevel(0) 139 | 140 | ## build message 141 | msg = MIMEMultipart() 142 | msg['From'] = sender 143 | msg['To'] = recipient 144 | msg['Subject'] = subject 145 | 146 | ## Signature 147 | if BMConfig().get("bmgateway", "bmgateway", "signature") is not None: 148 | body += "-- \n" + \ 149 | BMConfig().get("bmgateway", "bmgateway", "signature") + \ 150 | "\n" 151 | 152 | enc_body = None 153 | sender_key = None 154 | recipient_key = None 155 | sign = BMConfig().get("bmgateway", "pgp", "sign") 156 | encrypt = BMConfig().get("bmgateway", "pgp", "encrypt") 157 | 158 | if userdata: 159 | if userdata.expired(): 160 | sign = False 161 | encrypt = False 162 | else: 163 | # only override if not expired and pgp allowed globally 164 | if sign: 165 | sign = (userdata.pgp == 1) 166 | if encrypt: 167 | encrypt = (userdata.pgp == 1) 168 | 169 | #TODO find out if already encrypted/signed 170 | 171 | ## generate a signing key if we dont have one 172 | if sign: 173 | if not lib.gpg.check_key(sender, whatreturn="key", operation="sign"): 174 | lib.gpg.create_primary_key(sender) 175 | sender_key = lib.gpg.check_key(sender, whatreturn="key", operation="sign") 176 | if not sender_key: 177 | logging.error('Could not find or upload user\'s keyid: %s', sender) 178 | 179 | ## search for recipient PGP key 180 | if encrypt: 181 | # if lib.gpg.find_key(recipient): 182 | recipient_key = lib.gpg.check_key(recipient, whatreturn="key", operation="encrypt") 183 | if not recipient_key: 184 | logging.info('Could not find recipient\'s keyid, not encrypting: %s', recipient) 185 | # make sure sender has an encryption key 186 | if not lib.gpg.check_key(sender, whatreturn="key", operation="encrypt"): 187 | if not lib.gpg.check_key(sender, whatreturn="key", operation="sign"): 188 | lib.gpg.create_primary_key(sender) 189 | lib.gpg.create_subkey(sender) 190 | 191 | if sender_key and recipient_key: 192 | enc_body = lib.gpg.encrypt_text(body, recipient_key, sender_key) 193 | logging.info('Encrypted and signed outbound mail from %s to %s', sender, recipient) 194 | elif recipient_key: 195 | enc_body = lib.gpg.encrypt_text(body, recipient_key) 196 | logging.info('Encrypted outbound mail from %s to %s', sender, recipient) 197 | elif sender_key and not recipient == BMConfig().get("bmgateway", "bmgateway", "bug_report_address_email"): 198 | logging.info('Signed outbound mail from %s to %s', sender, recipient) 199 | enc_body = lib.gpg.sign_text(body, sender_key) 200 | 201 | ## only encrypt if the operation was successful 202 | if enc_body: 203 | body = enc_body 204 | 205 | text = body 206 | 207 | ## encode as needed 208 | body = MIMEText(body, 'plain') 209 | 210 | ## attach body with correct encoding 211 | msg.attach(body) 212 | text = msg.as_string() 213 | 214 | ## send message 215 | try: 216 | status = server.sendmail(sender, recipient, text, [], ["NOTIFY=SUCCESS,FAILURE,DELAY", "ORCPT=rfc822;" + recipient]) 217 | logging.info('Sent email from %s to %s', sender, recipient) 218 | BMMessage.deleteStatic(bm_id, folder = "inbox") 219 | ## send failed 220 | 221 | except smtplib.SMTPException as e: 222 | logging.error('Could not send email from %s to %s: %s', sender, recipient, e) 223 | server.quit() 224 | for rcpt in e.recipients: 225 | return e.recipients[rcpt] 226 | 227 | server.quit() 228 | return 229 | 230 | ## list known addresses 231 | def list_addresses(): 232 | ## print all addresses 233 | print "\n####################################\nInternal Address List\n####################################" 234 | for tmp_email in BMAPI().address_list: 235 | print tmp_email + "\t\t\t" + BMAPI().address_list[tmp_email] 236 | print '' 237 | 238 | print "\n####################################\nUser List\n####################################" 239 | lib.user.GWUser() 240 | print "" 241 | 242 | 243 | def str_timestamp(): 244 | return time.strftime("Generated at: %b %d %Y %H:%M:%S\r\n", time.gmtime()) 245 | 246 | ## delete address 247 | def delete_address(address): 248 | ## try to delete and don't worry about if it actually goes through 249 | BMAPI().conn().deleteAddressBookEntry(address) 250 | BMAPI().conn().deleteAddress(address) 251 | lib.user.GWUser(bm = address).delete() 252 | 253 | if BMConfig().get("bmgateway", "bmgateway", "debug"): 254 | logging.debug('Deleted bitmessage address, ' + address) 255 | 256 | def field_in_list(message, address_list, 257 | message_field, list_field): 258 | result = False 259 | try: 260 | if message[message_field] == address_list[BMConfig().get("bmgateway", "bmgateway", list_field)]: 261 | result = True 262 | except: 263 | result = False 264 | return result 265 | 266 | 267 | ## check for new bitmessages to process 268 | def check_bminbox(intcond): 269 | global interrupted 270 | ## get all messages 271 | #all_messages = json.loads(api['conn'].getAllInboxMessages())['inboxMessages'] 272 | 273 | logging.info("Entering BM inbox checker loop") 274 | intcond.acquire() 275 | while not interrupted: 276 | all_messages = get_inbox() 277 | 278 | ## if no messages 279 | if not all_messages: 280 | try: 281 | intcond.wait(BMConfig().get("bmgateway", "bmgateway", "process_interval")) 282 | except KeyboardInterrupt: 283 | break 284 | continue 285 | 286 | ## loop through messages to find unread 287 | for a_message in all_messages: 288 | 289 | ## if already read, delete and break 290 | if a_message['read'] == 1: 291 | BMMessage.deleteStatic(a_message['msgid']) 292 | continue 293 | 294 | ## check if already processed, maybe from another instance 295 | if lib.bminbox.check_message_processed(a_message['msgid']): 296 | logging.info('Message %s has already been processed deleting...', a_message['msgid']) 297 | # BMMessage.deleteStatic(a_message['msgid']) 298 | # continue 299 | 300 | ## if the message is unread, load it by ID to trigger the read flag 301 | message = json.loads(BMAPI().conn().getInboxMessageByID(a_message['msgid'], False))['inboxMessage'][0] 302 | 303 | ## if a blank message was returned 304 | if not message: 305 | logging.error('API returned blank message when requesting a message by msgID') 306 | delete_bitmessage_inbox(bm_id) 307 | BMMessage.deleteStatic(a_message['msgid']) 308 | continue 309 | 310 | ## find message ID 311 | bm_id = message['msgid'] 312 | 313 | ## check if receive address is a DEregistration request 314 | if field_in_list(message, BMAPI().address_list, 315 | 'toAddress', 'deregistration_address_label'): 316 | 317 | ## check if address is registered 318 | userdata = lib.user.GWUser(bm = message['fromAddress']) 319 | 320 | ## if the sender is actually registered and wants to deregister 321 | if userdata.check(): 322 | ## process deregistration 323 | logging.info('Processed deregistration request for user ' + userdata.email) 324 | delete_address(message['fromAddress']) 325 | 326 | ## send deregistration confirmation email 327 | SendBMTemplate( 328 | sender = BMAPI().get_address(BMConfig().get("bmgateway", "bmgateway", "deregistration_address_label")), 329 | recipient = message['fromAddress'], 330 | template = "deregistration-confirmed", 331 | addmaps = { 332 | 'email': userdata.email 333 | }) 334 | 335 | ## bogus deregistration request 336 | else: 337 | logging.warn('Purged malicious deregistration bitmessage from ' + message['fromAddress']) 338 | 339 | elif field_in_list(message, BMAPI().address_list, 'toAddress', 'bug_report_address_label'): 340 | userdata = lib.user.GwUser(bm = message['fromAddress']) 341 | # if not, create a fake one 342 | # relay to ticket 343 | 344 | ## check if receive address is a registration request 345 | elif field_in_list(message, BMAPI().address_list, 346 | 'toAddress', 'registration_address_label'): 347 | 348 | userdata = lib.user.GWUser(bm = message['fromAddress']) 349 | 350 | if userdata.check(): # status, config, etc 351 | command = base64.b64decode(message['subject']).lower() 352 | if command == "config": 353 | logging.info('Config request from %s', message['fromAddress']) 354 | body = base64.b64decode(message['message']) 355 | data = {} 356 | for line in body.splitlines(): 357 | line = re.sub("#.*", "", line) 358 | option = re.search("(\S+)\s*:\s*(\S+)", line) 359 | if option is None: 360 | continue 361 | if option.group(1).lower() == "pgp": 362 | data['pgp'] = lib.user.GWUserData.pgp(option.group(2)) 363 | elif option.group(1).lower() == "attachments": 364 | data['attachments'] = lib.user.GWUserData.zero_one(option.group(2)) 365 | #elif option.group(1).lower() == "flags": 366 | #data['flags'] = lib.user.GWUserData.numeric(option.group(2)) 367 | elif option.group(1).lower() == "archive": 368 | data['archive'] = lib.user.GWUserData.zero_one(option.group(2)) 369 | elif option.group(1).lower() == "masterpubkey_btc": 370 | data['masterpubkey_btc'] = lib.user.GWUserData.public_seed(option.group(2)) 371 | # reset offset unless set explicitly 372 | if data['masterpubkey_btc'] is not None and not 'offset_btc' in data: 373 | data['offset_btc'] = "0" 374 | elif option.group(1).lower() == "offset_btc": 375 | data['offset_btc'] = lib.user.GWUserData.numeric(option.group(2)) 376 | elif option.group(1).lower() == "feeamount": 377 | data['feeamount'] = lib.user.GWUserData.numeric(option.group(2), 8) 378 | elif option.group(1).lower() == "feecurrency": 379 | data['feecurrency'] = lib.user.GWUserData.currency(option.group(2)) 380 | else: 381 | pass 382 | if userdata.update(data): 383 | SendBMTemplate( 384 | sender = BMAPI().get_address(BMConfig().get("bmgateway", "bmgateway", "registration_address_label")), 385 | recipient = message['fromAddress'], 386 | template = "configchange", 387 | addmaps = { 388 | }) 389 | else: 390 | SendBMTemplate( 391 | sender = BMAPI().get_address(BMConfig().get("bmgateway", "bmgateway", "registration_address_label")), 392 | recipient = message['fromAddress'], 393 | template = "confignochange", 394 | addmaps = { 395 | }) 396 | pass 397 | elif command == "status" or command == "" or not command: 398 | logging.info('Status request from %s', message['fromAddress']) 399 | SendBMTemplate( 400 | sender = BMAPI().get_address(BMConfig().get("bmgateway", "bmgateway", "registration_address_label")), 401 | recipient = message['fromAddress'], 402 | template = "status", 403 | addmaps = { 404 | 'email': userdata.email, 405 | 'domain': userdata.domain, 406 | 'active': "Yes" if userdata.active else "No", 407 | 'cansend': "Yes" if userdata.cansend else "No", 408 | 'cancharge': "Yes" if userdata.cancharge else "No", 409 | 'caninvoice': "Yes" if userdata.caninvoice else "No", 410 | 'pgp': "server" if userdata.pgp else "local", 411 | 'attachments': "Yes" if userdata.attachments else "No", 412 | 'expires': userdata.exp.strftime("%B %-d %Y"), 413 | 'masterpubkey_btc': userdata.masterpubkey_btc if userdata.masterpubkey_btc else "N/A", 414 | 'offset_btc': str(userdata.offset_btc) if userdata.masterpubkey_btc else "N/A", 415 | 'feeamount': str(userdata.feeamount) if userdata.masterpubkey_btc else "N/A", 416 | 'feecurrency': str(userdata.feecurrency) if userdata.masterpubkey_btc else "N/A", 417 | 'archive': "Yes" if userdata.archive else "No", 418 | 'flags': hex(userdata.flags), 419 | 'aliases': ', '.join(userdata.aliases) if userdata.aliases else "None" 420 | }) 421 | else: 422 | logging.info('Invalid command from %s', message['fromAddress']) 423 | SendBMTemplate( 424 | sender = BMAPI().get_address(BMConfig().get("bmgateway", "bmgateway", "registration_address_label")), 425 | recipient = message['fromAddress'], 426 | template = "command-invalid", 427 | addmaps = { 428 | 'command': command, 429 | 'email': userdata.email 430 | }) 431 | 432 | else: # attempt to register new user 433 | ## find requested username 434 | proposed_registration_user = base64.b64decode(message['subject']).lower() 435 | 436 | #full_registration_user = registration_user + '@' + BMConfig().get("bmgateway", "bmgateway", "domain_name") 437 | valid_one = re.match('^[\w]{4,20}$', proposed_registration_user) is not None 438 | valid_two = re.match('^[\w]{4,20}@' + BMConfig().get("bmgateway", "bmgateway", "domain_name") + '$', proposed_registration_user) is not None 439 | 440 | # strip domain if they sent it during registration 441 | if valid_one: 442 | full_registration_user = proposed_registration_user.lower() + '@' + BMConfig().get("bmgateway", "bmgateway", "domain_name") 443 | registration_user = proposed_registration_user.lower() 444 | elif valid_two: 445 | full_registration_user = proposed_registration_user.lower() 446 | registration_user = proposed_registration_user.split('@')[0].lower() 447 | else: 448 | logging.info('Invalid email address in registration request for %s', proposed_registration_user) 449 | SendBMTemplate( 450 | sender = BMAPI().get_address(BMConfig().get("bmgateway", "bmgateway", "registration_address_label")), 451 | recipient = message['fromAddress'], 452 | template = "registration-invalid", 453 | addmaps = { 454 | 'email': proposed_registration_user 455 | }) 456 | BMMessage.deleteStatic(bm_id) 457 | continue 458 | 459 | ## if username is valid check if it's available 460 | 461 | ## check if address is already registered to a username or is banned 462 | if is_banned_username(registration_user): 463 | logging.info('Banned email address in registration request for %s', registration_user) 464 | SendBMTemplate( 465 | sender = BMAPI().get_address(BMConfig().get("bmgateway", "bmgateway", "registration_address_label")), 466 | recipient = message['fromAddress'], 467 | template = "registration-duplicate", 468 | addmaps = { 469 | 'email': full_registration_user 470 | }) 471 | BMMessage.deleteStatic(bm_id) 472 | continue 473 | elif lib.user.GWUser(email = full_registration_user).check(): 474 | logging.info('Duplicate email address in registration request for %s', registration_user) 475 | SendBMTemplate( 476 | sender = BMAPI().get_address(BMConfig().get("bmgateway", "bmgateway", "registration_address_label")), 477 | recipient = message['fromAddress'], 478 | template = "registration-duplicate", 479 | addmaps = { 480 | 'email': full_registration_user 481 | }) 482 | BMMessage.deleteStatic(bm_id) 483 | continue 484 | 485 | logging.info('Received registration request for email address %s ', full_registration_user) 486 | lib.user.GWUser(empty = True).add(bm = message['fromAddress'], email = full_registration_user) 487 | SendBMTemplate( 488 | sender = BMAPI().get_address(BMConfig().get("bmgateway", "bmgateway", "registration_address_label")), 489 | recipient = message['fromAddress'], 490 | template = "registration-confirmed", 491 | addmaps = { 492 | 'email': full_registration_user 493 | }) 494 | ## if sent to the generic recipient or sender address 495 | elif field_in_list(message, BMAPI().address_list, 496 | 'toAddress', 'relay_address_label'): 497 | 498 | ## if user is not registered, purge 499 | userdata = lib.user.GWUser(bm = message['fromAddress']) 500 | if not userdata.check(): 501 | if BMConfig().get("bmgateway", "bmgateway", "allow_unregistered_senders"): 502 | bm_sender = message['fromAddress'] + '@' + BMConfig().get("bmgateway", "bmgateway", "domain_name") 503 | else: 504 | logging.warn('Purged bitmessage from non-registered user ' + message['fromAddress']) 505 | BMMessage.deleteStatic(bm_id) 506 | continue 507 | 508 | ## if user is registered, find their username @ domain 509 | else: 510 | bm_sender = userdata.email 511 | 512 | ## find outbound email address 513 | bm_receiver = re.findall(r'[\w\.\+-]+@[\w\.-]+\.[\w]+', base64.b64decode(message['subject'])) 514 | if len(bm_receiver) > 0: 515 | bm_receiver = bm_receiver[0] 516 | 517 | ## if there is no receiver mapping or the generic address didnt get a valid outbound email, deny it 518 | if not bm_receiver: 519 | # FIXME explain to sender what is whrong 520 | logging.warn('Received and purged bitmessage with unknown recipient (likely generic address and bad subject)') 521 | if BMConfig().get("bmgateway", "bmgateway", "respond_to_missing"): 522 | SendBMTemplate( 523 | sender = message['toAddress'], 524 | recipient = message['fromAddress'], 525 | template = "relay-missing-recipient", 526 | addmaps = { 527 | 'email': userdata.email, 528 | }) 529 | BMMessage.deleteStatic(bm_id) 530 | continue 531 | 532 | # expired or cannot send 533 | if (userdata.expired() or userdata.cansend == 0) and not \ 534 | (bm_receiver == BMConfig().get("bmgateway", "bmgateway", "bug_report_address_email")): # can still contact bugreport 535 | btcaddress, amount = lib.payment.payment_exists_domain (BMConfig().get("bmgateway", "bmgateway", "domain_name"), userdata.bm) 536 | # create new one 537 | if btcaddress == False: 538 | btcaddress, amount = lib.payment.create_invoice_domain (BMConfig().get("bmgateway", "bmgateway", "domain_name"), userdata.bm) 539 | 540 | SendBMTemplate( 541 | sender = BMAPI().get_address(BMConfig().get("bmgateway", "bmgateway", "registration_address_label")), 542 | recipient = message['fromAddress'], 543 | template = "accountexpired", 544 | addmaps = { 545 | 'btcuri': lib.payment.create_payment_uri(btcaddress, 'BTC', amount, 546 | BMConfig().get("bmgateway", "bmgateway", "companyname"), 'User ' + userdata.bm + " / " + userdata.email + ' subscription'), 547 | 'service': 'Subscription for ' + userdata.email + ' from ' + datetime.date.today().strftime("%B %-d %Y") + 548 | ' until ' + userdata.exp.strftime("%B %-d %Y"), 549 | 'email': userdata.email 550 | }) 551 | logging.warn("User " + message['fromAddress'] + " notified of payment requirement") 552 | BMMessage.deleteStatic(bm_id) 553 | continue 554 | 555 | bm_subject = base64.b64decode(message['subject']) 556 | 557 | ## handle removal of embedded MAILCHUCK-FROM:: tag for replies 558 | bm_subject = bm_subject.replace('MAILCHUCK-FROM::' + bm_receiver + ' | ', ''); 559 | 560 | ## remove email address from subject 561 | if field_in_list(message, BMAPI().address_list, 'toAddress', 'relay_address_label'): 562 | bm_subject = bm_subject.replace(bm_receiver, '') 563 | 564 | ## get message contents 565 | bm_body = base64.b64decode(message['message']) 566 | 567 | ## pad with a newline, otherwise it may look ugly 568 | if bm_body[-1:] != '\n': 569 | bm_body += '\n' 570 | 571 | ## send message and delete bitmessage, bitches 572 | if (float(userdata.lastrelay) + BMConfig().get("bmgateway", "bmgateway", "throttle") > time.time()): 573 | SendBMTemplate( 574 | sender = message['toAddress'], 575 | recipient = message['fromAddress'], 576 | template = "relay-throttle", 577 | addmaps = { 578 | 'email': userdata.email, 579 | 'throttledelta': str(int((float(userdata.lastrelay) + 580 | BMConfig().get("bmgateway", "bmgateway", "throttle") - time.time() + 60)/60)) 581 | }) 582 | logging.warn('Throttled %s', message['fromAddress']) 583 | BMMessage.deleteStatic(bm_id) 584 | continue 585 | else: 586 | retval = send_email(bm_receiver, bm_sender, bm_subject, bm_body, bm_id, userdata = userdata) 587 | if retval is None: 588 | logging.info('Relayed from %s to %s', message['fromAddress'], bm_receiver) 589 | else: 590 | if retval[0] >= 400 and retval[0] < 500: 591 | # do not delete, repeatable 592 | continue 593 | else: 594 | SendBMTemplate( 595 | sender = message['toAddress'], 596 | recipient = message['fromAddress'], 597 | template = "smtperror", 598 | addmaps = { 599 | 'emailrcpt': bm_receiver, 600 | 'errcode': retval[0], 601 | 'errmessage': retval[1] 602 | } 603 | ) 604 | 605 | 606 | ## remove message 607 | BMMessage.deleteStatic(bm_id) 608 | lib.bminbox.set_message_processed(bm_id) 609 | intcond.wait(BMConfig().get("bmgateway", "bmgateway", "process_interval")) 610 | intcond.release() 611 | logging.info("Leaving BM inbox checker loop") 612 | 613 | def check_bmoutbox(intcond): 614 | global interrupted 615 | ## get all messages 616 | #all_messages = json.loads(api['conn'].getAllInboxMessages())['inboxMessages'] 617 | logging.info("Entering BM outbox checker loop") 618 | intcond.acquire() 619 | while not interrupted: 620 | all_messages = get_outbox() 621 | 622 | logging.info("Trashing old outbox messages") 623 | ## if no messages 624 | if not all_messages: 625 | try: 626 | intcond.wait(BMConfig().get("bmgateway", "bmgateway", "outbox_process_interval")) 627 | except KeyboardInterrupt: 628 | break 629 | continue 630 | 631 | ## loop through messages to find unread 632 | for a_message in all_messages: 633 | if a_message['status'] == 'ackreceived': 634 | userdata = lib.user.GWUser(bm = a_message['toAddress']) 635 | if userdata: 636 | userdata.setlastackreceived(a_message['lastActionTime']) 637 | BMMessage.deleteStatic(a_message['msgid'], folder = "outbox") 638 | 639 | logging.info("Vacuuming DB") 640 | result = BMAPI().conn().deleteAndVacuum() 641 | 642 | intcond.wait(BMConfig().get("bmgateway", "bmgateway", "outbox_process_interval")) 643 | intcond.release() 644 | logging.info("Leaving BM outbox checker loop") 645 | 646 | def check_boxes(): 647 | check_bminbox() 648 | check_bmoutbox() 649 | 650 | def handle_email(k): 651 | global address_list 652 | userdata = None 653 | 654 | ## read email from file 655 | msg_raw = read_email(k) 656 | if not msg_raw: 657 | logging.error('Could not open email file: ' + k) 658 | return 659 | 660 | 661 | ## extract header 662 | msg_headers = Parser().parsestr(msg_raw) 663 | 664 | ## check if email was valid 665 | if not msg_headers: 666 | logging.error('Malformed email detected and purged') 667 | delete_email(k) 668 | return 669 | 670 | ## find email source and dest addresses 671 | msg_sender = msg_headers["From"] 672 | 673 | ## failed delivery email 674 | if msg_sender == '<>' or not msg_sender: 675 | msg_sender = BMConfig().get("bmgateway", "bmgateway", "relay_address_label") 676 | else: 677 | try: 678 | msg_sender = re.findall(r'[\w\.+-]+@[\w\.-]+.[\w]+', msg_sender)[0] 679 | except: 680 | pass 681 | msg_sender = msg_sender.lower() 682 | 683 | msg_recipient = "" 684 | 685 | ## find email details 686 | if msg_headers["To"]: 687 | rcpts = re.findall(r'[\w\.+-]+@[\w\.-]+.[\w]+', msg_headers["To"]) 688 | if len(rcpts) > 0: 689 | msg_recipient = rcpts[0] 690 | ## strip extension (user+foo@domain) 691 | msg_recipient = re.sub(r'\+.*@', '@', msg_recipient) 692 | msg_recipient = msg_recipient.lower() 693 | userdata = lib.user.GWUser(email = msg_recipient, unalias = True) 694 | 695 | ## check if we have a recipient address for the receiving email 696 | if not userdata or not userdata.check(): 697 | ## look for X-Original-To instead 698 | rcpts = re.findall(r'[\w\.+-]+@[\w\.-]+.[\w]+', msg_headers["X-Original-To"]) 699 | if len(rcpts) > 0: 700 | msg_recipient = rcpts[0] 701 | msg_recipient = re.sub(r'\+.*@', '@', msg_recipient) 702 | msg_recipient = msg_recipient.lower() 703 | userdata = lib.user.GWUser(email = msg_recipient, unalias = True) 704 | 705 | ## no valid recipient 706 | #if not msg_recipient in addressbook: 707 | # logging.warn('Purged email destined for unknown user ' + msg_recipient + ' from ' + msg_sender) 708 | # logging.debug(msg_headers) 709 | # delete_email(k) 710 | # return 711 | 712 | ## check if we have valid sender and recipient details 713 | if not msg_sender or not msg_recipient: 714 | logging.warn('Malformed email detected and purged') 715 | delete_email(k) 716 | return 717 | 718 | ## set bitmessage destination address 719 | bm_to_address = userdata.bm 720 | 721 | ## set from address 722 | ## check to see if we need to generate a sending address for the source email address 723 | # if not msg_sender in address_list: 724 | # bm_from_address = generate_sender_address(msg_sender) 725 | # address_list[msg_sender] = bm_from_address 726 | # else: 727 | bm_from_address = BMAPI().get_address(BMConfig().get("bmgateway", "bmgateway", "relay_address_label")) 728 | 729 | ## find message subject 730 | msg_subject = decode_header(msg_headers['subject'])[0] 731 | if(msg_subject[1]): 732 | msg_subject = unicode(msg_subject[0], msg_subject[1]) 733 | else: 734 | msg_subject = lib.charset.safeDecode(msg_subject[0]) 735 | 736 | ## find message body contents in plaintext 737 | msg_tmp = email.message_from_string(msg_raw) 738 | 739 | # handle DSN 740 | if msg_tmp.get_content_type() == "multipart/report" and msg_tmp.get_param("report-type", "") == "delivery-status" and msg_tmp.get("Auto-Submitted", "") == "auto-replied": 741 | for part in msg_tmp.walk(): 742 | if part and part.get_content_type() == 'message/delivery-status': 743 | for subpart in part.get_payload(decode = 0): 744 | if subpart.get("Action", "") in ("relayed", "delivered", "expanded"): 745 | logging.info ("Successful DSN from " + bm_to_address) 746 | lib.user.GWUser(bm = bm_to_address).setlastrelay(lastrelay = time.time()) 747 | delete_email(k) 748 | return 749 | 750 | msg_body = u'' 751 | body_raw = '' 752 | decrypt_ok = False 753 | sigverify_ok = False 754 | mega_fileids = [] 755 | 756 | # DKIM 757 | ar = msg_tmp.get_param("dkim", "missing", "Authentication-Results") 758 | if ar == "missing": 759 | try: 760 | domain = msg_sender.split("@")[-1] 761 | if lib.user.GWDomain(domain).check() and domain == msg_tmp.get_param("d", "missing", "DKIM-Signature"): 762 | ar = "pass" # we trust MTA to reject fakes 763 | except: 764 | pass 765 | 766 | ## inline PGP 767 | for part in msg_tmp.walk(): 768 | if part and part.get_content_type() == 'text/plain' and not (part.has_key("Content-Disposition") and part.__getitem__("Content-Disposition")[:11] == "attachment;"): 769 | part_str = part.get_payload(decode=1) 770 | if userdata.pgp == 1: 771 | if userdata.flags & 1 == 1: 772 | pgpparts = part_str.split("-----") 773 | # hack for absent pgp 774 | if not pgpparts or len(pgpparts) < 4: 775 | msg_body += lib.charset.safeDecode(part_str, part.get_content_charset(None)) 776 | continue 777 | state = 0 778 | pgp_body = "" 779 | for pgppart in pgpparts: 780 | if pgppart == "BEGIN PGP MESSAGE": 781 | pgp_body = "-----" + pgppart + "-----" 782 | state = 1 783 | elif pgppart == "END PGP MESSAGE": 784 | pgp_body += "-----" + pgppart + "-----" 785 | # import from sql if necessary 786 | lib.gpg.check_key(msg_recipient) 787 | decrypted, sigverify_ok = lib.gpg.decrypt_content(pgp_body, msg_sender, msg_recipient) 788 | if isinstance(decrypted, basestring): 789 | part_str = decrypted 790 | decrypt_ok = True 791 | #else: 792 | #part_str = part.get_payload(decode = 0) 793 | sigresult = "fail" 794 | if sigverify_ok: 795 | sigresult = "ok" 796 | logging.info("Decrypted email from " + msg_sender + " to " + msg_recipient + ", signature: " + sigresult) 797 | state = 0 798 | elif pgppart == "BEGIN PGP SIGNED MESSAGE": 799 | pgp_body += "-----" + pgppart + "-----" 800 | state = 2 801 | elif pgppart == "BEGIN PGP SIGNATURE": 802 | pgp_body += "-----" + pgppart + "-----" 803 | state = 3 804 | elif pgppart == "END PGP SIGNATURE": 805 | pgp_body += "-----" + pgppart + "-----" 806 | # import from sql if necessary 807 | lib.gpg.check_key(msg_recipient) 808 | plain, sigverify_ok = lib.gpg.verify(pgp_body, msg_sender, msg_recipient) 809 | if isinstance(plain, basestring): 810 | part_str = plain 811 | #else: 812 | #part_str = part.get_payload(decode = 0) 813 | sigresult = "fail" 814 | if sigverify_ok: 815 | sigresult = "ok" 816 | logging.info("Verifying PGP signature from " + msg_sender + " to " + msg_recipient + ": " + sigresult) 817 | state = 0 818 | elif state == 0: 819 | msg_body += lib.charset.safeDecode(pgppart, part.get_content_charset(None)) 820 | elif state > 0: 821 | pgp_body += lib.charset.safeDecode(pgppart, part.get_content_charset(None)) 822 | else: 823 | if "BEGIN PGP MESSAGE" in part_str: 824 | # import from sql if necessary 825 | lib.gpg.check_key(msg_recipient) 826 | decrypted, sigverify_ok = lib.gpg.decrypt_content(part_str, msg_sender, msg_recipient) 827 | if isinstance(decrypted, basestring): 828 | part_str = decrypted 829 | decrypt_ok = True 830 | else: 831 | part_str = part.get_payload(decode = 0) 832 | logging.info("Decrypted email from " + msg_sender + " to " + msg_recipient) 833 | elif "BEGIN PGP SIGNED MESSAGE" in part_str: 834 | # import from sql if necessary 835 | lib.gpg.check_key(msg_recipient) 836 | plain, sigverify_ok = lib.gpg.verify(part_str, msg_sender, msg_recipient) 837 | if isinstance(plain, basestring): 838 | part_str = plain 839 | else: 840 | part_str = part.get_payload(decode = 0) 841 | # PGP END 842 | 843 | body_raw += part.as_string(False) 844 | #print part.get_content_charset() 845 | #print msg_tmp.get_charset() 846 | part_str = lib.charset.safeDecode(part_str, part.get_content_charset(None)) 847 | msg_body += part_str 848 | 849 | ## if there's no plaintext content, convert the html 850 | if not msg_body or userdata.html == 2: 851 | for part in msg_tmp.walk(): 852 | if part and part.get_content_type() == 'text/html' and not (part.has_key("Content-Disposition") and part.__getitem__("Content-Disposition")[:11] == "attachment;"): 853 | part_str = part.get_payload(decode=1) 854 | h = html2text.HTML2Text() 855 | h.inline_links = False 856 | if userdata.html == 1: 857 | msg_body += lib.charset.safeDecode(part_str, part.get_content_charset(None)) 858 | elif userdata.html == 2: 859 | msg_body = lib.charset.safeDecode(part_str, part.get_content_charset(None)) 860 | else: 861 | msg_body += h.handle(lib.charset.safeDecode(part_str, part.get_content_charset(None))) 862 | #msg_body = msg_body + html2text.html2text(unicode(part.get_payload(), part.get_content_charset())) 863 | 864 | ## if there's no plaintext or html, check if it's encrypted 865 | # PGP/MIME 866 | has_encrypted_parts = False 867 | if not msg_body: 868 | for part in msg_tmp.walk(): 869 | if part.get_content_type() == 'application/pgp-encrypted': 870 | has_encrypted_parts = True 871 | # import from sql if necessary 872 | if userdata.pgp == 1: 873 | lib.gpg.check_key(msg_recipient) 874 | 875 | ## look for encrypted attachment containing text 876 | if part.get_content_type() == 'application/octet-stream' and has_encrypted_parts: 877 | part_str = part.get_payload(decode=1) 878 | 879 | if userdata.pgp == 0: 880 | msg_body += part_str 881 | continue 882 | 883 | ## if we see the pgp header, decrypt 884 | if 'BEGIN PGP MESSAGE' in part_str: 885 | decrypted_data, sigverify_ok = lib.gpg.decrypt_content(part_str, msg_sender, msg_recipient, True) 886 | 887 | ## decrypt failed 888 | if not decrypted_data: 889 | logging.error("Decryption of email destined for " + msg_recipient + " failed") 890 | msg_body += part.get_payload(decode=0) 891 | continue 892 | 893 | logging.info("Decrypted email from " + msg_sender + " to " + msg_recipient) 894 | msg_body += decrypted_data 895 | decrypt_ok = True 896 | elif "BEGIN PGP SIGNED MESSAGE" in part_str: 897 | plain, sigverify_ok = lib.gpg.verify(part_str, msg_sender, msg_recipient) 898 | if isinstance(plain, basestring): 899 | msg_body += plain 900 | else: 901 | msg_body += part.get_payload(decode = 0) 902 | 903 | ## unknown attachment 904 | else: 905 | logging.debug("Received application/octet-stream type in inbound email, but did not see encryption header") 906 | 907 | if not sigverify_ok: 908 | for part in msg_tmp.walk(): 909 | if part.get_content_type() == 'application/pgp-signature': 910 | 911 | if userdata.pgp == 0: 912 | msg_body = '-----BEGIN PGP SIGNED MESSAGE-----\n' + msg_body 913 | msg_body += '\n-----BEGIN PGP SIGNATURE-----\n' 914 | msg_body += part.get_payload(decode=0) 915 | msg_body += '\n-----END PGP SIGNATURE-----\n' 916 | continue 917 | 918 | # import from sql if necessary 919 | lib.gpg.check_key(msg_recipient) 920 | plain, sigverify_ok = lib.gpg.verify(body_raw, msg_sender, msg_recipient, part.get_payload(decode=1)) 921 | 922 | if userdata.attachments == 1 and not userdata.expired(): 923 | for part in msg_tmp.walk(): 924 | if part.has_key("Content-Disposition") and part.__getitem__("Content-Disposition")[:11] == "attachment;": 925 | # fix encoding 926 | try: 927 | filename = email.header.decode_header(part.get_filename()) 928 | encoding = filename[0][1] 929 | filename = filename[0][0] 930 | except: 931 | filename = part.get_filename() 932 | encoding = False 933 | 934 | fileid, link = lib.bmmega.mega_upload(userdata.bm, filename, part.get_payload(decode = 1)) 935 | mega_fileids.append(fileid) 936 | if encoding: 937 | filename = unicode(filename, encoding) 938 | logging.info("Attachment \"%s\" (%s)", filename, part.get_content_type()) 939 | msg_body = "Attachment \"" + filename + "\" (" + part.get_content_type() + "): " + link + "\n" + msg_body 940 | if userdata.pgp == 1: 941 | if not decrypt_ok: 942 | msg_body = "WARNING: PGP encryption missing or invalid. The message content could be exposed to third parties.\n" + msg_body 943 | if not sigverify_ok: 944 | msg_body = "WARNING: PGP signature missing or invalid. The authenticity of the message could not be verified.\n" + msg_body 945 | else: 946 | # msg_body = "WARNING: Server-side PGP is off, passing message as it is.\n" + msg_body 947 | pass 948 | 949 | if not ar[0:4] == "pass": 950 | msg_body = "WARNING: DKIM signature missing or invalid. The email may not have been sent through legitimate servers.\n" + msg_body 951 | 952 | logging.info('Incoming email from %s to %s', msg_sender, msg_recipient) 953 | 954 | sent = SendBM(bm_from_address, bm_to_address, 955 | 'MAILCHUCK-FROM::' + msg_sender + ' | ' + msg_subject.encode('utf-8'), 956 | msg_body.encode('utf-8')) 957 | if sent.status: 958 | for fileid in mega_fileids: 959 | # cur.execute ("UPDATE mega SET ackdata = %s WHERE fileid = %s AND ackdata IS NULL", (ackdata.decode("hex"), fileid)) 960 | pass 961 | ## remove email file 962 | if userdata.archive == 1: 963 | #print msg_body 964 | save_email(k) 965 | else: 966 | delete_email(k) 967 | 968 | class InotifyEventHandler(pyinotify.ProcessEvent): 969 | def process_default(self, event): 970 | if os.path.isfile(event.pathname): 971 | # to avoid crashes from taking down the whole processing, in the future for parallel processing 972 | fhandle = open (event.pathname, 'r') 973 | fcntl.flock(fhandle, fcntl.LOCK_EX) 974 | email_thread = threading.Thread(target=handle_email, name='EmailIn', args=(event.name,)) 975 | email_thread.start() 976 | email_thread.join() 977 | fcntl.flock(fhandle, fcntl.LOCK_UN) 978 | fhandle.close() 979 | 980 | def inotify_incoming_emails(): 981 | mdir = BMConfig().get("bmgateway", "bmgateway", "mail_folder") 982 | 983 | wm = pyinotify.WatchManager() 984 | notifier = pyinotify.ThreadedNotifier(wm, InotifyEventHandler()) 985 | notifier.setName ("Inotify") 986 | wm.add_watch (mdir, pyinotify.IN_CREATE|pyinotify.IN_CLOSE_WRITE|pyinotify.IN_MOVED_TO) 987 | #wm.add_watch (mdir, pyinotify.ALL_EVENTS, rec=True) 988 | return notifier 989 | 990 | ## check for new mail to process 991 | def check_emails(intcond=None): 992 | ## find new messages in folders 993 | mdir = os.listdir(BMConfig().get("bmgateway", "bmgateway", "mail_folder")) 994 | 995 | ## no new mail 996 | if not mdir: 997 | return 998 | 999 | ## iterate through new messages, each in thread so that crashes do not prevent continuing 1000 | for k in mdir: 1001 | try: 1002 | atime = os.stat(os.path.join(BMConfig().get("bmgateway", "bmgateway", "mail_folder"), k)).st_atime 1003 | except: 1004 | continue 1005 | else: 1006 | if atime > time.time() - 10: 1007 | continue 1008 | fhandle = open (os.path.join(BMConfig().get("bmgateway", "bmgateway", "mail_folder"), k), 'r') 1009 | try: 1010 | fcntl.flock(fhandle, fcntl.LOCK_EX|fcntl.LOCK_NB) 1011 | except IOError: 1012 | fhandle.close() 1013 | # locked, skip this time 1014 | continue 1015 | email_thread = threading.Thread(target=handle_email, name="EmailIn", args=(k,)) 1016 | email_thread.start() 1017 | email_thread.join() 1018 | fcntl.flock(fhandle, fcntl.LOCK_UN) 1019 | fhandle.close() 1020 | 1021 | if not BMMySQL().connect(): 1022 | print "Failed to connect to mysql" 1023 | sys.exit() 1024 | 1025 | lib.gpg.gpg_init() 1026 | 1027 | ## main 1028 | parser = argparse.ArgumentParser(description='An email <-> bitmessage gateway.') 1029 | parser.add_argument('-l','--list', help='List known internal and external addresses',required=False, action='store_true') 1030 | parser.add_argument('-d','--delete', help='Delete an address',required=False, default=False) 1031 | parser.add_argument('-a','--add', help='Generate a new bitmessage address with given label',required=False, default=False) 1032 | 1033 | args = parser.parse_args() 1034 | 1035 | ## call correct function 1036 | if args.list == True: 1037 | list_addresses() 1038 | 1039 | elif args.delete: 1040 | delete_address(args.delete) 1041 | 1042 | elif args.add: 1043 | generate_sender_address(args.add) 1044 | 1045 | else: 1046 | while BMAPI().conn() is False: 1047 | print "Failure connecting to API, sleeping..." 1048 | time.sleep(random.random()+0.5) 1049 | 1050 | milter_thread = threading.Thread() 1051 | maintenance_thread = threading.Thread() 1052 | email_thread = threading.Thread() 1053 | inotify_thread = threading.Thread() 1054 | bminbox_thread = threading.Thread() 1055 | bmoutbox_thread = threading.Thread() 1056 | 1057 | interrupted = False 1058 | intcond = threading.Condition() 1059 | 1060 | outboxlast = 0 1061 | 1062 | logging.info("Starting BM gateway") 1063 | 1064 | ## run managers in threads 1065 | while not interrupted: 1066 | if BMConfig().get("bmgateway", "bmgateway", "incoming_thread"): 1067 | if not email_thread.isAlive(): 1068 | if email_thread.ident is not None: 1069 | email_thread.join() 1070 | email_thread = threading.Thread(target=check_emails, name="EmailIn", args=(intcond,)) 1071 | email_thread.start() 1072 | if have_inotify and not inotify_thread.isAlive(): 1073 | inotify_thread = inotify_incoming_emails() 1074 | inotify_thread.start() 1075 | if BMConfig().get("bmgateway", "bmgateway", "outgoing_thread"): 1076 | if not bminbox_thread.isAlive(): 1077 | if bminbox_thread.ident is not None: 1078 | bminbox_thread.join() 1079 | bminbox_thread = threading.Thread(target=check_bminbox, name="BMIn", args=(intcond,)) 1080 | bminbox_thread.start() 1081 | 1082 | if time.time() - outboxlast > BMConfig().get("bmgateway", "bmgateway", "outbox_process_interval") and not bmoutbox_thread.isAlive(): 1083 | #if not bmoutbox_thread.isAlive(): 1084 | if bmoutbox_thread.ident is not None: 1085 | bmoutbox_thread.join() 1086 | outboxlast = time.time() 1087 | bmoutbox_thread = threading.Thread(target=check_bmoutbox, name="BMOut", args=(intcond,)) 1088 | bmoutbox_thread.start() 1089 | 1090 | if BMConfig().get("bmgateway", "bmgateway", "milter_thread") and not milter_thread.isAlive(): 1091 | if milter_thread.ident is not None: 1092 | milter_thread.join() 1093 | milter_thread = threading.Thread(target=lib.milter.run, name="Milter") 1094 | milter_thread.start() 1095 | 1096 | if BMConfig().get("bmgateway", "bmgateway", "maintenance_thread") and not maintenance_thread.isAlive(): 1097 | if maintenance_thread.ident is not None: 1098 | maintenance_thread.join() 1099 | maintenance_thread = threading.Thread(target=lib.maintenance.serve, name="Maintenance") 1100 | maintenance_thread.start() 1101 | 1102 | 1103 | try: 1104 | time.sleep(BMConfig().get("bmgateway", "bmgateway", "process_interval")) 1105 | except KeyboardInterrupt: 1106 | interrupted = True 1107 | 1108 | logging.info("Shutting down BM gateway") 1109 | print "Shutting down BM gateway, please wait a couple of seconds ..." 1110 | 1111 | intcond.acquire() 1112 | intcond.notifyAll() 1113 | intcond.release() 1114 | 1115 | if BMConfig().get("bmgateway", "bmgateway", "incoming_thread"): 1116 | if have_inotify and inotify_thread.isAlive: 1117 | inotify_thread.stop() 1118 | inotify_thread.join() 1119 | email_thread.join() 1120 | 1121 | if BMConfig().get("bmgateway", "bmgateway", "outgoing_thread"): 1122 | bminbox_thread.join() 1123 | if bmoutbox_thread.ident is not None: 1124 | bmoutbox_thread.join() 1125 | 1126 | if BMConfig().get("bmgateway", "bmgateway", "maintenance_thread"): 1127 | if maintenance_thread.isAlive: 1128 | maintenance_thread.stop() 1129 | maintenance_thread.join() 1130 | 1131 | if BMConfig().get("bmgateway", "bmgateway", "milter_thread") and milter_thread.isAlive(): 1132 | #milter_thread.stop() 1133 | try: 1134 | Milter.stop() 1135 | except: 1136 | pass 1137 | milter_thread.join() 1138 | 1139 | logging.info("Stopped BM gateway") 1140 | -------------------------------------------------------------------------------- /bitmessage-lmtpd.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python 2 | 3 | import asyncore 4 | import lib.lmtpd 5 | 6 | server = lib.lmtpd.LMTPServer(('localhost', 10025), None) 7 | 8 | asyncore.loop() 9 | -------------------------------------------------------------------------------- /lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterSurda/bitmessage-email-gateway/6818fc0cc405282ce4e9e958d20e684f7e45cab8/lib/__init__.py -------------------------------------------------------------------------------- /lib/badparsers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2.7 2 | 3 | import email 4 | import logging 5 | import lib.payment 6 | import os.path 7 | from lib.config import BMConfig 8 | import logging 9 | import chardet 10 | #from lib.user import BMUser 11 | import email 12 | 13 | class EmailParser(object): 14 | def __init__ (self): 15 | self.raw = None 16 | self.body = None 17 | self.subject = None 18 | self.sender = None 19 | self.recipient = None 20 | self.recipientbm = None 21 | self.pgpbody = None 22 | self.headers = {} 23 | self.signature = False 24 | self.encryption = False 25 | self.dkim = False 26 | self.status = False 27 | self.multipart = False 28 | self.maintype = None 29 | self.subtype = None 30 | self.charset = None 31 | self.attachment = None 32 | self.dsn = None 33 | self.parent = None 34 | self.out = None 35 | 36 | def parse(self): 37 | if self.parent == None: 38 | try: 39 | self.parse_headers() 40 | except: 41 | return False 42 | try: 43 | self.parse_body() 44 | except: 45 | return False 46 | 47 | self.subject = base64.b64encode(subject) 48 | self.body = base64.b64encode(body) 49 | if (sender[0:3] == "BM-"): 50 | senderbm = sender 51 | else: 52 | senderbm = BMAPI().get_address(sender) 53 | recipientbm = recipient 54 | userdata = lib.user.GWUser(bm = recipient) 55 | if userdata.check(): 56 | recipient = userdata['email'] 57 | try: 58 | ackData = BMAPI().conn().sendMessage(recipientbm, senderbm, self.subject, self.body, 2) 59 | logging.info("Sent BM from %s to %s", sender, recipient) 60 | except: 61 | logging.error("Failure sending BM from %s to %s", sender, recipient) 62 | return False 63 | return True 64 | 65 | def read_from_file(self, fname): 66 | try: 67 | fullpath = os.path.join(BMConfig().get("bmgateway", "bmgateway", "mail_folder"), fname) 68 | f = open(fullpath, 'r') 69 | self.raw = f.read() 70 | f.close() 71 | except IOError: 72 | logging.error('Could not read email from: ' + fullpath) 73 | 74 | def from_data(self, data): 75 | self.raw = data 76 | 77 | def parse_headers(self): 78 | self.headers = email.parser.Parser().parsestr(self.raw) 79 | if not self.headers: 80 | logging.error('Email missing headers') 81 | raise 82 | self.extract_sender() 83 | self.extract_recipient() 84 | if not self.sender or not self.recipient: 85 | logging.warn('Email missing sender or recipient') 86 | raise 87 | self.extract_subject() 88 | self.extract_dkim() 89 | 90 | def extract_sender(self): 91 | self.sender = self.headers["From"] 92 | 93 | ## DSN or missing sender 94 | if self.sender == '<>' or not self.sender: 95 | self.sender = BMConfig().get("bmgateway", "bmgateway", "relay_address_label") 96 | else: 97 | self.sender = re.findall(r'[\w\.+-]+@[\w\.-]+.[\w]+', self.sender)[0] 98 | self.sender = self.sender.lower() 99 | 100 | self.body = email.message_from_string(self.raw) 101 | 102 | if self.body.get_content_maintype == "multipart": 103 | self.multipart = True 104 | self.maintype = self.body.get_content_maintype() 105 | self.subtype = self.body.get_content_subtype() 106 | self.charset = self.body.get_content_charset() 107 | 108 | def attachment( 109 | 110 | if self.body.has_key("Content-Disposition") and self.body.__getitem__("Content-Disposition")[:11] == "attachment": 111 | self.attachment = email.header.decode_header(part.get_filename())[0] 112 | 113 | def extract_recipient(self): 114 | ## find email details 115 | userdata = None 116 | rcpts = () 117 | for rcpthdr in ("To", "X-Original-To", "Cc"): 118 | rcpts.extend(re.findall(r'[\w\.+-]+@[\w\.-]+.[\w]+', self.headers[rcpthdr])) 119 | for candidate in rcpts: 120 | ## strip extension (user+foo@domain) 121 | self.recipient = re.sub(r'\+.*@', '@', candidate) 122 | ## lowercasse 123 | self.recipient = self.recipient.lower() 124 | ## check if user exists 125 | userdata = lib.user.GWUser(email = self.recipient, unalias = True) 126 | if userdata.check(): 127 | break 128 | 129 | def extract_subject(self): 130 | self.subject = email.header.decode_header(self.headers['Subject'])[0] 131 | if(self.subject[1]): 132 | self.subject = unicode(self.subject[0], self.subject[1]) 133 | else: 134 | self.subject = self.subject[0] 135 | 136 | def extract_dkim(self): 137 | ar = self.body.get_param("dkim", "missing", "Authentication-Results") 138 | if ar == "missing": 139 | domain = self.sender.split("@")[-1] 140 | if lib.user.GWDomain(domain).check() and domain == self.body.get_param("d", "missing", "DKIM-Signature"): 141 | ar = "pass" # we trust MTA to reject fakes from domains that hare handled locally 142 | if ar[0:4] == "pass": 143 | self.dkim = True 144 | 145 | def parse_body(self) 146 | cipher = None 147 | signature = None 148 | if self.multipart: 149 | for part in self.body.walk(): 150 | if part.get_content_type() == 'message/delivery-status': 151 | part_str = part.get_payload(decode = 0) 152 | for subpart in part_str: 153 | if subpart.get("Action", "") in ("relayed", "delivered", "expanded") and 154 | self.body.get_param("report-type", "") == "delivery-status" and self.body.get("Auto-Submitted", "") == "auto-replied": 155 | self.dsn = True 156 | elif part.get_content_type() == 'text/plain' and 157 | (self.subtype != "alternative" or self.out == None): # lower precedence if other content exists 158 | self.handle_text(part) 159 | elif part.get_content_type() == 'message/rfc822': 160 | self.handle_text(part) 161 | elif part.get_content_type() == 'text/html': 162 | self.handle_html(part) 163 | elif part.has_key("Content-Disposition") and part.__getitem__("Content-Disposition")[:11] == "attachment;": 164 | if has setting 165 | handle_attachment() 166 | 167 | elif self.subtype == "mixed": 168 | elif self.subtype == "digest": 169 | elif self.subtype == "alternative": 170 | elif self.subtype == "related": 171 | elif self.subtype == "signed": 172 | elif self.subtype == "encrypted": 173 | elif self.maintype == "message" and self.subtype == "rfc822": 174 | self.handle_text(self.body) 175 | elif self.maintype == "text" and self.subtype == "plain": 176 | self.handle_text(self.body) 177 | elif self.maintype == "text" and self.subtype == "html": 178 | self.handle_html(part) 179 | return True 180 | 181 | def handle_text(self, msg): 182 | text = msg.get_payload(decode = True) 183 | if (self.has_pgp(text)): 184 | self.out += self.handle_pgp(msg) 185 | else: 186 | self.out += text 187 | 188 | def handle_html(self, msg): 189 | text = msg.get_payload(decode = True) 190 | if (self.has_pgp(text)): 191 | text = self.handle_pgp(text) 192 | h = html2text.HTML2Text() 193 | h.inline_links = False 194 | if not msg.get_content_charset(): 195 | charset = chardet.detect(text) 196 | if charset['encoding'] == None: 197 | charset = 'ascii' 198 | else: 199 | charset = charset['encoding'] 200 | 201 | text = h.handle(text).decode(charset)) 202 | self.out += text 203 | 204 | def has_pgp(self, msg): 205 | if "-----BEGIN PGP SIGNED MESSAGE-----" in text or "-----BEGIN PGP MESSAGE-----" in text: 206 | return True 207 | return False 208 | 209 | def handle_pgp(self, msg): 210 | text = msg.get_payload(decode = True).decode(charset) 211 | pgpparts = text.split("-----") 212 | state = 0 213 | pgp_body = "" 214 | out = "" 215 | for pgppart in pgpparts: 216 | if pgppart == "BEGIN PGP MESSAGE": 217 | pgp_body = "-----" + pgppart + "-----" 218 | state = 1 219 | elif pgppart == "END PGP MESSAGE": 220 | pgp_body += "-----" + pgppart + "-----" 221 | decrypted, sigverify_ok = lib.gpg.decrypt_content(pgp_body, self.sender, self.recipient) 222 | self.encryption = False 223 | if isinstance(decrypted, basestring): 224 | out += decrypted 225 | self.encryption = True 226 | else: 227 | out += pgp_body 228 | self.signature = False 229 | if sigverify_ok: 230 | self.signature = True 231 | logging.info("Decryption email from %s to %s: %s, signature: %s", self.sender, self.recipient, 232 | ("ok" if self.encryption else "fail"), ("ok" if self.signature else "fail")) 233 | state = 0 234 | elif pgppart == "BEGIN PGP SIGNED MESSAGE": 235 | pgp_body += "-----" + pgppart + "-----" 236 | state = 2 237 | elif pgppart == "BEGIN PGP SIGNATURE": 238 | pgp_body += "-----" + pgppart + "-----" 239 | state = 3 240 | elif pgppart == "END PGP SIGNATURE": 241 | pgp_body += "-----" + pgppart + "-----" 242 | plain, sigverify_ok = lib.gpg.verify(pgp_body, self.sender, self.recipient) 243 | if isinstance(plain, basestring): 244 | out += plain 245 | else: 246 | part_str += pgp_body 247 | self.signature = False 248 | if sigverify_ok: 249 | self.signature = True 250 | logging.info("Verifying PGP signature from %s to %s: %s", self.sender, self.recipient, 251 | ("ok" if self.signature else "fail")) 252 | state = 0 253 | elif state == 0: 254 | if part.get_content_charset(): 255 | msg_body += pgppart.decode(part.get_content_charset()) 256 | else: 257 | charset = chardet.detect(pgppart) 258 | if charset['encoding']: 259 | msg_body += pgppart.decode(charset['encoding']) 260 | else: 261 | msg_body += pgppart.decode('ascii') 262 | elif state > 0: 263 | pgp_body += pgppart 264 | return out 265 | 266 | 267 | class BMParser(object): 268 | -------------------------------------------------------------------------------- /lib/bmapi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2.7 2 | 3 | import xmlrpclib 4 | import json 5 | import lib.singleton 6 | import logging 7 | import threading # xmlrpc ServerProxy is not thread save, so we need a separate object for each thread 8 | from lib.config import BMConfig 9 | 10 | class BaseBMAPI(object): 11 | thrdata = None 12 | address_list = {} 13 | 14 | def __init__(self): 15 | self.thrdata = threading.local() 16 | self.connect() 17 | self._load_address_list() 18 | 19 | def check_connection(self): 20 | if not (hasattr(self.thrdata, 'bm') and self.thrdata.bm is not None): 21 | self.connect() 22 | if not bool(self.address_list): 23 | self._load_address_list() 24 | return self.thrdata.bm 25 | 26 | def connect(self): 27 | for bm in BMConfig().get("bmapi"): 28 | self.thrdata.bm = xmlrpclib.ServerProxy('http://' + 29 | BMConfig().get("bmapi", bm, "username") + ':' + 30 | BMConfig().get("bmapi", bm, "password") + '@' + 31 | BMConfig().get("bmapi", bm, "host") + ':' + 32 | str(BMConfig().get("bmapi", bm, "port")) + '/') 33 | 34 | ## check if API is responding 35 | try: 36 | response = self.thrdata.bm.add(2,2) 37 | logging.info("Connected to Bitmessage API on %s:%i", BMConfig().get("bmapi", bm, "host"), BMConfig().get("bmapi", bm, "port")) 38 | break 39 | except: 40 | self.thrdata.bm = None 41 | if self.thrdata.bm is not None: 42 | return self.thrdata.bm 43 | else: 44 | logging.error('Could not connect to Bitmessage API ') 45 | return False 46 | 47 | def _load_address_list(self): 48 | # does not check for connection, only use internally 49 | self.address_list = {} 50 | bm_addresses = json.loads(self.thrdata.bm.listAddresses())['addresses'] 51 | for address in bm_addresses: 52 | self.address_list[address['label']] = address['address'] 53 | 54 | def get_address(self, label): 55 | if not bool(self.address_list): 56 | self.check_connection() 57 | if label in self.address_list: 58 | return self.address_list[label] 59 | else: 60 | return None 61 | 62 | def conn(self): 63 | return self.check_connection() 64 | 65 | def disconnect(self): 66 | self.thrdata.bm = None 67 | 68 | class BMAPI(BaseBMAPI): 69 | __metaclass__ = lib.singleton.Singleton 70 | -------------------------------------------------------------------------------- /lib/bmfolder.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2.7 2 | 3 | import logging 4 | import json 5 | from lib.config import BMConfig 6 | from lib.bmapi import BMAPI 7 | 8 | class BMFolder(object): 9 | def __init__ (self, folder = inbox): 10 | if folder == "inbox": 11 | try: 12 | self.msg = json.loads(BMAPI().conn().getInboxMessageByID(msgid, False))['inboxMessage'][0] 13 | except: 14 | logging.error('API error when retrieving inbox message %s', msgid) 15 | pass 16 | if not self.msg: 17 | logging.error('API returned blank message when retrieving inbox message %s', msgid) 18 | # deleteStatic(msgid) 19 | self.folder = folder 20 | self.msgid = self.msg['msgid'] 21 | 22 | def delete (self): 23 | return deleteStatic(self.msgid, folder = self.folder) 24 | 25 | @staticmethod 26 | def deleteStatic (msgid, folder = "inbox"): 27 | if folder == "inbox": 28 | result = BMAPI().conn().trashMessage(msgid) 29 | elif folder == "outbox": 30 | result = BMAPI().conn().trashSentMessage(msgid) 31 | if BMConfig().get("bmgateway", "bmgateway", "debug"): 32 | logging.debug('Deleted bitmessage %s from %s, API response: %s', msgid, folder, result) 33 | else: 34 | logging.info('Deleted bitmessage %s from %s', msgid, folder) 35 | 36 | -------------------------------------------------------------------------------- /lib/bminbox.py: -------------------------------------------------------------------------------- 1 | from binascii import unhexlify 2 | import MySQLdb 3 | from warnings import filterwarnings 4 | 5 | from lib.mysql import BMMySQL 6 | 7 | def check_message_processed(msgid): 8 | cur = BMMySQL().conn().cursor(MySQLdb.cursors.DictCursor) 9 | filterwarnings('ignore', category = MySQLdb.Warning) 10 | cur.execute ("UPDATE inboxids SET lastseen = UNIX_TIMESTAMP(NOW()) WHERE msgid = %s", (unhexlify(msgid))) 11 | if cur.rowcount >= 1: 12 | cur.close() 13 | return True 14 | else: 15 | cur.close() 16 | return False 17 | 18 | def set_message_processed(msgid): 19 | cur = BMMySQL().conn().cursor(MySQLdb.cursors.DictCursor) 20 | filterwarnings('ignore', category = MySQLdb.Warning) 21 | cur.execute ("INSERT INTO inboxids (msgid, lastseen) VALUES (%s, UNIX_TIMESTAMP(NOW())) ON DUPLICATE KEY UPDATE lastseen = UNIX_TIMESTAMP(NOW())", (unhexlify(msgid))) 22 | cur.close() 23 | -------------------------------------------------------------------------------- /lib/bmlogging.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import logging 4 | import logging.handlers 5 | import lib.singleton 6 | from lib.config import BMConfig 7 | 8 | class BaseBMLogging(object): 9 | logger = None 10 | handler = None 11 | formatter = None 12 | 13 | def __init__(self): 14 | self.logger = logging.getLogger() 15 | if BMConfig().get("bmgateway", "bmgateway", "debug"): 16 | self.logger.setLevel(logging.DEBUG) 17 | else: 18 | self.logger.setLevel(logging.INFO) 19 | self.handler = logging.handlers.WatchedFileHandler(BMConfig().get("bmgateway", "bmgateway", "log_filename")) 20 | self.formatter = logging.Formatter('%(asctime)s [%(threadName)s] %(levelname)s: %(message)s') 21 | self.handler.setFormatter(self.formatter) 22 | self.logger.addHandler(self.handler) 23 | 24 | class BMLogging(BaseBMLogging): 25 | __metaclass__ = lib.singleton.Singleton 26 | -------------------------------------------------------------------------------- /lib/bmmega.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2.7 2 | 3 | from lib.config import BMConfig 4 | from lib.mysql import BMMySQL 5 | import logging 6 | from mega import Mega 7 | import sys 8 | import time 9 | import pprint 10 | 11 | def mega_login(): 12 | mega = Mega({'verbose': False}) 13 | try: 14 | m = mega.login(BMConfig().get("bmgateway", "mega", "username"), BMConfig().get("bmgateway", "mega", "password")) 15 | except: 16 | logging.error ("Mega login error") 17 | return None 18 | return m 19 | 20 | def mega_upload(bm, fname, data): 21 | m = mega_login() 22 | if m == None: 23 | return None, None 24 | foldername = BMConfig().get("bmgateway", "mega", "folder") 25 | folder = m.find(foldername) 26 | loops = 30 27 | while folder == None and loops > 0: 28 | try: 29 | m.create_folder(foldername) 30 | folder = m.find(foldername) 31 | except: 32 | pass 33 | if folder == None: 34 | time.sleep (1) 35 | loops -= 1 36 | if folder == None: 37 | return None, None 38 | 39 | 40 | uploadedfile = None 41 | loops = 30 42 | while uploadedfile == None and loops > 0: 43 | try: 44 | uploadedfile = m.upload(data, folder[0], dest_filename=fname, save_key=False) 45 | except: 46 | pass 47 | if uploadedfile == None: 48 | time.sleep (1) 49 | loops -= 1 50 | 51 | file_id = uploadedfile['f'][0]['h'] 52 | link = m.get_upload_link(uploadedfile) 53 | cur = BMMySQL().conn().cursor() 54 | cur.execute ("INSERT IGNORE INTO mega (fileid, bm) VALUES (%s, %s)", ( 55 | file_id, bm)) 56 | cur.close() 57 | return file_id, link 58 | 59 | def search_fileids(ackdata): 60 | fileids = [] 61 | # cur.execute("SELECT fileid FROM mega WHERE ackdata = %s", (ackdata.decode("hex"))) 62 | # cur.close() 63 | # loop results 64 | 65 | return fileids 66 | 67 | def mega_delete(file_id): 68 | m = mega_login() 69 | if m == None: 70 | return False 71 | retval = m.delete(file_id) 72 | #if retval == 0: 73 | # cur.execute("DELETE FROM mega WHERE fileid = %s", (file_id)) 74 | # cur.close() 75 | 76 | return retval 77 | 78 | # download from an url 79 | #m.download_from_url('https://mega.co.nz/#!wYo3AYZC!Zwi1f3ANtYwKNOc07fwuN1enOoRj4CreFouuGqi4D6Y') 80 | 81 | -------------------------------------------------------------------------------- /lib/bmmessage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2.7 2 | 3 | import logging 4 | import json 5 | from lib.config import BMConfig 6 | from lib.bmapi import BMAPI 7 | 8 | class BMMessage(object): 9 | def __init__ (self, msgid, folder = "inbox"): 10 | if folder == "inbox": 11 | try: 12 | self.msg = json.loads(BMAPI().conn().getInboxMessageByID(msgid, False))['inboxMessage'][0] 13 | except: 14 | logging.error('API error when retrieving inbox message %s', msgid) 15 | pass 16 | if not self.msg: 17 | logging.error('API returned blank message when retrieving inbox message %s', msgid) 18 | # deleteStatic(msgid) 19 | self.folder = folder 20 | self.msgid = self.msg['msgid'] 21 | 22 | def delete (self): 23 | return deleteStatic(self.msgid, folder = self.folder) 24 | 25 | @staticmethod 26 | def deleteStatic (msgid, folder = "inbox"): 27 | if folder == "inbox": 28 | result = BMAPI().conn().trashMessage(msgid) 29 | elif folder == "outbox": 30 | result = BMAPI().conn().trashSentMessage(msgid) 31 | if BMConfig().get("bmgateway", "bmgateway", "debug"): 32 | logging.debug('Deleted bitmessage %s from %s, API response: %s', msgid, folder, result) 33 | else: 34 | logging.info('Deleted bitmessage %s from %s', msgid, folder) 35 | 36 | -------------------------------------------------------------------------------- /lib/bmsignal.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import signal, os 4 | import lib.bmlogging 5 | 6 | def huphandler(signum, frame): 7 | lib.bmlogging.init_logging() 8 | 9 | #signal.signal(signal.SIGHUP, huphandler) 10 | -------------------------------------------------------------------------------- /lib/charset.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import chardet 4 | 5 | class SafeDecodeError(Exception): 6 | def __init__(self, message): 7 | self.message = message 8 | 9 | def safeDecode(text, charset = None): 10 | if isinstance(text, unicode): 11 | return text 12 | if len(text) == 0: 13 | return u"" 14 | if charset is not None: 15 | try: 16 | return text.decode(charset) 17 | except: 18 | pass 19 | try: 20 | detected_charset = chardet.detect(text) 21 | if detected_charset['encoding']: 22 | return text.decode(detected_charset['encoding'], errors='replace') 23 | except: 24 | raise SafeDecodeError("SafeDecode failed to detect charset") 25 | -------------------------------------------------------------------------------- /lib/config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import lib.singleton 4 | import ConfigParser, os 5 | 6 | class BaseBMConfig(object): 7 | files = { 8 | "bmgateway": 0, 9 | "pgpkeyservers": 0, 10 | "bmapi": 0, 11 | "smtp": 0, 12 | "mysql": 0 13 | } 14 | valuetypes = { 15 | "bmgateway": { 16 | "banned_usernames": "boolean", 17 | "bmgateway": { 18 | "debug": "boolean", 19 | "respond_to_invalid": "boolean", 20 | "respond_to_missing": "boolean", 21 | "allow_unregistered_senders": "boolean", 22 | "throttle": "float", 23 | "incoming_thread": "int", 24 | "outgoing_thread": "int", 25 | "maintenance_thread": "boolean", 26 | "milter_thread": "boolean", 27 | "process_interval": "float", 28 | "outbox_process_interval": "float" 29 | }, 30 | "pgp": { 31 | "sign": "boolean", 32 | "encrypt": "boolean", 33 | "delete_expired_delay": "int" 34 | }, 35 | "default": { 36 | "attachments": "boolean", 37 | "pgp": "boolean" 38 | }, 39 | "bitcoind": { 40 | "port": "int" 41 | } 42 | }, 43 | "bmapi": { 44 | "*": { 45 | "port": "int" 46 | } 47 | }, 48 | "smtp": { 49 | "*": { 50 | "port": "int" 51 | } 52 | } 53 | } 54 | cfg = {} 55 | 56 | @staticmethod 57 | def getvaluetype (fname, section, option): 58 | try: 59 | return BaseBMConfig.valuetypes[fname][section][option] 60 | except: 61 | pass 62 | try: 63 | return BaseBMConfig.valuetypes[fname]["*"][option] 64 | except: 65 | pass 66 | try: 67 | return BaseBMConfig.valuetype[fname][section] 68 | except: 69 | pass 70 | try: 71 | return BaseBMConfig.valuetype[fname] 72 | except: 73 | pass 74 | 75 | #default 76 | return "text" 77 | 78 | def __init__(self): 79 | self.loadconf() 80 | 81 | def loadconf(self): 82 | tmp = {} 83 | dataFolder = os.path.join (os.environ["HOME"], ".config", "bmgateway") 84 | for fname in BaseBMConfig.files: 85 | config = ConfigParser.SafeConfigParser(allow_no_value=True) 86 | config.readfp(open(os.path.join(dataFolder, fname + ".conf"))) 87 | tmp[fname] = {} 88 | for section in config.sections(): 89 | tmp[fname][section] = {} 90 | for option in config.options(section): 91 | vtype = BaseBMConfig.getvaluetype(fname, section, option) 92 | if vtype == "boolean": 93 | tmp[fname][section][option] = config.getboolean(section, option) 94 | elif vtype == "int": 95 | tmp[fname][section][option] = config.getint(section, option) 96 | elif vtype == "float": 97 | tmp[fname][section][option] = config.getfloat(section, option) 98 | else: 99 | tmp[fname][section][option] = config.get(section, option) 100 | self.cfg = tmp 101 | 102 | def get(self, fname, section = None, option = None): 103 | if (section == None): 104 | try: 105 | return self.cfg[fname] 106 | except KeyError: 107 | return None 108 | elif (option == None): 109 | try: 110 | return self.cfg[fname][section] 111 | except KeyError: 112 | return None 113 | else: 114 | try: 115 | return self.cfg[fname][section][option] 116 | except KeyError: 117 | return None 118 | return None 119 | 120 | class BMConfig(BaseBMConfig): 121 | __metaclass__ = lib.singleton.Singleton 122 | -------------------------------------------------------------------------------- /lib/gpg.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import gnupg 4 | import email 5 | import urllib 6 | import urllib2 7 | import re 8 | import logging 9 | import time 10 | from lib.config import BMConfig 11 | from lib.mysql import BMMySQL 12 | import lib.unicode 13 | import MySQLdb 14 | from BeautifulSoup import BeautifulSoup 15 | import datetime 16 | import time 17 | import traceback 18 | import os 19 | import pyme.core 20 | import pyme.errors 21 | import pyme.constants 22 | #import hkp 23 | 24 | #init 25 | ## GPG key cache 26 | #gpgme.set_protocol(OpenPGP) 27 | gpg = None 28 | gpgme = None 29 | 30 | class KeyEditor: 31 | def __init__(self, mode): 32 | if mode == "addkey": 33 | self.steps = ["addkey", "quit"] 34 | elif mode == "delkey": 35 | self.steps = ["expired_keynum", "delkey", "quit"] 36 | elif mode == "revkey": 37 | self.steps = ["revkey", "quit"] 38 | self.step = 0 39 | 40 | def edit_fnc(self, status, args, out): 41 | # print "[-- Response --]" 42 | # out.seek(0,0) 43 | # print out.read(), 44 | # print "[-- Code: %d, %s --]" % (status, args) 45 | out.seek(0,0) 46 | 47 | if args == "keyedit.prompt": 48 | if self.steps[self.step] == "expired_keynum": 49 | #sub:u:4096:1:39044548FE31C29F:1428010312:1428615112::: 50 | 51 | keynum = 0 52 | result = "help" 53 | 54 | for line in out.read().splitlines(): 55 | items = line.split(":") 56 | if items[0] == "sub": 57 | keynum += 1 58 | if int(items[6]) + BMConfig().get("bmgateway", "pgp", "delete_expired_delay") < time.time(): 59 | print "Key " + str(keynum) + " is expired" 60 | result = "key " + str(keynum) 61 | break 62 | else: 63 | result = self.steps[self.step] 64 | self.step += 1 65 | elif args == "keyedit.save.okay": 66 | result = "Y" 67 | elif args == "keyedit.remove.subkey.okay": 68 | result = "Y" 69 | elif args == "keyedit.revoke.subkey.okay": 70 | result = "Y" 71 | elif args == "ask_revocation_reason.code": 72 | result = "3" 73 | elif args == "ask_revocation_reason.text": 74 | result = "" 75 | elif args == "ask_revocation_reason.okay": 76 | result = "Y" 77 | elif args == "keygen.algo": 78 | result = "6" 79 | elif args == "keygen.size": 80 | result = "4096" 81 | elif args == "keygen.valid": 82 | result = BMConfig().get("bmgateway", "pgp", "expire_subkey") 83 | else: 84 | result = None 85 | 86 | return result 87 | 88 | def gpg_init(): 89 | global gpg 90 | global gpgme 91 | os.environ["GNUPGHOME"] = BMConfig().get("bmgateway", "pgp", "home") 92 | gpg = gnupg.GPG(gnupghome=BMConfig().get("bmgateway", "pgp", "home"), verbose=False) 93 | gpgme = pyme.core.Context() 94 | # gpgme.set_keylist_mode(pyme.constants.KEYLIST_MODE_LOCAL | pyme.constants.KEYLIST_MODE_EXTERN) 95 | 96 | ## encrypt text 97 | def encrypt_text(text, recipient_key, sender_key = None): 98 | global gpgme 99 | plain = pyme.core.Data(lib.unicode.to_string(text)) 100 | encrypted = pyme.core.Data() 101 | if sender_key: 102 | gpgme.signers_clear() 103 | if sender_key.can_sign: 104 | gpgme.signers_add(sender_key) 105 | if not gpgme.signers_enum(0): 106 | # can't sign 107 | return False 108 | gpgme.set_armor(1) 109 | # if not recipient_key.can_encrypt: 110 | # return False 111 | if sender_key: 112 | gpgme.op_encrypt_sign ([recipient_key], 1, plain, encrypted) 113 | logging.info('GPG encrypted to %s, signed by %s' % (recipient_key.subkeys[0].fpr[-8:], sender_key.subkeys[0].fpr[-8:])) 114 | else: 115 | logging.info('GPG encrypted to %s, no signature' % (recipient_key.subkeys[0].fpr[-8:])) 116 | gpgme.op_encrypt ([recipient_key], 1, plain, encrypted) 117 | encrypted.seek (0, 0) 118 | return encrypted.read() 119 | #return str(gpg.encrypt(text, fingerprint, always_trust = True, sign = sign)) 120 | 121 | def sign_text(text, key): 122 | # global gpg 123 | global gpgme 124 | plain = pyme.core.Data(lib.unicode.to_string(text)) 125 | signed = pyme.core.Data() 126 | gpgme.signers_clear() 127 | if key.can_sign: 128 | gpgme.signers_add(key) 129 | if not gpgme.signers_enum(0): 130 | # can't sign 131 | return False 132 | gpgme.op_sign (plain, signed, pyme.pygpgme.GPGME_SIG_MODE_CLEAR) 133 | logging.info('GPG signed by %s, not encrypted' % (key.subkeys[0].fpr[-8:])) 134 | signed.seek (0, 0) 135 | 136 | return str(signed.read()) 137 | 138 | ## add GPG locally 139 | def import_key(data): 140 | # global gpg 141 | # import_result = gpg.import_keys(key) 142 | # logging.info('Imported GPG key') 143 | # return import_result 144 | global gpgme 145 | if isinstance(data, unicode): 146 | newkey = pyme.core.Data(data.encode("utf-8")) 147 | else: 148 | newkey = pyme.core.Data(data) 149 | gpgme.op_import(newkey) 150 | logging.info('Imported GPG key') 151 | result = gpgme.op_import_result() 152 | if result: 153 | for k in dir(result): 154 | break 155 | if not k in result.__dict__ and not k.startswith("_"): 156 | if k == "imports": 157 | print k, ":", 158 | for impkey in result.__getattr__(k): 159 | print " fpr=%s result=%d status=%x" % \ 160 | (impkey.fpr, impkey.result, impkey.status) 161 | else: 162 | print k, ":", result.__getattr__(k) 163 | #FIXME this might not work always 164 | if result.imported > 0 or result.unchanged > 0: 165 | return True 166 | return False 167 | 168 | 169 | def list_keys(searchtext = None): 170 | global gpgme 171 | gpgme.op_keylist_start(searchtext, 0) 172 | # FIXME this does not work 173 | entries = [] 174 | while True: 175 | key = gpgme.op_keylist_next() 176 | if not key: 177 | break 178 | entry = {} 179 | entry['uids'] = [] 180 | for uid in key.uids: 181 | entry['uids'].append(uid.email.lower()) 182 | #print key.uids[0].email.lower() + otheruids + (", expired" if key.expired == 1 else "") 183 | entry['disabled'] = key.disabled 184 | entry['expired'] = key.expired 185 | entry['revoked'] = key.revoked 186 | entry['subkeys'] = [] 187 | for subkey in key.subkeys: 188 | subentry = {} 189 | subentry['expires'] = subkey.expires 190 | subentry['expired'] = subkey.expired 191 | subentry['revoked'] = subkey.revoked 192 | subentry['disabled'] = subkey.disabled 193 | subentry['secret'] = subkey.secret 194 | subentry['invalid'] = subkey.invalid 195 | subentry['keyid'] = subkey.keyid 196 | subentry['fpr'] = subkey.fpr 197 | subentry['can_encrypt'] = subkey.can_encrypt 198 | subentry['can_sign'] = subkey.can_sign 199 | entry['subkeys'].append(subentry) 200 | entries.append(entry) 201 | # pprint.pprint (key.subkeys) 202 | #pprint.pprint (key) 203 | 204 | # print "signature", index, ":" 205 | 206 | # print " summary: ", sign.summary 207 | # print " status: ", sign.status 208 | # print " timestamp: ", sign.timestamp 209 | # print " fingerprint:", sign.fpr 210 | # print " email: ", gpgme.get_key(sign.fpr, 0).uids[0].email.lower() 211 | # print " uid: ", gpgme.get_key(sign.fpr, 0).uids[0].uid 212 | return entries 213 | 214 | 215 | def key_to_mysql(key): 216 | # stupid gpgme library has no support for exporting secret keys 217 | global gpg 218 | public = gpg.export_keys(key.subkeys[0].fpr) # Public 219 | secret = gpg.export_keys(key.subkeys[0].fpr, True) # Private 220 | 221 | rowcount = 0 222 | 223 | cur = BMMySQL().conn().cursor() 224 | cur.execute ("INSERT INTO gpg (email, fingerprint, private, exp, data) VALUES (%s, %s, %s, FROM_UNIXTIME(%s), %s) ON DUPLICATE KEY UPDATE exp = VALUES(exp), data = VALUES(data)", 225 | (key.uids[0].email, key.subkeys[0].fpr, 0, key.subkeys[0].expires, public)) 226 | rowcount += cur.rowcount 227 | cur.execute ("INSERT INTO gpg (email, fingerprint, private, exp, data) VALUES (%s, %s, %s, FROM_UNIXTIME(%s), %s) ON DUPLICATE KEY UPDATE exp = VALUES(exp), data = VALUES(data)", 228 | (key.uids[0].email, key.subkeys[0].fpr, 1, key.subkeys[0].expires, secret)) 229 | rowcount += cur.rowcount 230 | cur.close() 231 | return rowcount 232 | 233 | def key_from_mysql(key): 234 | cur = BMMySQL().conn().cursor(MySQLdb.cursors.DictCursor) 235 | logging.info("Importing GPG keys from SQL for %s", key) 236 | cur.execute ("SELECT data FROM gpg WHERE email = %s", (key)) 237 | for row in cur.fetchall(): 238 | import_key(row['data']) 239 | cur.close() 240 | 241 | def create_primary_key(address): 242 | global gpgme 243 | gpgme.set_armor(1) 244 | params = """ 245 | Key-Type: RSA 246 | Key-Length: 4096 247 | Key-Usage: sign,auth 248 | Name-Real: """ + address + """ 249 | Name-Email: """ + address + """ 250 | Name-Comment: Generated by mailchuck 251 | Expire-Date: """ + BMConfig().get("bmgateway", "pgp", "expire") + """ 252 | 253 | """ 254 | gpgme.op_genkey(params, None, None) 255 | key = check_key(address, whatreturn="key", operation="sign") 256 | key_to_mysql(key) 257 | return upload_key(key) 258 | 259 | def create_subkey(address): 260 | global gpgme 261 | 262 | out = pyme.core.Data() 263 | key = check_key(address, whatreturn="key", operation="sign") 264 | gpgme.op_edit(key, KeyEditor("addkey").edit_fnc, out, out) 265 | subkey = check_key(address, whatreturn="key", operation="encrypt") 266 | key_to_mysql(key) 267 | return upload_key(subkey) 268 | 269 | def delete_expired_subkey(address): 270 | global gpgme 271 | 272 | out = pyme.core.Data() 273 | key = check_key(address, whatreturn="key", operation="any", expired=True) 274 | if key: 275 | gpgme.op_edit(key, KeyEditor("delkey").edit_fnc, out, out) 276 | keyagain = check_key(address, whatreturn="key", operation="any", expired=True) 277 | if not keyagain: 278 | cur = BMMySQL().conn().cursor() 279 | cur.execute ("INSERT INTO gpg (email, fingerprint, private, exp, data) VALUES (%s, %s, %s, FROM_UNIXTIME(%s), %s) ON DUPLICATE KEY UPDATE exp = VALUES(exp), data = VALUES(data)", 280 | (key.uids[0].email, key.subkeys[0].fpr, 0, key.subkeys[0].expires, public)) 281 | rowcount += cur.rowcount 282 | cur.close() 283 | # if changed but still exists, update 284 | # if not exists anymore, delete 285 | 286 | 287 | def upload_key(key): 288 | global gpg 289 | uploaded = 0 290 | for server in BMConfig().get("pgpkeyservers"): 291 | server_url = BMConfig().get("pgpkeyservers", server, "url").split("/")[2] 292 | try: 293 | result = gpg.send_keys(server_url, key.subkeys[0].fpr) 294 | uploaded += 1 295 | logging.debug('Uploading PGP key to ' + server_url + ' : ' + str(result)) 296 | except: 297 | logging.error('Uploading PGP key to ' + server_url + ' fail') 298 | 299 | return (uploaded > 0) 300 | 301 | # create PGP key for user 302 | def create_key(address): 303 | ## generate key 304 | time_start = time.time() 305 | config = BMConfig() 306 | input_data = gpg.gen_key_input(name_email=address, name_real=address, name_comment='Generated by mailchuck.com', key_type="RSA", key_length=4096, expire_date=config.get("bmgateway", "pgp", "expire")) 307 | try: 308 | key = gpg.gen_key(input_data) 309 | except: 310 | return False 311 | time_stop = time.time() 312 | time_total = int(time_stop - time_start) 313 | logging.debug('Generated PGP key for ' + address + ' in ' + str(time_total) + ' seconds') 314 | 315 | ## upload key 316 | keyid = check_key(address, whatreturn="keyid", operation="any") 317 | return (upload_key(keyid) > 0) 318 | 319 | 320 | ## lookup PGP key by email 321 | def download_key(address): 322 | seen = {} 323 | imported = False 324 | for server in BMConfig().get("pgpkeyservers"): 325 | ## try to grab key 326 | try: 327 | soup = BeautifulSoup(urllib2.urlopen(BMConfig().get("pgpkeyservers", server, "url") + 328 | BMConfig().get("pgpkeyservers", server, "begin") + 329 | urllib.quote(address) + 330 | BMConfig().get("pgpkeyservers", server, "end")).read()) 331 | 332 | ## extract key result 333 | key_url = '' 334 | for item in soup(text=re.compile(r'pub ')): 335 | for key_link in item.parent('a'): 336 | key_url = key_link.get('href') 337 | if key_url in seen: 338 | continue 339 | seen[key_url] = True 340 | key_url = BMConfig().get("pgpkeyservers", server, "url") + key_url 341 | key = '' 342 | try: 343 | key_soup = BeautifulSoup(urllib2.urlopen(key_url)) 344 | key = key_soup.find('pre').getText() 345 | if not key: 346 | continue 347 | if import_key(key): 348 | imported = True 349 | except urllib2.URLError, e: 350 | if e == 'HTTP Error 404: Not found': 351 | continue 352 | else: 353 | logging.error('PGP keyfinder encountered an error when contacting the ' + server + ' keyserver: ' + str(e)) 354 | continue 355 | ## if there is an error 356 | except urllib2.URLError, e: 357 | 358 | ## no key available, so return 359 | if e == 'HTTP Error 404: Not found': 360 | continue 361 | 362 | ## something went wrong! 363 | else: 364 | logging.error('PGP keyfinder encountered an error when contacting the ' + server + ' keyserver: ' + str(e)) 365 | continue 366 | return imported 367 | 368 | ## check if we have a GPG key in our keyring 369 | def check_key(address, whatreturn="keyid", operation="any", expired=False): 370 | global gpgme 371 | #gpgme.op_keylist_start(address, 0) 372 | if BMConfig().get("bmgateway", "bmgateway", "outgoing_thread") == 0: 373 | key_from_mysql(address) 374 | #gpgme.set_keylist_mode(pyme.constants.KEYLIST_MODE_LOCAL | pyme.constants.KEYLIST_MODE_EXTERN) 375 | for i in range(0, 1): 376 | for key in gpgme.op_keylist_all(address, 0): 377 | if (key.expired and not expired) or key.disabled or key.revoked: 378 | continue 379 | # TODO differentiate signing and encryption 380 | for subkey in key.subkeys: 381 | if (not expired and not subkey.expired) and not subkey.disabled and not subkey.revoked and (operation == "any" or 382 | (operation == "encrypt" and subkey.can_encrypt) or (operation == "sign" and subkey.can_sign)): 383 | if whatreturn == "keyid": 384 | return subkey.keyid 385 | elif whatreturn == "fpr": 386 | return subkey.fpr 387 | elif whatreturn == "key": 388 | return key 389 | else: 390 | return subkey 391 | if i == 0 and address and not download_key(address): 392 | break 393 | return False 394 | 395 | def verify(signed, msg_sender, msg_recipient, detached_sig = None): 396 | global gpgme 397 | plain = pyme.core.Data() 398 | cipher = pyme.core.Data(lib.unicode.to_string(signed)) 399 | retval = signed 400 | 401 | check_key (msg_sender, whatreturn = "key", operation = "any") 402 | 403 | try: 404 | if detached_sig: 405 | gpgme.op_verify(pyme.core.Data(detached_sig), cipher, None) 406 | else: 407 | gpgme.op_verify(cipher, None, plain) 408 | if plain: 409 | plain.seek(0,0) 410 | retval = plain.read() 411 | except: 412 | logging.error("Signature verification of email destined for " + msg_recipient + " failed") 413 | return False, False 414 | return retval, verify_parse(msg_sender) 415 | 416 | def verify_parse(msg_sender): 417 | global gpgme 418 | # signatures 419 | verifiedresult = gpgme.op_verify_result() 420 | verified = False 421 | index = 0 422 | for sign in verifiedresult.signatures: 423 | index += 1 424 | try: 425 | for uid in gpgme.get_key(sign.fpr, 0).uids: 426 | if uid.email.lower() == msg_sender and sign.status == 0: 427 | verified = True 428 | except: 429 | pass 430 | # print "signature", index, ":" 431 | # print " summary: ", sign.summary 432 | # print " status: ", sign.status 433 | # print " timestamp: ", sign.timestamp 434 | # print " fingerprint:", sign.fpr 435 | # print " email: ", gpgme.get_key(sign.fpr, 0).uids[0].email.lower() 436 | # print " uid: ", gpgme.get_key(sign.fpr, 0).uids[0].uid 437 | return verified 438 | 439 | def decrypt_content(encrypted, msg_sender, msg_recipient, multi = False): 440 | global gpgme 441 | plain = pyme.core.Data() 442 | cipher = pyme.core.Data(lib.unicode.to_string(encrypted)) 443 | decrypted = "" 444 | decrypted_raw = "" 445 | detached_sig = None 446 | 447 | check_key (msg_sender, whatreturn = "key", operation = "any") 448 | 449 | try: 450 | gpgme.op_decrypt_verify(cipher, plain) 451 | except: 452 | logging.error("Decryption of email destined for " + msg_recipient + " failed") 453 | return False, False 454 | 455 | ## convert to email message format 456 | plain.seek(0,0) 457 | 458 | ## extract decrypted data 459 | if multi: 460 | decrypted_msg = email.message_from_string(plain.read()) 461 | for decrypted_part in decrypted_msg.walk(): 462 | if decrypted_part.get_content_type() == "text/plain": 463 | decrypted_str = decrypted_part.get_payload(decode=1) 464 | decrypted_raw += decrypted_part.as_string(False) 465 | if decrypted_part.get_content_charset(): 466 | decrypted += decrypted_str.decode(decrypted_part.get_content_charset()) 467 | else: 468 | decrypted += decrypted_str 469 | elif decrypted_part.get_content_type() == "application/pgp-signature": 470 | detached_sig = decrypted_part.get_payload(decode=1) 471 | else: 472 | decrypted = plain.read() 473 | 474 | if detached_sig: 475 | trash, sigverify = verify(decrypted_raw, msg_sender, msg_recipient, detached_sig) 476 | return decrypted, sigverify 477 | else: 478 | return decrypted, verify_parse(msg_sender) 479 | -------------------------------------------------------------------------------- /lib/lmtpd.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python 2 | 3 | from smtpd import SMTPChannel, SMTPServer 4 | 5 | class LMTPChannel(SMTPChannel): 6 | # LMTP "LHLO" command is routed to the SMTP/ESMTP command 7 | def smtp_LHLO(self, arg): 8 | self.smtp_HELO(arg) 9 | 10 | def smtp_RCPT(self, arg): 11 | print >> DEBUGSTREAM, '===> RCPT', arg 12 | if not self.__mailfrom: 13 | self.push('503 Error: need MAIL command') 14 | return 15 | address = self.__getaddr('TO:', arg) if arg else None 16 | if not address: 17 | self.push('501 Syntax: RCPT TO:
') 18 | return 19 | ### Check for a valid mailbox 20 | if not address in VALID_ADDRESSES: 21 | self.push("550 No such user here") 22 | return 23 | self.__rcpttos.append(address) 24 | print >> DEBUGSTREAM, 'recips:', self.__rcpttos 25 | self.push('250 Ok') 26 | 27 | class LMTPServer(SMTPServer): 28 | def __init__(self, localaddr, remoteaddr): 29 | SMTPServer.__init__(self, localaddr, remoteaddr) 30 | 31 | def process_message(self, peer, mailfrom, rcpttos, data): 32 | print 'Receiving message from:', peer 33 | print 'Message addressed from:', mailfrom 34 | #rcpttos is a list 35 | print 'Message addressed to :', rcpttos 36 | print 'Message length :', len(data) 37 | print 'Message :', data 38 | return 39 | 40 | def handle_accept(self): 41 | conn, addr = self.accept() 42 | channel = LMTPChannel(self, conn, addr) 43 | 44 | 45 | -------------------------------------------------------------------------------- /lib/maintenance.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2.7 2 | 3 | from lib.mysql import BMMySQL 4 | 5 | class MaintenanceThread (object): 6 | def getSchedule(key): 7 | cur = BMMySQL().conn().cursor(MySQLdb.cursors.DictCursor) 8 | cur.execute ("SELECT ts FROM schedules WHERE id = %s", (key)) 9 | ret = None 10 | for row in cur.fetchall(): 11 | ret = row['ts'] 12 | cur.close() 13 | return ret 14 | 15 | def sanitisePayments(): 16 | return None 17 | 18 | def revokeOldPGP(): 19 | return None 20 | 21 | def deleteOldPGP(): 22 | return None 23 | 24 | def generateNewPGP(): 25 | return None 26 | 27 | def deleteOldMEGA(): 28 | return None 29 | 30 | def notifyExpiration(): 31 | for days in (1, 3, 7): 32 | pass 33 | return None 34 | 35 | def maintenance_thread(): 36 | pass 37 | -------------------------------------------------------------------------------- /lib/milter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2.7 2 | 3 | import socket 4 | import Milter 5 | import Milter.utils 6 | import hashlib 7 | import MySQLdb 8 | from lib.mysql import BMMySQL 9 | from lib.config import BMConfig 10 | import lib.user 11 | 12 | class BMMilter(Milter.Base): 13 | def __init__ (self): 14 | self.mailfrom = None # sender in SMTP form 15 | self.id = Milter.uniqueID() 16 | 17 | def envfrom (self, f, *str): 18 | addr = Milter.utils.parse_addr(f.lower()) 19 | if len(addr) == 2: 20 | self.mailfrom = addr[0] + "@" + addr[1] 21 | else: 22 | self.mailfrom = f.lower() 23 | return Milter.CONTINUE 24 | 25 | def envrcpt (self, to, *str): 26 | # <> 27 | if self.mailfrom == "": 28 | return Milter.CONTINUE 29 | 30 | # de +ify 31 | addr = Milter.utils.parse_addr(to.lower()) 32 | rcpt = addr[0] + "@" + addr[1] 33 | 34 | userdata = lib.user.GWUser(email = rcpt) 35 | 36 | # non exising user 37 | if not userdata.check(): 38 | return Milter.CONTINUE 39 | 40 | if userdata.cancharge == 0 or userdata.masterpubkey_btc == None or userdata.feeamount == 0: 41 | return Milter.CONTINUE 42 | 43 | h = hashlib.new('sha256') 44 | h.update (self.mailfrom + "!" + rcpt); 45 | digest = h.digest() 46 | 47 | cur = BMMySQL().conn().cursor(MySQLdb.cursors.DictCursor) 48 | 49 | cur.execute("SELECT id, status FROM sendercharge WHERE hash = %s", (digest)) 50 | 51 | result = False 52 | for row in cur.fetchall(): 53 | result = row 54 | 55 | if result and result['status'] == 1: 56 | cur.close() 57 | return Milter.CONTINUE 58 | 59 | if result: # result['status'] == 0 60 | cur.execute("SELECT address, amount FROM invoice WHERE sendercharge_id = %s", (row['id'])) 61 | for row in cur.fetchall(): 62 | result = row 63 | url = lib.payment.create_payment_url(result['address'], 'BTC', result['amount'], rcpt, 'Sending emails') 64 | else: # no record 65 | btcaddress, amount = lib.payment.create_invoice_user(rcpt) 66 | url = lib.payment.create_payment_url(btcaddress, 'BTC', amount, rcpt, 'Sending emails') 67 | cur.execute("INSERT INTO sendercharge (hash, status) values (%s, %s)", (digest, 0)) 68 | sendercharge_id = cur.lastrowid 69 | cur.execute("UPDATE invoice SET sendercharge_id = %s WHERE address = %s and coin = 'BTC'", (sendercharge_id, btcaddress)) 70 | cur.close() 71 | 72 | url = url.replace("%", "%%") 73 | self.setreply("550", "5.7.0", "PAYMENT REQUIRED: %s" % url) 74 | return Milter.REJECT 75 | 76 | # signal handlers 77 | def stop(self): 78 | return Milter.TEMPFAIL 79 | 80 | def abort(self): 81 | return Milter.TEMPFAIL 82 | 83 | def run(): 84 | Milter.factory = BMMilter 85 | socket.setdefaulttimeout(60) 86 | Milter.runmilter("bmmilter", BMConfig().get("bmgateway", "bmgateway", "miltersocket") ,600) 87 | -------------------------------------------------------------------------------- /lib/msgtemplate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2.7 2 | 3 | import urlparse 4 | import os 5 | import email 6 | import time 7 | import lib.payment 8 | import string 9 | import logging 10 | import re 11 | from lib.config import BMConfig 12 | from lib.bmapi import BMAPI 13 | 14 | class MsgTemplate(object): 15 | subject = None 16 | body = None 17 | def __init__(self, maps, base): 18 | # load template source 19 | src = None 20 | try: 21 | with open(os.path.join(os.path.dirname(__file__), '..', 'templates', base + '.txt')) as tempsrc: 22 | src = email.message_from_file(tempsrc) 23 | except: 24 | return None 25 | 26 | # init general maps 27 | maps['timestamp'] = time.strftime("Generated at: %b %d %Y %H:%M:%S GMT", time.gmtime()) 28 | maps['domain'] = BMConfig().get("bmgateway", "bmgateway", "domain_name") 29 | maps['relayaddress'] = BMAPI().get_address(BMConfig().get("bmgateway", "bmgateway", "relay_address_label")) 30 | maps['deregisteraddress'] = BMAPI().get_address(BMConfig().get("bmgateway", "bmgateway", "deregistration_address_label")) 31 | maps['registeraddress'] = BMAPI().get_address(BMConfig().get("bmgateway", "bmgateway", "registration_address_label")) 32 | maps['bugreportemail'] = BMConfig().get("bmgateway", "bmgateway", "bug_report_address_email") 33 | maps['bugreportaddress'] = BMConfig().get("bmgateway", "bmgateway", "bug_report_address_bitmessage") 34 | maps['mailinglistaddress'] = BMConfig().get("bmgateway", "bmgateway", "broadcast_address_bitmesage") 35 | maps['companyname'] = BMConfig().get("bmgateway", "bmgateway", "companyname") 36 | maps['companyaddress'] = BMConfig().get("bmgateway", "bmgateway", "companyaddress") 37 | 38 | # BTC URI 39 | if 'btcuri' in maps: 40 | addr = re.search('^bitcoin:([^?]+)(\?(.*))?', maps['btcuri']) 41 | if addr: 42 | maps['btcaddress'] = addr.group(1) 43 | attr = urlparse.parse_qs(addr.group(3)) 44 | if 'amount' in attr: 45 | maps['btcamount'] = attr['amount'][0] 46 | maps['qrbtcuri'] = lib.payment.qrcode_encoded(maps['btcuri']) 47 | 48 | subst = string.Template(src.get_payload()).safe_substitute(maps) 49 | self.body = subst.replace('\n', '\r\n') 50 | 51 | if src.has_key("Subject"): 52 | self.subject = string.Template(src.get("Subject")).safe_substitute(maps) 53 | #return self 54 | 55 | def getsubject(self): 56 | return self.subject 57 | 58 | def getbody(self): 59 | return self.body 60 | -------------------------------------------------------------------------------- /lib/mysql.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2.7 2 | 3 | from lib.config import BMConfig 4 | import lib.singleton 5 | import os 6 | import sys 7 | import time 8 | import MySQLdb 9 | import MySQLdb.converters 10 | from MySQLdb.constants import FIELD_TYPE 11 | import threading 12 | from warnings import filterwarnings 13 | 14 | class BaseBMMySQL(object): 15 | def __init__(self): 16 | self.thrdata = threading.local() 17 | #self.thrdata.db = self.connect() 18 | 19 | def check_connection(self): 20 | if not (hasattr(self.thrdata, 'db') and self.thrdata.db is not None): 21 | self.connect() 22 | # if not bool(self.address_list): 23 | # self._load_address_list() 24 | return self.thrdata.db 25 | 26 | def connect(self): 27 | orig_conv = MySQLdb.converters.conversions 28 | #Adding support for bit data type 29 | orig_conv[FIELD_TYPE.BIT] = bool 30 | 31 | for mysql in BMConfig().get("mysql"): 32 | if BMConfig().get("mysql", mysql, "unix_socket"): 33 | try: 34 | self.thrdata.db = MySQLdb.connect(unix_socket = BMConfig().get("mysql", mysql, "unix_socket"), 35 | user = BMConfig().get("mysql", mysql, "user"), 36 | passwd = BMConfig().get("mysql", mysql, "passwd"), 37 | db = BMConfig().get("mysql", mysql, "db"), conv = orig_conv) 38 | return self.thrdata.db 39 | except MySQLdb.Error, e: 40 | print "MySQLdb.Error is %d: %s" % (e.args[0], e.args[1]) 41 | continue 42 | except: 43 | print "Error connecting to " + mysql 44 | continue 45 | elif BMConfig().get("mysql", mysql, "host"): 46 | try: 47 | self.thrdata.db = MySQLdb.connect(host = BMConfig().get("mysql", mysql, "host"), 48 | user = BMConfig().get("mysql", mysql, "user"), 49 | passwd = BMConfig().get("mysql", mysql, "passwd"), 50 | db = BMConfig().get("mysql", mysql, "db")) 51 | return self.thrdata.db 52 | except MySQLdb.Error, e: 53 | print "MySQLdb.Error is %d: %s" % (e.args[0], e.args[1]) 54 | continue 55 | except: 56 | print "Error connecting to " + mysql 57 | continue 58 | else: 59 | self.thrdata.db = None 60 | print "No host or unix socket in mysql definition for " + mysql 61 | return False 62 | 63 | def ping(self): 64 | ok = False 65 | while not ok: 66 | try: 67 | self.thrdata.db.ping(True) 68 | ok = True 69 | except: 70 | if not self.connect(): 71 | time.sleep(5) 72 | 73 | def filter_column_names (self, table, data): 74 | self.ping() 75 | cur = self.thrdata.db.cursor() 76 | cur.execute("SHOW COLUMNS FROM user") 77 | all_column_names = {} 78 | for row in cur.fetchall(): 79 | all_column_names[row[0]] = True 80 | cur.close() 81 | column_names = {} 82 | for key in data: 83 | if key in all_column_names: 84 | column_names[key] = data[key] 85 | return column_names 86 | 87 | def conn(self): 88 | return self.check_connection() 89 | 90 | class BMMySQL(BaseBMMySQL): 91 | __metaclass__ = lib.singleton.Singleton 92 | -------------------------------------------------------------------------------- /lib/netstring.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | def readns(sock): 4 | """read a netstring from a socket.""" 5 | size = "" 6 | while 1: 7 | c = sock.recv(1) 8 | if c == ":": 9 | break 10 | elif not c: 11 | raise IOError, "short netstring read 1" 12 | size = size + c 13 | size = sz = int(size) 14 | s = "" 15 | while sz: 16 | ss = sock.recv(sz) 17 | if not ss: 18 | raise IOError, "short netstring read 2" 19 | s += ss 20 | sz -= len(ss) 21 | if len(s) != size: 22 | raise IOError, "short netstring read 3" 23 | if sock.recv(1) != ",": 24 | raise IOError, "missing netstring terminator" 25 | return s 26 | 27 | def writens(sock, s): 28 | """write a netstring to a socket.""" 29 | s = encode(s) 30 | while len(s): 31 | l = sock.send(s) 32 | s = s[l:] 33 | 34 | def encode(s): 35 | return "%d:%s," % (len(s), s) 36 | 37 | def decode(s): 38 | try: 39 | if s[-1] != ",": 40 | raise ValueError 41 | p = s.index(":") 42 | l = int(s[0:p]) 43 | if len(s) != p + l + 2: 44 | raise ValueError 45 | return s[p+1:-1] 46 | except ValueError: 47 | raise ValueError, "netstring format error: " + s 48 | 49 | def freadns(f): 50 | """read a netstring from a file.""" 51 | size = "" 52 | while 1: 53 | c = f.read(1) 54 | if c == ":": 55 | break 56 | elif not c: 57 | raise IOError, "short netstring read" 58 | size = size + c 59 | size = sz = int(size) 60 | s = "" 61 | while sz: 62 | ss = f.read(sz) 63 | if not ss: 64 | raise IOError, "short netstring read" 65 | s += ss 66 | sz -= len(ss) 67 | if len(s) != size: 68 | raise IOError, "short netstring read" 69 | if f.read(1) != ",": 70 | raise IOError, "missing netstring terminator" 71 | return s 72 | 73 | def fwritens(f, s): 74 | """write a netstring to a file.""" 75 | s = encode(s) 76 | f.write(s) 77 | 78 | -------------------------------------------------------------------------------- /lib/parsers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2.7 2 | 3 | import email 4 | import logging 5 | import lib.payment 6 | import os.path 7 | from lib.config import BMConfig 8 | import logging 9 | import chardet 10 | #from lib.user import BMUser 11 | import email 12 | 13 | class BMParser(object): 14 | 15 | -------------------------------------------------------------------------------- /lib/payment.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2.7 2 | 3 | import urllib, json 4 | import qrcode 5 | import base64 6 | import bitcoin 7 | import MySQLdb 8 | import StringIO 9 | import jsonrpclib 10 | import decimal 11 | from warnings import filterwarnings 12 | from lib.config import BMConfig 13 | from lib.mysql import BMMySQL 14 | 15 | def create_payment_url(address, currency, amount, label, message): 16 | base = "https://mailchuck.com/payment/#" 17 | base += create_payment_uri(address, currency, amount, label, message) 18 | return base 19 | 20 | def create_payment_uri(address, currency, amount, label, message): 21 | base = "bitcoin:" + address 22 | if currency in ("USD", "GBP", "EUR"): 23 | amount /= get_bitcoin_price(currency) 24 | amount = "%.8f" % amount 25 | base += "?amount=" + amount 26 | base += "&label=" + urllib.quote(label, safe='~()*!.\'') 27 | base += "&message=" + urllib.quote(message, safe='~()*!.\'') 28 | return base 29 | 30 | def payment_exists_domain (domain, payer): 31 | cur = BMMySQL().conn().cursor(MySQLdb.cursors.DictCursor) 32 | cur.execute ("SELECT address, amount FROM invoice WHERE type = 0 AND payer = %s AND paid = '0000-00-00 00:00:00'", (payer)) 33 | result = False 34 | for row in cur.fetchall(): 35 | result = row 36 | if result: 37 | return result['address'], result['amount'] 38 | cur.close() 39 | return False, False 40 | 41 | 42 | def create_invoice_domain (domain, payer): 43 | cur = BMMySQL().conn().cursor(MySQLdb.cursors.DictCursor) 44 | filterwarnings('ignore', category = MySQLdb.Warning) 45 | cur.execute ("SELECT bm, masterpubkey_btc, offset_btc, feeamount, feecurrency FROM domain WHERE name = %s AND active = 1", (domain)) 46 | result = False 47 | for row in cur.fetchall(): 48 | result = row 49 | while result: 50 | if result['masterpubkey_btc'][0:4] == "xpub": 51 | # BIP44 52 | dpk1 = bitcoin.bip32_ckd(result['masterpubkey_btc'], 0) 53 | dpk2 = bitcoin.bip32_ckd(dpk1, result['offset_btc']) 54 | pubkey = bitcoin.bip32_extract_key(dpk2) 55 | else: 56 | # Electrum 1.x 57 | pubkey = bitcoin.electrum_pubkey(result['masterpubkey_btc'], result['offset_btc']) 58 | address = bitcoin.pubkey_to_address(pubkey) 59 | bitcoind_importaddress(address) 60 | cur.execute ("UPDATE domain SET offset_btc = offset_btc + 1 WHERE name = %s AND active = 1 AND masterpubkey_btc = %s", (domain, result['masterpubkey_btc'])) 61 | if result['feecurrency'] in ("USD", "GBP", "EUR"): 62 | result['feeamount'] /= decimal.Decimal(get_bitcoin_price(result['feecurrency'])) 63 | cur.execute ("INSERT IGNORE INTO invoice (issuer, payer, address, coin, amount, type, paid) VALUES (%s, %s, %s, 'BTC', %s, 0, 0)", (result['bm'], payer, address, result['feeamount'])) 64 | 65 | # invoice already exists for that address, increment 66 | if cur.rowcount == 0: 67 | cur.execute ("SELECT bm, masterpubkey_btc, offset_btc, feeamount, feecurrency FROM domain WHERE name = %s AND active = 1", (domain)) 68 | result = False 69 | for row in cur.fetchall(): 70 | result = row 71 | continue 72 | 73 | cur.close() 74 | return address, result['feeamount']; 75 | cur.close() 76 | return False 77 | 78 | 79 | def create_invoice_user (email): 80 | cur = BMMySQL().conn().cursor(MySQLdb.cursors.DictCursor) 81 | cur.execute ("SELECT bm, masterpubkey_btc, offset_btc, feeamount, feecurrency FROM user WHERE email = %s AND active = 1", (email)) 82 | result = False 83 | for row in cur.fetchall(): 84 | result = row 85 | if result: 86 | if result['masterpubkey_btc'][0:4] == "xpub": 87 | # BIP44 88 | dpk1 = bitcoin.bip32_ckd(result['masterpubkey_btc'], 0) 89 | dpk2 = bitcoin.bip32_ckd(dpk1, result['offset_btc']) 90 | pubkey = bitcoin.bip32_extract_key(dpk2) 91 | else: 92 | # Electrum 1.x 93 | pubkey = bitcoin.electrum_pubkey(result['masterpubkey_btc'], result['offset_btc']) 94 | address = bitcoin.pubkey_to_address(pubkey) 95 | bitcoind_importaddress(address) 96 | cur.execute ("UPDATE user SET offset_btc = offset_btc + 1 WHERE email = %s AND active = 1 AND masterpubkey_btc = %s", (email, result['masterpubkey_btc'])) 97 | if result['feecurrency'] in ("USD", "GBP", "EUR"): 98 | result['feeamount'] /= decimal.Decimal(get_bitcoin_price(result['feecurrency'])) 99 | cur.execute ("INSERT INTO invoice (issuer, address, coin, amount, type, paid) VALUES (%s, %s, 'BTC', %s, 1, 0)", (result['bm'], address, result['feeamount'])) 100 | cur.close() 101 | return address, result['feeamount']; 102 | cur.close() 103 | return False 104 | 105 | def get_bitcoin_price (currency): 106 | if currency in ("USD", "GBP", "EUR"): 107 | url = 'https://api.coindesk.com/v1/bpi/currentprice.json' 108 | response = urllib.urlopen(url) 109 | data = json.loads(response.read()) 110 | return data['bpi'][currency]['rate_float'] 111 | else: 112 | return False 113 | 114 | def bitcoind_importaddress (address): 115 | bitcoinrpc = jsonrpclib.Server('http://' + 116 | BMConfig().get("bmgateway", "bitcoind", "username") + ":" + 117 | BMConfig().get("bmgateway", "bitcoind", "password") + "@" + 118 | BMConfig().get("bmgateway", "bitcoind", "host") + ":" + 119 | str(BMConfig().get("bmgateway", "bitcoind", "port")) + "/") 120 | # returns null on success 121 | rpcreply = bitcoinrpc.importaddress(address, "", False) 122 | 123 | def qrcode_encoded(data): 124 | qr = qrcode.QRCode(version = None, error_correction = qrcode.constants.ERROR_CORRECT_L, box_size = 5, border = 4) 125 | qr.add_data(data) 126 | qr.make(fit=True) 127 | img = qr.make_image() 128 | tmp = StringIO.StringIO() 129 | img.save(tmp) 130 | out = "data:image/png;base64," + base64.b64encode(tmp.getvalue()) 131 | tmp.close() 132 | return out 133 | -------------------------------------------------------------------------------- /lib/sendbm.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2.7 2 | 3 | import urlparse 4 | import os 5 | import base64 6 | import email 7 | import logging 8 | import lib.payment 9 | from lib.config import BMConfig 10 | from lib.bmapi import BMAPI 11 | from lib.msgtemplate import MsgTemplate 12 | 13 | class SendBM(object): 14 | def __init__ (self, sender, recipient, subject, body): 15 | self.subject = base64.b64encode(subject) 16 | self.body = base64.b64encode(body) 17 | self.status = False 18 | self.ackdata = None 19 | if (sender[0:3] == "BM-"): 20 | senderbm = sender 21 | else: 22 | senderbm = BMAPI().get_address(sender) 23 | recipientbm = recipient 24 | userdata = lib.user.GWUser(bm = recipient) 25 | if userdata.check(): 26 | recipient = userdata.email 27 | self.status = False 28 | try: 29 | ackData = BMAPI().conn().sendMessage(recipientbm, senderbm, self.subject, self.body, 2) 30 | logging.info("Sent BM from %s to %s: %s", sender, recipient, ackData) 31 | self.ackdata = ackData 32 | self.status = True 33 | except: 34 | logging.error("Failure sending BM from %s to %s", sender, recipient) 35 | 36 | class SendBMTemplate(SendBM): 37 | def __init__ (self, sender, recipient, template, addmaps = None): 38 | maps = {'sender' : sender, 'recipient' : recipient } 39 | if isinstance(addmaps, dict): 40 | for key, value in addmaps.iteritems(): 41 | maps[key] = value 42 | 43 | obj = MsgTemplate(maps = maps, base = template) 44 | #super(SendBMTemplate, self).__init__(self, sender = sender, recipient = recipient, subject = obj.subject(), body = obj.body()) 45 | SendBM.__init__(self, sender = sender, recipient = recipient, subject = obj.getsubject(), body = obj.getbody()) 46 | 47 | #class RelayBM(SendBM): 48 | #def __init (self, sender, recipient): 49 | #maps 50 | -------------------------------------------------------------------------------- /lib/singleton.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | class Singleton(type): 4 | _instances = {} 5 | 6 | def __call__(cls, *args, **kwargs): 7 | if cls not in cls._instances: 8 | cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) 9 | return cls._instances[cls] 10 | -------------------------------------------------------------------------------- /lib/socketmap.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2.7 2 | 3 | import SocketServer 4 | import xmlrpclib 5 | import base64 6 | import lib.netstring 7 | import json 8 | import pprint 9 | from lib.config import BMConfig 10 | 11 | class MyTCPHandler(SocketServer.BaseRequestHandler): 12 | """ 13 | The RequestHandler class for our server. 14 | 15 | It is instantiated once per connection to the server, and must 16 | override the handle() method to implement communication to the 17 | client. 18 | """ 19 | 20 | def handle(self): 21 | # self.request is the TCP socket connected to the client 22 | api = xmlrpclib.ServerProxy('http://' + BMConfig().get("bmapi", "bm1", "username") + ':' + 23 | BMConfig().get("bmapi", "bm1", "password") + '@' + 24 | BMConfig().get("bmapi", "bm1", "host") + ':' + 25 | str(BMConfig().get("bmapi", "bm1", "port")) + '/') 26 | 27 | closed = 0 28 | while not closed: 29 | try: 30 | rq = lib.netstring.readns(self.request) 31 | #print rq 32 | table, key = rq.split(" ", 1) 33 | except: 34 | lib.netstring.writens(self.request, "TEMP Network error") 35 | closed = 1 36 | break 37 | 38 | 39 | key = key.lower() 40 | enckey = base64.b64encode(key) 41 | 42 | print "Searching for " + key + " in " + table 43 | 44 | #out = api.listAddressBookEntries(enckey) 45 | #pprint.pprint(out) 46 | ret = 0 47 | try: 48 | bmaddrs = json.loads(api.listAddressBookEntries(enckey))['addresses'] 49 | for address in bmaddrs: 50 | if base64.b64decode(address['label']) == key: 51 | ret = address['address'] 52 | except: 53 | ret = 1 54 | 55 | try: 56 | if ret == 0: 57 | lib.netstring.writens(self.request, "NOTFOUND") 58 | elif ret == 1: 59 | lib.netstring.writens(self.request, "TEMP Network error") 60 | else: 61 | lib.netstring.writens(self.request, "OK bmgateway") 62 | except: 63 | closed = 1 64 | break 65 | -------------------------------------------------------------------------------- /lib/unicode.py: -------------------------------------------------------------------------------- 1 | def to_string(data): 2 | if isinstance(data, unicode): 3 | return str(data) 4 | elif isinstance(data, str): 5 | return data 6 | else: 7 | return str(data) 8 | -------------------------------------------------------------------------------- /lib/user.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2.7 2 | 3 | from lib.config import BMConfig 4 | from lib.mysql import BMMySQL 5 | import MySQLdb 6 | import lib.singleton 7 | import os 8 | import sys 9 | import pwd 10 | import datetime 11 | import logging 12 | from warnings import filterwarnings 13 | 14 | class GWUser(object): 15 | def __init__(self, empty = False, bm = None, uid = None, email = None, unalias = False): 16 | if bm != None: 17 | self.load(bm = bm) 18 | elif uid != None: 19 | self.load(uid = uid) 20 | elif email != None: 21 | self.load(email = email, unalias = unalias) 22 | elif empty: 23 | self.uid = None 24 | else: 25 | # if no arguments, return all 26 | self.load() 27 | 28 | def load(self, bm = None, uid = None, email = None, unalias = False): 29 | cur = BMMySQL().conn().cursor(MySQLdb.cursors.DictCursor) 30 | filterwarnings('ignore', category = MySQLdb.Warning) 31 | multirow = False 32 | self.uid = None 33 | if bm != None: 34 | cur.execute ("SELECT * FROM user WHERE bm = %s", (bm)) 35 | elif uid != None: 36 | cur.execute ("SELECT * FROM user WHERE uid = %s", (uid)) 37 | elif email != None: 38 | if unalias: 39 | alias = GWAlias(email = email).target 40 | if alias: 41 | email = alias 42 | cur.execute ("SELECT * FROM user WHERE email = %s", (email)) 43 | else: 44 | # if no arguments, return all 45 | multirow = True 46 | cur.execute ("SELECT * FROM user ORDER BY email") 47 | 48 | for row in cur.fetchall(): 49 | if multirow: 50 | print "%1s %-40s %-39s " % ("*" if row['active'] else " ", row['email'], row['bm']) 51 | else: 52 | for column in row: 53 | setattr(self, column, row[column]) 54 | cur.close() 55 | if hasattr(self, 'email'): 56 | self.aliases = GWAlias(alias = self.email).aliases 57 | 58 | def check(self): 59 | return self.uid != None 60 | 61 | def expired(self): 62 | return self.exp < datetime.date.today() 63 | 64 | def add(self, bm, email, postmap = None): 65 | cur = BMMySQL().conn().cursor() 66 | if postmap == None: 67 | postmap = pwd.getpwuid(os.getuid())[0] 68 | trash, domain = email.split("@") 69 | filterwarnings('ignore', category = MySQLdb.Warning) 70 | pgp = 1 if BMConfig().get("bmgateway", "default", "pgp") else 0 71 | attachments = 1 if BMConfig().get("bmgateway", "default", "attachments") else 0 72 | cur.execute ("""INSERT IGNORE INTO user (bm, email, postmap, domain, pgp, credits, exp, active, cansend, cancharge, caninvoice, attachments, html, lastackreceived) 73 | VALUES (%s, %s, %s, %s, %s, '0', '1971-01-01', 1, 1, 0, 0, %s, 0, UNIX_TIMESTAMP(NOW()))""", (bm, email, postmap, domain, pgp, attachments)) 74 | uid = None 75 | if cur.rowcount == 1: 76 | uid = cur.lastrowid 77 | self.load(uid = uid) 78 | logging.info('Registered new user (%u) %s', uid, email) 79 | else: 80 | logging.error('Failed to add new user entry into the database for %s', email) 81 | cur.close() 82 | return uid 83 | 84 | def delete(self): 85 | cur = BMMySQL().conn().cursor() 86 | filterwarnings('ignore', category = MySQLdb.Warning) 87 | if self.uid: 88 | cur.execute ("DELETE FROM user WHERE uid = %s", (self.uid)) 89 | if (cur.rowcount == 1): 90 | logging.info('Deleted user (%u) %s / %s', self.uid, self.email, self.bm) 91 | elif (cur.rowcount > 1): 92 | logging.warning('Deleted user (%u) returned more than one row', self.uid) 93 | else: 94 | logging.info('Asked to delete nonexisting user') 95 | cur.close() 96 | 97 | def update(self, data): 98 | col_names = BMMySQL().filter_column_names("user", data) 99 | cur = BMMySQL().conn().cursor() 100 | update_list = [] 101 | for key in col_names: 102 | if data[key] is not None: 103 | update_list.append("`" + key + "`" + " = \"" + BMMySQL().conn().escape_string(data[key]) + "\"") 104 | if len(update_list) == 0: 105 | return False 106 | cur.execute("UPDATE user SET " + ", ".join(update_list) + " WHERE bm = %s", (self.bm)) 107 | #print ("UPDATE user SET " + ", ".join(update_list) + " WHERE bm = %s" % (self.bm)) 108 | if cur.rowcount == 1: 109 | cur.close() 110 | return True 111 | else: 112 | cur.close() 113 | return False 114 | 115 | def setlastrelay(self, lastrelay = None): 116 | cur = BMMySQL().conn().cursor() 117 | filterwarnings('ignore', category = MySQLdb.Warning) 118 | if lastrelay == None: 119 | cur.execute ("UPDATE user SET lastrelay = UNIX_TIMESTAMP() WHERE uid = %s", (self.uid)) 120 | else: 121 | cur.execute ("UPDATE user SET lastrelay = %s WHERE uid = %s", (lastrelay, self.uid)) 122 | if (cur.rowcount == 1): 123 | logging.debug('Set lastrelay for (%u)', self.uid) 124 | else: 125 | logging.warning('Failure setting lastrelay for (%u)', self.uid) 126 | cur.close() 127 | 128 | def setlastackreceived(self, lastackreceived = None): 129 | # for example user deleted 130 | if self.uid is None: 131 | return 132 | cur = BMMySQL().conn().cursor() 133 | filterwarnings('ignore', category = MySQLdb.Warning) 134 | if lastackreceived == None: 135 | cur.execute ("UPDATE user SET lastackreceived = UNIX_TIMESTAMP() WHERE uid = %s", (self.uid)) 136 | else: 137 | cur.execute ("UPDATE user SET lastackreceived = %s WHERE uid = %s", (lastackreceived, self.uid)) 138 | if (cur.rowcount == 1): 139 | logging.debug('Set lastackreceived for (%u)', self.uid) 140 | else: 141 | logging.warning('Failure setting lastackreceived for (%u)', self.uid or -1) 142 | cur.close() 143 | 144 | class GWUserData(object): 145 | 146 | @staticmethod 147 | def zero_one(text): 148 | text = text.lower() 149 | if text in ("1", "on", "true", "yes"): 150 | return "1" 151 | if text in ("0", "off", "false", "no"): 152 | return "0" 153 | else: 154 | return None 155 | @staticmethod 156 | def pgp(text): 157 | text = text.lower() 158 | if text in ("server"): 159 | return "1" 160 | if text in ("local"): 161 | return "0" 162 | else: 163 | return GWUserData.zero_one(text) 164 | 165 | @staticmethod 166 | def is_float(text): 167 | try: 168 | i = float(text) 169 | return True 170 | except (ValueError, TypeError): 171 | return False 172 | return False 173 | 174 | @staticmethod 175 | def numeric(text, decimals = 0): 176 | if decimals == 0: 177 | return text if text.isdigit else None 178 | elif isinstance(decimals, int) and decimals > 0 and decimals < 10: 179 | if GWUserData.is_float(text): 180 | return str(round(float(text),decimals)) 181 | else: 182 | return None 183 | else: 184 | return None 185 | 186 | @staticmethod 187 | def public_seed(text): 188 | if not text.isalnum(): 189 | return None 190 | # BIP32 191 | elif text[:4] == "xpub" and len(text) <= 112 and len(text) >= 100: 192 | return text 193 | # electrum 194 | elif len(text) == 32 or len(text) == 64: 195 | return text 196 | else: 197 | return None 198 | 199 | @staticmethod 200 | def currency(text): 201 | text = text.lower() 202 | if text in ("usd", "dollar"): 203 | return "USD" 204 | elif text in ("gbp", "pound", "sterling"): 205 | return "GBP" 206 | elif text in ("eur", "euro"): 207 | return "EUR" 208 | elif text in ("btc", "xbt", "bitcoin", "bitcoins"): 209 | return "BTC" 210 | else: 211 | return "BTC" 212 | 213 | class GWAlias(object): 214 | def __init__(self, email = None, alias = None): 215 | self.aliases = [] 216 | self.target = None 217 | cur = BMMySQL().conn().cursor(MySQLdb.cursors.DictCursor) 218 | result = False 219 | if email: 220 | seen = {email: True} 221 | src = email 222 | while not result: 223 | cur.execute ("SELECT target FROM alias WHERE alias = %s", (src)) 224 | result = True 225 | for row in cur.fetchall(): 226 | result = False 227 | seen[row['target']] = True 228 | src = row['target'] 229 | for column in row: 230 | setattr(self, column, row[column]) 231 | elif alias: 232 | cur.execute ("SELECT alias FROM alias WHERE target = %s", (alias)) 233 | for row in cur.fetchall(): 234 | self.aliases.append(row['alias']) 235 | cur.close() 236 | 237 | def gettarget(self): 238 | return self.target 239 | 240 | class GWDomain(object): 241 | def __init__(self, domain = None): 242 | self.name = None 243 | self.active = None 244 | cur = BMMySQL().conn().cursor(MySQLdb.cursors.DictCursor) 245 | filterwarnings('ignore', category = MySQLdb.Warning) 246 | multirow = False 247 | if domain != None: 248 | cur.execute ("SELECT * FROM domain WHERE active = 1 AND name = %s", (domain)) 249 | else: 250 | multirow = True 251 | cur.execute ("SELECT * FROM domain WHERE active = 1") 252 | for row in cur.fetchall(): 253 | if multirow: 254 | print "%40s %1u" % (row['name'], row['active']) 255 | else: 256 | self.name = row['name'] 257 | self.active = row['active'] 258 | cur.close() 259 | 260 | def check(self): 261 | return self.name != None 262 | 263 | -------------------------------------------------------------------------------- /templates/accountexpired.txt: -------------------------------------------------------------------------------- 1 | Subject: Paid account expired, payment required 2 | 3 | It looks like your paid account expired. 4 | 5 | NOTE: THIS ONLY AFFECTS EMAIL GATEWAY SERVICES. SENDING MESSAGES TO BITMESSAGE 6 | USERS DOESN'T REQUIRE A SUBSCRIPTION. 7 | 8 | If you would like to continue as a paid account, please send 9 | 10 | ${btcamount} bitcoins 11 | to ${btcaddress} 12 | 13 | to extend the paid account for one month. If you would like to extend your subscription for a period of more than one month, multiply the amount by the number of months you would like your account to be extended (for example, 3*${btcamount} for a 3 month extension). 14 | 15 | If you would like to pay with a QR code, either view this message as HTML (right click on the message in the message list, and select "View HTML code as formatted text", or copy and paste the data between the quotes (beginning with data:img/png) into a browser. 16 | 17 |
18 | 19 | If you have a local Bitcoin URI handler setup, instead of using the QR code, you can use the following Bitcoin URI to pay: 20 | 21 | ${btcuri} 22 | 23 | If you do not pay, your account will continue to be serviced as a free account, without any of the advanced features available with paid acccounts. You will not be able to send emails, receive attachments or charge for incoming emails. 24 | 25 | ${timestamp} 26 | -------------------------------------------------------------------------------- /templates/command-invalid.txt: -------------------------------------------------------------------------------- 1 | Subject: Invalid command 2 | 3 | The command you specified, ${command}, is not valid. 4 | 5 | Valid commands are: 6 | status 7 | 8 | ${timestamp} 9 | -------------------------------------------------------------------------------- /templates/configchange.txt: -------------------------------------------------------------------------------- 1 | Subject: Configuration changed 2 | 3 | The configuration has been changed. In order to review the current settings, 4 | reply with the subject "status". 5 | 6 | ${timestamp} 7 | -------------------------------------------------------------------------------- /templates/confignochange.txt: -------------------------------------------------------------------------------- 1 | Subject: Configuration NOT changed 2 | 3 | The configuration has not been changed, it didn't contain any valid settings. 4 | 5 | Valid settings are: 6 | 7 | pgp: server or local 8 | attachments: yes or no 9 | flags: numeric 10 | archive: yes or no 11 | masterpubkey_btc: public key seed (electrum or BIP32) 12 | offset_btc: numeric (defaults to 0) 13 | feeamount: floating point fee amount 14 | feecurrency: USD, GPB, EUR or BTC 15 | 16 | ${timestamp} 17 | -------------------------------------------------------------------------------- /templates/deregistration-confirmed.txt: -------------------------------------------------------------------------------- 1 | Subject: We are sorry to see you leaving. 2 | 3 | We have unregistered your account ${email} as requested. In order to improve 4 | our service, we would like to know the reason why you are leaving and what 5 | would you like to change. Please send any feedback you have to 6 | ${bugreportaddress}. 7 | 8 | ${timestamp} 9 | -------------------------------------------------------------------------------- /templates/invoice.txt: -------------------------------------------------------------------------------- 1 | Subject: ${companyname} invoice / receipt 2 | 3 | Thank you for your payment. This message serves as an invoice and a receipt. 4 | 5 | ${companyname} 6 | ${companyaddress} 7 | 8 | ${timestamp} 9 | 10 | Service provided: 11 | ${service} 12 | 13 | Amount paid: 14 | ${btcamount} bitcoins 15 | 16 | Have a nice day. 17 | -------------------------------------------------------------------------------- /templates/registration-confirmed.txt: -------------------------------------------------------------------------------- 1 | Subject: Registration Request Accepted 2 | 3 | Thank you for your registration request for ${email}. Your account is now set up and ready to use! 4 | 5 | NOTE: THIS REGISTRATION IS ONLY FOR USING EMAIL GATEWAY SERVICES. BITMESSAGE 6 | ITSELF DOESN'T REQUIRE A GATEWAY. 7 | 8 | *** Sending Emails *** 9 | To send emails to the Internet, send a bitmessage to ${relayaddress} with the destination email address in the subject line. 10 | Kindly note that sending emails requires a paid subscription (1 USD per month), 11 | and upon sending the first message, you will receive a payment request. 12 | 13 | *** Unregistering Your Account *** 14 | To unregister, simply send a message from this address to ${deregisteraddress}. 15 | 16 | *** ${domain} Addresses to Trust (Add to Address Book) *** 17 | Registration Address: ${registeraddress} 18 | Deregistration Address: ${deregisteraddress} 19 | Relay Address: ${relayaddress} 20 | 21 | *** Reporting Bugs *** 22 | Please send any comments/bug reports to ${bugreportemail} or send us a bitmessage at ${bugreportaddress}. 23 | 24 | ${timestamp} 25 | -------------------------------------------------------------------------------- /templates/registration-duplicate.txt: -------------------------------------------------------------------------------- 1 | Subject: Registration Request Denied 2 | 3 | Your registration request for email address ${email} was denied because the username is reserved, already in use, or you have already registered an address! 4 | 5 | ${timestamp} 6 | -------------------------------------------------------------------------------- /templates/registration-invalid.txt: -------------------------------------------------------------------------------- 1 | Subject: Registration Request Denied 2 | 3 | Your registration request for email address ${email} was denied because the submitted username format is invalid. 4 | 5 | Please register an alpha-numeric only username with a length of 4-20 characters. We will add the ${domain} suffix if necessary. 6 | 7 | ${timestamp} 8 | -------------------------------------------------------------------------------- /templates/relay-missing-recipient.txt: -------------------------------------------------------------------------------- 1 | Subject: Relay missing recipient 2 | 3 | I would really love to relay your message, however the recipient appears to be missing. 4 | Please specify the recipient email address in the subject of the message. 5 | 6 | ${timestamp} 7 | -------------------------------------------------------------------------------- /templates/relay-throttle.txt: -------------------------------------------------------------------------------- 1 | Subject: Message throttling engaged 2 | 3 | You seem to be sending messages too quickly. As a precaution message throttling was engaged. 4 | Please wait ${throttledelta} more minutes. 5 | 6 | ${timestamp} 7 | -------------------------------------------------------------------------------- /templates/smtperror.txt: -------------------------------------------------------------------------------- 1 | Subject: Relay error 2 | 3 | Your email to ${emailrcpt} was not relayed. The SMTP server replied: 4 | 5 | Error code: ${errcode} 6 | Error message: ${errmessage} 7 | 8 | ${timestamp} 9 | -------------------------------------------------------------------------------- /templates/status.txt: -------------------------------------------------------------------------------- 1 | Subject: Account status report 2 | 3 | Bitmessage Address: ${recipient} 4 | Email address: ${email} 5 | Domain: ${domain} 6 | Active: ${active} 7 | Can send emails: ${cansend} 8 | Can charge for emails: ${cancharge} 9 | Can send invoices: ${caninvoice} 10 | PGP: ${pgp} 11 | Attachments: ${attachments} 12 | Expires: ${expires} 13 | Master public key bitcoin: ${masterpubkey_btc} 14 | Offset public key bitcoin: ${offset_btc} 15 | Incoming mail fee: ${feeamount} ${feecurrency} 16 | Archiving emails: ${archive} 17 | Flags: ${flags} 18 | Aliases: ${aliases} 19 | 20 | ${timestamp} 21 | -------------------------------------------------------------------------------- /walletnotify.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2.7 2 | 3 | import jsonrpclib 4 | import MySQLdb 5 | import sys 6 | import hashlib 7 | import datetime 8 | import decimal 9 | 10 | from lib.config import BMConfig 11 | from lib.mysql import BMMySQL 12 | from lib.bmlogging import BMLogging 13 | from lib.user import GWUser 14 | from lib.bmapi import BMAPI 15 | from lib.sendbm import SendBMTemplate 16 | 17 | from warnings import filterwarnings 18 | 19 | def parse_configfile(fname): 20 | ret = { 21 | 'rpcport': 8332, 22 | 'rpcuser': '', 23 | 'rpcpassword': '', 24 | 'rpcconnect': '127.0.0.1' 25 | } 26 | from os.path import expanduser 27 | 28 | fname = expanduser(fname) 29 | with open(fname) as f: 30 | for line in f.read().splitlines(): 31 | try: 32 | key, val = line.split("=") 33 | except: 34 | continue 35 | if not key: 36 | continue 37 | if key in ret: 38 | ret[key] = val 39 | return ret 40 | 41 | if len(sys.argv) < 3: 42 | print "Must specify coin (BTC or DRK) and txid" 43 | sys.exit() 44 | 45 | coin = sys.argv[1] 46 | txid = sys.argv[2] 47 | 48 | if coin == 'BTC': 49 | cfg = parse_configfile('~/.bitcoin/bitcoin.conf') 50 | elif coin == 'DRK': 51 | cfg = parse_configfile('~/.dash/dash.conf') 52 | else: 53 | print "Unknown coin " + coin 54 | sys.exit() 55 | 56 | client = jsonrpclib.Server('http://' + cfg['rpcuser'] + ":" + cfg['rpcpassword'] + "@" + cfg['rpcconnect'] + ":" + str(cfg['rpcport']) + "/") 57 | 58 | txinfo = client.gettransaction(txid, True) 59 | 60 | BMLogging() 61 | cur = BMMySQL().conn().cursor(MySQLdb.cursors.DictCursor) 62 | while BMAPI().conn() is False: 63 | print "Failure connecting to API, sleeping..." 64 | time.sleep(random.random()+0.5) 65 | 66 | filterwarnings('ignore', category = MySQLdb.Warning) 67 | 68 | #amount = txinfo['amount'] 69 | for detail in txinfo['details']: 70 | if detail['category'] == "receive": 71 | print detail['address'] + " -> " + str(detail['amount']) 72 | cur.execute ( """INSERT IGNORE INTO payment (address, coin, txid, amount, confirmations) VALUES (%s, %s, %s, %s, %s)""", 73 | (detail['address'], coin, txid, detail['amount'], txinfo['confirmations'])) 74 | if txinfo['confirmations'] == 0: 75 | # select from invoice 76 | cur.execute ("SELECT amount, paid, type, payer, sendercharge_id FROM invoice WHERE address = %s AND coin = %s", (detail['address'], coin)) 77 | for row in cur.fetchall(): 78 | invoice = row 79 | # fetch 80 | totalpaid = 0 81 | cur.execute ("SELECT amount FROM payment WHERE address = %s AND coin = %s AND confirmations = 0", (detail['address'], coin)) 82 | for row in cur.fetchall(): 83 | payment = row 84 | totalpaid += payment['amount'] # fixme floating rounding problems? 85 | # fetch 86 | if invoice['paid'] == None or invoice['paid'] <= datetime.datetime(1971, 1, 1) and totalpaid >= invoice['amount']: 87 | cur.execute ("UPDATE invoice SET paid = NOW() WHERE address = %s AND coin = %s ", (detail['address'], coin)) 88 | if invoice['type'] == 0: 89 | incomingamount = decimal.Decimal(str(detail['amount'])) 90 | if incomingamount < invoice['amount']: 91 | months = 0 92 | else: 93 | months = incomingamount / invoice['amount'] 94 | print "Extending for " + str(months) + " months" 95 | userdata = GWUser(bm = invoice['payer']) 96 | if not hasattr(userdata, "exp"): 97 | # user was deleted 98 | continue 99 | datefrom = userdata.exp if userdata.exp > datetime.date.today() else datetime.date.today() 100 | cur.execute ("UPDATE user SET cansend = 1, exp = IF(exp < CURDATE(),DATE_ADD(CURDATE(), INTERVAL " + 101 | str(months) + " MONTH),DATE_ADD(exp, INTERVAL " + str(months) + 102 | " MONTH)) WHERE bm = %s", invoice['payer']); 103 | userdata = GWUser(bm = invoice['payer']) 104 | dateuntil = userdata.exp 105 | SendBMTemplate( 106 | sender = BMAPI().get_address(BMConfig().get("bmgateway", "bmgateway", "registration_address_label")), 107 | recipient = invoice['payer'], 108 | template = "invoice", 109 | addmaps = { 110 | 'btcamount': str(incomingamount), 111 | 'service': 'Subscription for ' + userdata.email + ' from ' + datefrom.strftime("%B %-d %Y") + 112 | ' until ' + dateuntil.strftime("%B %-d %Y"), 113 | 'email': userdata.email 114 | }) 115 | 116 | elif invoice['type'] == 1: 117 | # find in combo table and allow 118 | cur.execute ("UPDATE sendercharge SET status = 1 WHERE id = %s", (invoice['sendercharge_id'])) 119 | elif invoice['type'] == 2: 120 | # notify user that payment is incoming 121 | pass 122 | pass 123 | cur.close() 124 | --------------------------------------------------------------------------------