├── .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 |
--------------------------------------------------------------------------------