endpoint: /incoming/view
"
285 | "api_key: %s
"
286 | "access_key: %s
"
287 | "ip: %s
") % (v('api_key'), v('access_key'),
288 | ip)
289 |
290 | email_admin(msg, "Security alert!")
291 |
292 | return ("
Apologies for the glitch! Our staff" 293 | " has been notified in order to better assist you.
"), 401 294 | 295 | @incoming.route('/delete', methods=['POST', 'OPTIONS']) 296 | def delete(): 297 | """Delete an incoming fax""" 298 | 299 | from library.mailer import email_admin 300 | from boto.s3.connection import S3Connection 301 | from boto.s3.key import Key 302 | 303 | v = request.values.get 304 | 305 | access_key = v('access_key') 306 | account_id = Account.authorize(v('api_key')) 307 | 308 | if not account_id: 309 | return jsonify(api_error('API_UNAUTHORIZED')), 401 310 | 311 | faxes = IncomingFax.query.filter_by(access_key = access_key) 312 | fax = faxes.first() 313 | 314 | db.session.delete(fax) 315 | db.session.commit() 316 | 317 | try: 318 | conn = S3Connection(os.environ.get('AWS_ACCESS_KEY'), 319 | os.environ.get('AWS_SECRET_KEY')) 320 | bucket = conn.get_bucket(os.environ.get('AWS_S3_BUCKET')) 321 | k = Key(bucket) 322 | k.key ='incoming/' + access_key + '/fax.pdf' 323 | k.delete() 324 | except: 325 | email_admin("AWS S3 connect fail for fax deletion: %s" % access_key) 326 | 327 | return jsonify({"success": True}) 328 | 329 | def auto_recharge(account, ip, session = None): 330 | 331 | if not session: 332 | session = db.session 333 | 334 | try: 335 | stripe.api_key = os.environ.get('STRIPE_SECRET_KEY') 336 | 337 | charge = stripe.Charge.create( 338 | amount=1000, 339 | currency="usd", 340 | customer=account.stripe_token, 341 | description="FaxRobot.com: recurring service account #%s"%account.id 342 | ) 343 | data = { 344 | 'account_id': account.id, 345 | 'amount': 10, 346 | 'source': 'stripe', 347 | 'source_id': charge["id"], 348 | 'ip_address': ip, 349 | 'initial_balance': account.credit, 350 | 'trans_type': 'auto_recharge' 351 | } 352 | trans = Transaction(**data) 353 | session.add(trans) 354 | session.commit() 355 | 356 | account.add_credit(10, session) 357 | 358 | return True 359 | 360 | except: 361 | payment = {'_DEBUG': traceback.format_exc()} 362 | data = { 363 | 'amount': 10, 364 | 'account_id': account.id, 365 | 'source': 'stripe', 366 | 'debug': json.dumps(payment), 367 | 'ip_address': ip, 368 | 'payment_type': 'auto_recharge' 369 | } 370 | failed_payment = FailedPayment(**data) 371 | session.add(failed_payment) 372 | session.commit() 373 | return False 374 | 375 | def delete_number_from_phaxio(fax_number): 376 | payload = { 377 | "number": fax_number, 378 | "api_key": os.environ.get('PHAXIO_API_KEY'), 379 | "api_secret": os.environ.get('PHAXIO_API_SECRET') 380 | } 381 | 382 | try: 383 | url = "https://api.phaxio.com/v1/releaseNumber" 384 | r = requests.post(url, data=payload) 385 | response = json.loads(r.text) 386 | 387 | if response["success"] == False: 388 | return False 389 | except: 390 | return False 391 | 392 | return True -------------------------------------------------------------------------------- /controllers/jobs.py: -------------------------------------------------------------------------------- 1 | import os 2 | from flask import Blueprint, request, render_template, jsonify, current_app 3 | from models import db 4 | from library.grab_bag import fix_ip, o 5 | from library.errors import api_error, ValidationError 6 | from library.phaxio import valid_signature 7 | from models.job import Job 8 | from models.account import Account 9 | from rq import Queue 10 | from redis import Redis 11 | from workers.jobs import initial_process, send_fax, fail, mark_job_sent 12 | from datetime import datetime 13 | 14 | 15 | jobs = Blueprint('jobs', __name__, url_prefix='/jobs') 16 | 17 | @jobs.route('/create', methods=['GET', 'POST']) 18 | def create(): 19 | """Creates a new outgoing fax""" 20 | 21 | account_id = Account.authorize(request.values.get('api_key')) 22 | if account_id == None: 23 | return jsonify(api_error('API_UNAUTHORIZED')), 401 24 | 25 | ip = fix_ip(request.headers.get('x-forwarded-for', request.remote_addr)) 26 | 27 | if request.method == 'POST': 28 | 29 | if request.files and 'file' in request.files: 30 | uploaded_file = request.files['file'] 31 | else: 32 | uploaded_file = None 33 | 34 | v = request.values.get 35 | 36 | if uploaded_file or v('body'): 37 | 38 | data = { 39 | 'account_id': account_id, 40 | 'ip_address': ip, 41 | 'destination': v('destination'), 42 | 'send_authorized': v('send_authorized', 0), 43 | 'cover': v('cover', 0), 44 | 'cover_name': v('cover_name'), 45 | 'cover_address': v('cover_address'), 46 | 'cover_city': v('cover_city'), 47 | 'cover_state': v('cover_state'), 48 | 'cover_zip': v('cover_zip'), 49 | 'cover_country': v('cover_country'), 50 | 'cover_phone': v('cover_phone'), 51 | 'cover_email': v('cover_email'), 52 | 'cover_company': v('cover_company'), 53 | 'cover_to_name': v('cover_to_name'), 54 | 'cover_cc': v('cover_cc'), 55 | 'cover_subject': v('cover_subject'), 56 | 'cover_status': v('cover_status','review'), 57 | 'cover_comments': v('cover_comments'), 58 | 'callback_url': v('callback_url') 59 | } 60 | if uploaded_file: 61 | data['filename'] = uploaded_file.filename 62 | else: 63 | data['body'] = v('body') 64 | 65 | o(data) 66 | 67 | try: 68 | job = Job(**data); 69 | job.validate() 70 | job.determine_international() 71 | except ValidationError, err: 72 | return jsonify(api_error(err.ref)), 400 73 | 74 | db.session.add(job) 75 | db.session.commit() 76 | 77 | if uploaded_file: 78 | binary = uploaded_file.stream.read() 79 | else: 80 | binary = job.body.replace("\r\n", "\n").encode('utf-8') 81 | 82 | redis_conn = Redis.from_url(current_app.config['REDIS_URI']) 83 | q = Queue('high', connection=redis_conn) 84 | q.enqueue_call(func=initial_process, args=(job.id, binary), 85 | timeout=300) 86 | 87 | return jsonify(job.public_data()) 88 | else: 89 | return jsonify(api_error("JOBS_NO_ATTACHMENT")), 400 90 | 91 | else: 92 | return render_template('jobs_create.html') 93 | 94 | 95 | @jobs.route('/update/Thanks for registering for %s! Your new account is ' 17 | 'ready. To log in, visit %s' 18 | '/accounts/login.
I\'ll send you emails with ' 19 | 'important account-related updates, like a notification when ' 20 | 'your faxes are sent (or they fail to send for some reason). ' 21 | 'I respect your email address and won\'t send you junk email. ' 22 | 'You can change your email notification preferences by ' 23 | 'visiting %s/account.
I hope ' 24 | 'you have as much fun using the service as I do ' 25 | 'sending faxes on your behalf.
Thanks again,
%s' 26 | '
') % (project, base_url, base_url, base_url, base_url, 27 | project) 28 | } 29 | 30 | send_email(message, account) 31 | 32 | def email_password_change(account): 33 | message = { 34 | 'subject': '%s password changed' % project, 35 | 'html': ('Just a heads up: your %s password was changed.
' 36 | 'If you weren\'t aware of this, please email ' 37 | '%s ' 38 | 'as soon as possible.
--%s
') % (project, 39 | email_support, email_support, project) 40 | } 41 | send_email(message, account) 42 | 43 | def email_pending_deletion_warning(account, reason, fax_number): 44 | 45 | msg = 'Hello!
' 46 | 47 | if reason == "NO_FUNDS": 48 | msg += ("Your %s account does not have sufficient funds for this " 49 | "month's incoming fax service, and automatic payments are " 50 | "disabled for your account.
") % project 51 | else: 52 | msg += ("Your credit card was declined and your %s account has " 53 | "insufficient remaining funds for this month's incoming " 54 | "fax service.
") % project 55 | 56 | msg += ("Please update your payment information in your " 57 | "Account Settings as " 58 | "soon as possible. If we are still unable to charge your account " 59 | "after 7 days, your fax number will automatically be suspended." 60 | "
Need assistance? Please reply to this email or contact " 61 | "%s.
Thanks!
--%s
") % ( 62 | base_url, email_support, email_support, project) 63 | 64 | message = { 65 | 'subject': 'Payment overdue for your incoming fax number', 66 | 'html': msg 67 | } 68 | send_email(message, account) 69 | 70 | def email_deleted_number(account): 71 | message = { 72 | 'subject': 'Your fax number was suspended :(', 73 | 'html': ('Your monthly payment for incoming fax service could ' 74 | 'not be processed, and so the service was automatically ' 75 | 'suspended. For more information, please contact ' 76 | '%s ' 77 | 'as soon as possible.
--%s
') % ( 78 | email_support, email_support, project) 79 | } 80 | send_email(message, account) 81 | 82 | def email_api_key_change(account): 83 | message = { 84 | 'subject': '%s API key changed' % project, 85 | 'html': ('Just a heads up: your %s API key was changed.
' 86 | 'If you weren\'t aware of this, please email ' 87 | '%s ' 88 | 'as soon as possible.
--%s
') % (project, 89 | email_support, email_support, project) 90 | } 91 | send_email(message, account) 92 | 93 | def email_provision_info(account, fax_number): 94 | message = { 95 | 'subject': 'Your fax number is activated!', 96 | 'html': ('Your %s account will be charged $6 per month to ' 98 | 'keep your number active. You can manage your subscription by ' 99 | 'visiting your Account Settings' 100 | ' page.
Happy faxing!
--%s
') % ( 101 | fax_number, project, base_url, project) 102 | } 103 | send_email(message, account) 104 | 105 | def email_payment(account, charged, transaction_id, payment_last4, 106 | temporary_password = None): 107 | 108 | from datetime import datetime 109 | 110 | if temporary_password: 111 | html = ('Your temporary password is: %s' 112 | '
Thanks for your business! Your purchase of %s ' 113 | 'Credit allows you to send faxes any ' 114 | 'time, without worrying about monthly subscription fees. We ' 115 | 'hope you enjoy using the service. If you have any questions ' 116 | 'or feedback, please email ' 117 | '%s.
' 118 | 'You can log into your account at ' 119 | '%s/accounts/login.') % ( 120 | temporary_password, project, email_feedback, email_feedback, 121 | base_url, base_url) 122 | subject = 'Welcome to %s! Here\'s your receipt.' % project 123 | else: 124 | html = '
Account: %s
'
129 | 'Transaction ID: %s
'
130 | 'Payment Date: %s
'
131 | 'Payment Card: %s
'
132 | 'Amount: $%.2f
This is your receipt of payment against your credit card. ' 134 | 'Thanks!
For account information ' 135 | 'and current balance, please visit ' 136 | '%s/account.
' 137 | '--%s
') % (account.email, transaction_id, 138 | datetime.now().strftime('%B %d, %Y'), payment_last4, charged, 139 | base_url, base_url, project) 140 | 141 | message = { 142 | 'subject': subject, 143 | 'html': html 144 | } 145 | send_email(message, account) 146 | 147 | def email_receive_fail(account,reason,external_id,from_number,num_pages,cost): 148 | 149 | html = ("%s was unable to deliver an incoming fax from %s" 150 | " due to insufficient funds in your account.
") % ( 151 | project, from_number) 152 | 153 | if reason == "NO_FUNDS": 154 | html += ("Please visit your Account " 155 | "Settings page to add funds, and consider turning" 156 | " on the automatic charging feature. ") % base_url 157 | else: 158 | html += ("
Your credit card was declined. Please visit " 159 | "your Account Settings" 160 | " page to update your credit card info.") % base_url 161 | 162 | html += ("
Once your account is funded, please reply to this email " 163 | "and we can deliver your fax.
") 164 | 165 | html += ("Reference #: %s
"
167 | "From: %s
"
168 | "Pages: %s
"
169 | "Cost: $%.2f
Your fax is attached. " 182 | "You can also download or delete it from " 183 | "%s.
") % ( 184 | from_number, base_url, project) 185 | 186 | html += ("Reference #: %s
"
188 | "From: %s
"
189 | "Pages: %s
"
190 | "Cost: $%.2f
" 191 | "Remaining Account Balance: $%.2f
") % ( 192 | external_id, from_number, num_pages, cost, account.credit) 193 | 194 | message = { 195 | "subject": "Received fax from %s" % from_number, 196 | "html": html 197 | } 198 | send_email(message, account, "%s.pdf" % external_id, filename, 199 | "application/pdf") 200 | 201 | def email_recharge_payment(account, charged, transaction_id, payment_last4): 202 | 203 | from datetime import datetime 204 | 205 | message = { 206 | 'subject': '%s payment receipt' % project, 207 | 'html': ('Your ' 208 | 'remaining account credit dropped to $0, so we recharged it ' 209 | 'by $%.2f. You can manage your stored payment information and ' 210 | 'auto-recharge preferences at ' 211 | '%s/account.
Account: %s
'
213 | 'Transaction ID: %s
'
214 | 'Payment Date: %s
'
215 | 'Payment Card: %s
'
216 | 'Amount: $%.2f
This is your receipt of payment against your credit card. ' 218 | 'Thanks!
For account information ' 219 | 'and current balance, please visit ' 220 | '%s/account.
' 221 | '--%s
') % (project, charged, base_url, base_url, 222 | account.email, transaction_id, 223 | datetime.now().strftime('%B %d, %Y'), payment_last4, charged, 224 | base_url, base_url, project) 225 | } 226 | send_email(message, account) 227 | 228 | def email_fail(job, msg, code, status, debug_data=None): 229 | 230 | from datetime import datetime 231 | 232 | account = job.account 233 | 234 | body = ('Visit the fax details page to ' 235 | 'retry.
An error occurred while sending ' 236 | 'your fax:
' 237 | 'Job ID: %s
'
238 | 'Destination: %s
'
239 | 'Error: %s (Code %s)
'
240 | 'Date: %s
'
241 | ) % (base_url, job.access_key, job.id, job.destination,
242 | 'Internal error' if status == 'internal_error' else msg,
243 | code, datetime.now().strftime('%B %d, %Y'))
244 |
245 | if job.account.debug_mode and debug_data:
246 |
247 | if debug_data["device"]:
248 | body += 'Device: %s
' % debug_data["device"]
249 |
250 | if debug_data["output"]:
251 | output = "
".join(debug_data["output"].split("\n"))
252 | body += 'Output: %s
' % output
253 |
254 | body +=('
If you\'re having trouble, please email ' 255 | '%s ' 256 | 'and we\'ll do our best to help.
To change your email ' 257 | 'notification preferences, visit ' 258 | '%s/account.
--%s
') % ( 259 | email_support, email_support, base_url, base_url, project) 260 | 261 | message = { 262 | 'subject': 'Your fax could not be sent :(', 263 | 'html': body 264 | } 265 | send_email(message, account) 266 | 267 | def email_success(job): 268 | 269 | from datetime import datetime 270 | 271 | account = job.account 272 | 273 | message = { 274 | 'subject': 'Your fax was sent!', 275 | 'html': ('Great news! Your fax to %s was sent.
' 276 | 'Visit the fax details page ' 277 | 'for more info.
To change your email ' 278 | 'notification preferences, visit ' 279 | '%s/account.
--%s
' 280 | ) % (job.destination, base_url, job.access_key, base_url, 281 | base_url, project) 282 | } 283 | send_email(message, account) 284 | 285 | def email_password_reset(email, password_reset, account): 286 | 287 | message = { 288 | 'subject': 'Reset your %s password' % project, 289 | 'html': ('To reset your %s password, click on the link ' 290 | 'below:
If ' 291 | 'you weren\'t expecting this email, or weren\'t trying to ' 292 | 'reset your password, please contact ' 293 | '%s' 294 | '
--%s
' 295 | ) % (project, base_url, password_reset.reset_hash, base_url, 296 | password_reset.reset_hash, email_support, email_support, 297 | project) 298 | } 299 | send_email(message, account) 300 | 301 | def send_email(message, account = None, attach_name=None, attach_file=None, 302 | attach_mime=None): 303 | 304 | if not os.environ.get('SPARKPOST_API_KEY'): 305 | return False 306 | 307 | import requests 308 | import base64 309 | 310 | url = 'https://api.sparkpost.com/api/v1/transmissions' 311 | 312 | payload = {} 313 | 314 | payload['content'] = message 315 | payload['content']['from'] = { 316 | "name": os.environ.get('EMAIL_FROM_NAME'), 317 | "email": os.environ.get('EMAIL_FROM') 318 | } 319 | 320 | if account: 321 | if account.first_name and account.last_name: 322 | payload['recipients'] = [{ 323 | 'address': { 324 | 'email': account.email, 325 | 'name': account.first_name + ' ' + account.last_name 326 | } 327 | }] 328 | else: 329 | payload['recipients'] = [{'address': account.email}] 330 | 331 | if attach_name and attach_file and attach_mime: 332 | 333 | with open(attach_file) as f: 334 | attach_data = base64.b64encode(f.read()) 335 | 336 | payload['content']['attachments'] = [ 337 | { 338 | "type": attach_mime, 339 | "name": attach_name, 340 | "data": attach_data 341 | } 342 | ] 343 | 344 | try: 345 | response = requests.post(url, headers=email_headers, json=payload) 346 | except: 347 | o('SparkPost API Fail: %s' % response.text) 348 | 349 | def email_admin(message, subject = None): 350 | 351 | if not os.environ.get('SPARKPOST_API_KEY'): 352 | return False 353 | 354 | import requests 355 | 356 | url = 'https://api.sparkpost.com/api/v1/transmissions' 357 | 358 | payload = { 359 | "content": { 360 | "subject": subject if subject else "%s CRITICAL ERROR" % project, 361 | "html": message, 362 | "from": { 363 | "name": os.environ.get('EMAIL_FROM_NAME'), 364 | "email": os.environ.get('EMAIL_FROM') 365 | } 366 | }, 367 | "recipients": [{'address': os.environ.get('EMAIL_FROM')}] 368 | } 369 | 370 | try: 371 | response = requests.post(url, headers=email_headers, json=payload) 372 | except: 373 | o('SparkPost API Fail: %s' % response.text) 374 | 375 | 376 | 377 | 378 | -------------------------------------------------------------------------------- /library/phaxio.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import hmac 3 | 4 | def valid_signature(token, url, parameters, files, signature): 5 | # sort the post fields and add them to the URL 6 | for key in sorted(parameters.keys()): 7 | url += '{}{}'.format(key, parameters[key]) 8 | 9 | # sort the files and add their SHA1 sums to the URL 10 | for filename in sorted(files.keys()): 11 | file_hash = hashlib.sha1() 12 | file_hash.update(files[filename].read()) 13 | files[filename].stream.seek(0) 14 | url += '{}{}'.format(filename, file_hash.hexdigest()) 15 | 16 | return signature == hmac.new(key=token.encode('utf-8'), msg=url.encode('utf-8'), digestmod=hashlib.sha1).hexdigest() 17 | 18 | -------------------------------------------------------------------------------- /log/README.md: -------------------------------------------------------------------------------- 1 | This page intentionally left blank 2 | -------------------------------------------------------------------------------- /media/cover_sheets/default.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyonbros/faxrobot/4dfdd6c9bd4cd335a7dc4aaaa6188ad808b78d71/media/cover_sheets/default.pdf -------------------------------------------------------------------------------- /models/__init__.py: -------------------------------------------------------------------------------- 1 | from flask_sqlalchemy import SQLAlchemy 2 | db = SQLAlchemy() 3 | 4 | def initialize_database(app): 5 | from models.account import Account 6 | from models.job import Job 7 | from models.transaction import Transaction 8 | from models.failed_payment import FailedPayment 9 | from models.password_reset import PasswordReset 10 | from models.incoming_number import IncomingNumber 11 | from models.incoming_fax import IncomingFax 12 | import psycopg2 13 | import os 14 | 15 | with app.app_context(): 16 | try: 17 | db.create_all() 18 | account = Account.query.get(0) 19 | if not account: 20 | # insert guest account if needed. 21 | conn = psycopg2.connect(os.environ.get('DATABASE_URI')) 22 | query = ("INSERT INTO account (id, email, password, api_key) " 23 | "VALUES (%s, %s, %s, %s);") 24 | data = (0, 'anon@faxrobot.internal', 'trol', 'lol') 25 | cursor = conn.cursor() 26 | cursor.execute(query, data) 27 | conn.commit() 28 | conn.close() 29 | except: 30 | print("ERROR: COULD NOT AUTO-GENERATE DATABASE TABLES") 31 | print("Make sure your SQLALCHEMY_DATABASE_URI environment is set!") -------------------------------------------------------------------------------- /models/account.py: -------------------------------------------------------------------------------- 1 | from models import db 2 | from datetime import datetime 3 | from sqlalchemy import func, Column, BigInteger, SmallInteger, Integer, \ 4 | String, DateTime, ForeignKey, Text, Float 5 | from sqlalchemy.exc import SQLAlchemyError 6 | from validate_email import validate_email 7 | from library.grab_bag import random_hash, password_hash 8 | from library.errors import ValidationError 9 | import os 10 | 11 | default_cost_per_page = float(os.environ.get('DEFAULT_COST_PER_PAGE', '0.10')) 12 | 13 | class Account(db.Model): 14 | __tablename__ = 'account' 15 | 16 | id = Column(BigInteger, primary_key=True) 17 | email = Column(String(255), nullable=False,unique=True,index=True) 18 | password = Column(String(64), nullable=False) 19 | api_key = Column(String(64), nullable=False,unique=True,index=True) 20 | credit = Column(Float, default=0.00) 21 | base_rate = Column(Float, default=default_cost_per_page) 22 | allow_overflow = Column(SmallInteger, default=0) 23 | first_name = Column(String(255)) 24 | last_name = Column(String(255)) 25 | address = Column(String(255)) 26 | address2 = Column(String(255)) 27 | city = Column(String(255)) 28 | state = Column(String(64)) 29 | zip = Column(String(24)) 30 | stripe_token = Column(String(255)) 31 | stripe_card = Column(String(255)) 32 | last4 = Column(Integer) 33 | auto_recharge = Column(SmallInteger, default=0) 34 | email_success = Column(SmallInteger, default=1) 35 | email_fail = Column(SmallInteger, default=1) 36 | email_list = Column(SmallInteger, default=1) 37 | debug_mode = Column(SmallInteger, default=0) 38 | low_priority = Column(SmallInteger, default=0) 39 | create_date = Column(DateTime) 40 | mod_date = Column(DateTime) 41 | 42 | def __init__(self, email, password, first_name=None, last_name=None, 43 | address=None, address2=None, city=None, state=None, zip=None): 44 | 45 | self.create_date = datetime.now() 46 | 47 | if not email: 48 | raise ValidationError('ACCOUNTS_MISSING_EMAIL') 49 | elif not validate_email(email): 50 | raise ValidationError('ACCOUNTS_BAD_EMAIL') 51 | else: 52 | self.email = email 53 | 54 | if not password: 55 | raise ValidationError('ACCOUNTS_MISSING_PASSWORD') 56 | else: 57 | self.password = password_hash(password) 58 | 59 | self.api_key = random_hash("%s%s" % (email, self.password)) 60 | self.first_name = first_name 61 | self.last_name = last_name 62 | self.address = address 63 | self.address2 = address2 64 | self.city = city 65 | self.state = state 66 | self.zip = zip 67 | 68 | def validate(self): 69 | 70 | if not self.email: 71 | raise ValidationError('ACCOUNTS_MISSING_EMAIL') 72 | 73 | if not validate_email(self.email): 74 | raise ValidationError('ACCOUNTS_BAD_EMAIL') 75 | 76 | if self.first_name and len(self.first_name) > 255: 77 | raise ValidationError('ACCOUNTS_BAD_FIRST_NAME') 78 | 79 | if self.last_name and len(self.last_name) > 255: 80 | raise ValidationError('ACCOUNTS_BAD_LAST_NAME') 81 | 82 | if self.address and len(self.address) > 255: 83 | raise ValidationError('ACCOUNTS_BAD_ADDRESS') 84 | 85 | if self.address2 and len(self.address2) > 255: 86 | raise ValidationError('ACCOUNTS_BAD_ADDRESS2') 87 | 88 | if self.city and len(self.city) > 255: 89 | raise ValidationError('ACCOUNTS_BAD_CITY') 90 | 91 | if self.state and len(self.state) > 64: 92 | raise ValidationError('ACCOUNTS_BAD_STATE') 93 | 94 | if self.zip and len(self.zip) > 24: 95 | raise ValidationError('ACCOUNTS_BAD_ZIP') 96 | 97 | if self.email_success and self.email_success != 0 and \ 98 | self.email_success != 1: 99 | raise ValidationError('ACCOUNTS_BAD_REQUEST') 100 | 101 | if self.email_fail and self.email_fail != 0 and self.email_fail != 1: 102 | raise ValidationError('ACCOUNTS_BAD_REQUEST') 103 | 104 | if self.email_list and self.email_list != 0 and self.email_list != 1: 105 | raise ValidationError('ACCOUNTS_BAD_REQUEST') 106 | 107 | return True 108 | 109 | def add_credit(self, amount, session=None): 110 | 111 | if not session: 112 | session = db.session 113 | 114 | session.query(Account).filter_by(id=self.id).update( 115 | {Account.credit: Account.credit + amount} 116 | ) 117 | session.commit() 118 | 119 | def subtract_credit(self, amount, session=None): 120 | 121 | if not session: 122 | session = db.session 123 | 124 | session.query(Account).filter_by(id=self.id).update( 125 | {Account.credit: Account.credit - amount} 126 | ) 127 | session.commit() 128 | 129 | def get_priority(self): 130 | 131 | return "low" if self.low_priority else "default" 132 | 133 | # why have two nearly identical methods doing nearly exactly the same thing? 134 | # BECAUSE FUCK YOU. that's why. 135 | 136 | def public_data(self): 137 | """ 138 | Returns a dictionary of fields that are safe to display to the client. 139 | """ 140 | 141 | data = { 142 | 'api_key': self.api_key, 143 | 'email': self.email, 144 | 'first_name': self.first_name, 145 | 'last_name': self.last_name, 146 | 'address': self.address, 147 | 'address2': self.address2, 148 | 'city': self.city, 149 | 'state': self.state, 150 | 'zip': self.zip, 151 | 'credit': self.credit, 152 | 'auto_recharge': self.auto_recharge, 153 | 'has_stripe_token': True if self.stripe_token else False, 154 | 'last4': self.last4, 155 | 'email_success': self.email_success, 156 | 'email_fail': self.email_fail, 157 | 'email_list': self.email_list, 158 | }; 159 | 160 | if len(self.incoming_numbers) > 0: 161 | data["incoming_number"] = self.incoming_numbers[0].fax_number 162 | 163 | return data; 164 | 165 | @classmethod 166 | def authorize(cls_obj, api_key, required = False): 167 | """ 168 | Checks that any given api_key is valid 169 | """ 170 | if required == False and not api_key: 171 | return 0 # guest account ID is 0 172 | 173 | accounts = cls_obj.query.filter_by(api_key = api_key) 174 | return None if accounts.first() == None else accounts.first().id 175 | 176 | def __repr__(self): 177 | return "This is really just for testing. We shouldn't ever actually use this.
12 | 27 | 28 | -------------------------------------------------------------------------------- /templates/accounts_create.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |This is really just for testing. We shouldn't ever actually use this.
12 | 42 | 43 | -------------------------------------------------------------------------------- /templates/accounts_email_password_reset.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |This is really just for testing. We shouldn't ever actually use this.
12 | 18 | 19 | -------------------------------------------------------------------------------- /templates/accounts_login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |This is really just for testing. We shouldn't ever actually use this.
12 | 21 | 22 | -------------------------------------------------------------------------------- /templates/jobs_create.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |This is really just for testing. We shouldn't ever actually use this.
12 | 118 | 119 | -------------------------------------------------------------------------------- /templates/jobs_restart.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |This is really just for testing. We shouldn't ever actually use this.
12 | 118 | 119 | -------------------------------------------------------------------------------- /worker.py: -------------------------------------------------------------------------------- 1 | import os 2 | import redis 3 | from rq import Worker, Queue, Connection 4 | import sys 5 | import argparse 6 | 7 | parser = argparse.ArgumentParser(description='Run the Fax Robot Worker.') 8 | parser.add_argument('--listen', nargs=1, help='Listen queue: high|default|low') 9 | parser.add_argument('--device', nargs=1, help='Modem device to use for faxing') 10 | parser.add_argument('--callerid', nargs=1, help='Caller ID string for faxes') 11 | 12 | args = parser.parse_args() 13 | 14 | if args.listen: 15 | listen = args.listen[0].split(',') 16 | else: 17 | listen = ['high', 'default', 'low'] 18 | 19 | MODEM_DEVICE = args.device[0] if args.device else '/dev/ttyUSB0' 20 | CALLER_ID = args.callerid[0] if args.callerid else '' 21 | 22 | print >> sys.stderr, "Binding to modem device: " + MODEM_DEVICE 23 | 24 | conn = redis.from_url(os.environ.get('REDIS_URI')) 25 | 26 | if __name__ == '__main__': 27 | with Connection(conn): 28 | Worker.MODEM_DEVICE = MODEM_DEVICE 29 | Worker.CALLER_ID = CALLER_ID 30 | worker = Worker(map(Queue, listen)) 31 | worker.work() 32 | -------------------------------------------------------------------------------- /workers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyonbros/faxrobot/4dfdd6c9bd4cd335a7dc4aaaa6188ad808b78d71/workers/__init__.py -------------------------------------------------------------------------------- /workers/cron.py: -------------------------------------------------------------------------------- 1 | from controllers.incoming import auto_recharge, delete_number_from_phaxio 2 | from models.account import Account 3 | from models.incoming_number import IncomingNumber 4 | from models.transaction import Transaction 5 | from library.mailer import email_admin, email_deleted_number, \ 6 | email_pending_deletion_warning 7 | import psycopg2 8 | import os 9 | from sqlalchemy.orm import sessionmaker 10 | from sqlalchemy import create_engine 11 | from datetime import datetime 12 | from library.grab_bag import o 13 | 14 | engine = create_engine(os.environ.get('DATABASE_URI').strip()) 15 | Session = sessionmaker(bind=engine) 16 | session = Session() 17 | 18 | def charge_subscribers(): 19 | 20 | o("QUERYING INCOMING FAX NUMBERS FOR PAYMENT PROCESSING!") 21 | o(" ") 22 | 23 | conn = psycopg2.connect(os.environ.get('DATABASE_URI')) 24 | query = ( 25 | "SELECT id, account_id, fax_number, flagged_for_deletion, " 26 | " last_billed, create_date, mod_date " 27 | "FROM incoming_number " 28 | "WHERE ( " 29 | " mod_date IS NULL " 30 | " OR " 31 | " mod_date < now() - \'7 days\'::interval " 32 | " ) " 33 | "AND ( " 34 | " last_billed IS NULL " 35 | " OR " 36 | " last_billed < now() - \'30 days\'::interval " 37 | " ) " 38 | ) 39 | 40 | cursor = conn.cursor() 41 | cursor.execute(query) 42 | 43 | for row in cursor: 44 | id = row[0] 45 | account_id = row[1] 46 | fax_number = row[2] 47 | flagged_for_deletion = row[3] 48 | last_billed = row[4] 49 | 50 | account = session.query(Account).get(account_id) 51 | incoming_number = session.query(IncomingNumber).get(id) 52 | 53 | account_status = "SUCCESS" 54 | 55 | o("Fax number %s for account %s" % (fax_number, account_id)) 56 | 57 | if account.credit < 6 and not account.allow_overflow: 58 | 59 | o(" - account credit below threshold") 60 | 61 | if account.stripe_card and account.auto_recharge: 62 | if not auto_recharge(account, "localhost", session) == True: 63 | account_status = "DECLINE" 64 | o(" - CARD DECLINED :(") 65 | else: 66 | o(" - payment succeeded :)") 67 | else: 68 | account_status = "NO_FUNDS" 69 | o(" - AUTO-CHARGED DISABLED AND NO FUNDS IN ACCOUNT :(") 70 | 71 | if not account_status == "SUCCESS" and flagged_for_deletion: 72 | 73 | o(" - number is already marked for deletion >_<") 74 | 75 | session.delete(incoming_number) 76 | session.commit() 77 | 78 | if not delete_number_from_phaxio(fax_number): 79 | email_admin("Failed to delete Phaxio number: %s" % fax_number) 80 | 81 | email_deleted_number(account) 82 | 83 | o(" - deleted number and emailed customer.") 84 | 85 | elif not account_status == "SUCCESS" and not flagged_for_deletion: 86 | 87 | incoming_number.mod_date = datetime.now() 88 | incoming_number.flagged_for_deletion = 1 89 | session.commit() 90 | 91 | email_pending_deletion_warning(account, account_status, fax_number) 92 | 93 | o(" - flagged number for deletion, emailed warning to customer.") 94 | 95 | else: 96 | 97 | o(" - account successfully charged. hooray!") 98 | 99 | incoming_number.mod_date = datetime.now() 100 | incoming_number.last_billed = datetime.now() 101 | incoming_number.flagged_for_deletion = 0 102 | session.commit() 103 | 104 | data = { 105 | 'account_id': account.id, 106 | 'amount': -6.00, 107 | 'source': "phaxio", 108 | 'source_id': fax_number, 109 | 'ip_address': "localhost", 110 | 'initial_balance': account.credit, 111 | 'trans_type': "incoming_autopay" 112 | } 113 | trans = Transaction(**data) 114 | session.add(trans) 115 | session.commit() 116 | 117 | account.subtract_credit(6, session) 118 | 119 | o(" ") 120 | 121 | conn.commit() 122 | conn.close() 123 | 124 | email_admin("YAY!", "Payments cron job success!") 125 | 126 | o("ALL DONE!") 127 | return "lulz" 128 | -------------------------------------------------------------------------------- /workers/jobs.py: -------------------------------------------------------------------------------- 1 | import os 2 | from time import sleep 3 | from models import db 4 | from datetime import datetime 5 | from sqlalchemy import create_engine 6 | from sqlalchemy.orm import sessionmaker 7 | from library.errors import worker_error 8 | from library.grab_bag import o 9 | from models.job import Job 10 | from models.transaction import Transaction 11 | from models.failed_payment import FailedPayment 12 | from models.incoming_number import IncomingNumber 13 | 14 | engine = create_engine(os.environ.get('DATABASE_URI').strip()) 15 | Session = sessionmaker(bind=engine) 16 | session = Session() 17 | 18 | def initial_process(id, data = None): 19 | 20 | from boto.s3.connection import S3Connection 21 | from boto.s3.key import Key 22 | from subprocess import call, check_output, CalledProcessError 23 | from os.path import isfile, join 24 | import redis 25 | from rq import Worker, Queue, Connection 26 | 27 | from poster.encode import multipart_encode 28 | from poster.streaminghttp import register_openers 29 | import urllib2 30 | 31 | import requests 32 | 33 | job = session.query(Job).get(id) 34 | job.mod_date = datetime.now() 35 | job.status = 'uploading' 36 | session.commit() 37 | 38 | ######################################################################## 39 | # G3 TIFF CONVERSION PHASE 40 | ######################################################################## 41 | 42 | path = './tmp/' + job.access_key 43 | o('Creating temporary directory: %s' % path) ########################### 44 | 45 | try: 46 | if not os.path.exists(path): 47 | os.makedirs(path) 48 | except: 49 | return fail('JOBS_CREATE_DIR_FAIL', job, db) 50 | 51 | if job.filename: 52 | file_prefix, file_extension = os.path.splitext(job.filename) 53 | elif job.body: 54 | file_extension = '.txt' 55 | else: 56 | return fail('JOBS_NO_ATTACHMENT_FOUND', job, db) 57 | 58 | if file_extension == '.docx' or file_extension == '.doc': 59 | 60 | if not os.environ.get('SERVEOFFICE2PDF_URL'): 61 | return fail('JOBS_NO_SERVEOFFICE2PDF', job, db) 62 | 63 | try: 64 | job.filename = save_local_file(job.access_key, job.filename, data) 65 | except: 66 | return fail('JOBS_LOCAL_SAVE_FAIL', job, db) 67 | 68 | o('Calling ServeOffice2PDF to convert DOCX to PDF.') ############### 69 | 70 | office_url = os.environ.get('SERVEOFFICE2PDF_URL').strip() 71 | files = {"data": open('%s/%s' % (path, job.filename), 'rb')} 72 | params = {'filename': job.filename} 73 | response = requests.post(office_url, files=files, data=params) 74 | 75 | if not response.status_code == 200: 76 | return fail('JOBS_PDF_CONVERT_FAIL', job, db) 77 | 78 | o('Successfully converted file to PDF. Now converting to tiff.') ### 79 | pdf_file = open('%s/%s.pdf' % (path, job.filename), 'wb') 80 | pdf_file.write(response.content) 81 | pdf_file.close() 82 | 83 | try: 84 | convert_to_tiff(job.access_key, job.filename + '.pdf', True) 85 | except CalledProcessError, e: 86 | return fail('JOBS_IMG_CONVERT_FAIL', job, db, str(e)) 87 | 88 | elif file_extension == '.pdf': 89 | 90 | try: 91 | job.filename = save_local_file(job.access_key, job.filename, data) 92 | except: 93 | return fail('JOBS_LOCAL_SAVE_FAIL', job, db) 94 | 95 | try: 96 | convert_to_tiff(job.access_key, job.filename, True) 97 | except CalledProcessError, e: 98 | return fail('JOBS_IMG_CONVERT_FAIL', job, db, str(e)) 99 | 100 | elif file_extension == '.png' or file_extension == '.jpg': 101 | 102 | try: 103 | job.filename = save_local_file(job.access_key, job.filename, data) 104 | except: 105 | return fail('JOBS_LOCAL_SAVE_FAIL', job, db) 106 | 107 | try: 108 | convert_to_tiff(job.access_key, job.filename) 109 | except CalledProcessError, e: 110 | return fail('JOBS_IMG_CONVERT_FAIL', job, db, str(e)) 111 | 112 | elif file_extension == '.txt': 113 | 114 | txt_filename = job.filename if job.filename else "fax.txt" 115 | job.filename = save_local_file(job.access_key, txt_filename, data) 116 | 117 | try: 118 | convert_txt_to_ps(job.access_key, job.filename + ".ps") 119 | except CalledProcessError, e: 120 | return fail('JOBS_TXT_CONVERT_FAIL', job, db, str(e)) 121 | 122 | try: 123 | convert_to_tiff(job.access_key, job.filename + ".ps", True) 124 | except CalledProcessError, e: 125 | return fail('JOBS_IMG_CONVERT_FAIL', job, db, str(e)) 126 | 127 | else: 128 | return fail('JOBS_UNSUPPORTED_FORMAT', job, db) 129 | 130 | files = [ f for f in os.listdir(path) if isfile(join(path,f)) ] 131 | 132 | num_pages = 0 133 | 134 | for f in files: 135 | file_prefix, file_extension = os.path.splitext(f) 136 | if file_extension == '.tiff': 137 | num_pages = num_pages + 1 138 | 139 | job.status = 'processing' 140 | job.mod_date = datetime.now() 141 | job.num_pages = num_pages 142 | job.compute_cost() 143 | session.commit() 144 | 145 | ######################################################################## 146 | # OPTIONAL AMAZON S3 UPLOAD PHASE 147 | ######################################################################## 148 | 149 | if os.environ.get('AWS_STORAGE') == "on": 150 | o('Connecting to S3') ############################################## 151 | try: 152 | conn = S3Connection(os.environ.get('AWS_ACCESS_KEY'), 153 | os.environ.get('AWS_SECRET_KEY')) 154 | bucket = conn.get_bucket(os.environ.get('AWS_S3_BUCKET')) 155 | except: 156 | return fail('JOBS_CONNECT_S3_FAIL', job, db) 157 | 158 | for f in files: 159 | try: 160 | file_prefix, file_extension = os.path.splitext(f) 161 | 162 | if file_extension == '.tiff': 163 | o('Uploading to S3: ' + f) ############################# 164 | k = Key(bucket) 165 | k.key = 'fax/' + job.access_key + '/' + f 166 | k.set_contents_from_filename(path + '/' + f) 167 | k.set_acl('public-read') 168 | except: 169 | return fail('JOBS_UPLOAD_S3_FAIL', job, db) 170 | 171 | job.mod_date = datetime.now() 172 | 173 | if job.send_authorized: 174 | try: 175 | job.rate_limit(job.account_id) 176 | except: 177 | return fail('JOBS_RATE_LIMIT_REACHED', job, db) 178 | job.status = 'queued' 179 | session.commit() 180 | redis_conn = redis.from_url(os.environ.get('REDIS_URI')) 181 | q = Queue(job.account.get_priority(), connection=redis_conn) 182 | q.enqueue_call(func=send_fax, args=(id,), timeout=3600) 183 | else: 184 | job.status = 'ready' 185 | session.commit() 186 | if job.callback_url: 187 | send_job_callback(job, session) 188 | 189 | return "SUCCESS: %s" % id 190 | 191 | def send_fax(id): 192 | 193 | from boto.s3.connection import S3Connection 194 | from boto.s3.key import Key 195 | from datetime import date 196 | from subprocess import check_output, CalledProcessError, STDOUT 197 | import stripe 198 | import traceback 199 | import json 200 | from library.mailer import email_recharge_payment 201 | from rq import Worker 202 | 203 | device = Worker.MODEM_DEVICE 204 | caller_id = Worker.CALLER_ID 205 | 206 | stripe.api_key = os.environ.get('STRIPE_SECRET_KEY') 207 | 208 | job = session.query(Job).get(id) 209 | 210 | if not job.status == 'ready' and not job.status == 'queued': 211 | return fail('JOBS_CANNOT_SEND_NOW', job, db) 212 | 213 | if job.data_deleted: 214 | return fail('JOBS_FAIL_DATA_DELETED', job, db) 215 | 216 | cost = job.cost if not job.cover else job.cost + job.cover_cost 217 | 218 | ######################################################################## 219 | # MAKE SURE THE CUSTOMER ACTUALLY HAS MONEY PHASE 220 | ######################################################################## 221 | 222 | if os.environ.get('REQUIRE_PAYMENTS') == 'on': 223 | if job.account.credit - cost < 0 and not job.account.allow_overflow: 224 | if job.account.stripe_card and job.account.auto_recharge: 225 | try: 226 | charge = stripe.Charge.create( 227 | amount=1000, 228 | currency="usd", 229 | customer=job.account.stripe_token, 230 | description="Auto-recharging account %s"% job.account.id 231 | ) 232 | data = { 233 | 'account_id': job.account.id, 234 | 'amount': 10, 235 | 'source': 'stripe', 236 | 'source_id': charge["id"], 237 | 'job_id': job.id, 238 | 'job_destination': job.destination, 239 | 'ip_address': job.ip_address, 240 | 'initial_balance': job.account.credit, 241 | 'trans_type': 'auto_recharge' 242 | } 243 | trans = Transaction(**data) 244 | session.add(trans) 245 | session.commit() 246 | 247 | job.account.add_credit(10, session) 248 | # email_recharge_payment(job.account, 10, trans.id, 249 | # charge.source.last4) 250 | 251 | except: 252 | payment = {'_DEBUG': traceback.format_exc()} 253 | data = { 254 | 'amount': 10, 255 | 'account_id': job.account.id, 256 | 'source': 'stripe', 257 | 'debug': json.dumps(payment), 258 | 'ip_address': job.ip_address, 259 | 'payment_type': 'auto_recharge' 260 | } 261 | failed_payment = FailedPayment(**data) 262 | session.add(failed_payment) 263 | session.commit() 264 | # JL TODO ~ Notify customer that the card was declined 265 | return fail('JOBS_CARD_DECLINED', job, db) 266 | else: 267 | return fail('JOBS_INSUFFICIENT_CREDIT', job, db) 268 | 269 | job.mod_date = datetime.now() 270 | job.start_date = datetime.now() 271 | job.attempts = job.attempts + 1 272 | job.device = device 273 | job.status = 'started' 274 | session.commit() 275 | 276 | files_to_send = [] 277 | 278 | ######################################################################## 279 | # COVER SHEET GENERATION PHASE 280 | ######################################################################## 281 | 282 | path = './tmp/' + job.access_key 283 | o('Touching temporary directory: %s' % path) ########################### 284 | 285 | try: 286 | if not os.path.exists(path): 287 | os.makedirs(path) 288 | except: 289 | return fail('JOBS_CREATE_DIR_FAIL', job, db) 290 | 291 | if job.cover: 292 | o('Generating cover sheet') ######################################## 293 | 294 | try: 295 | o('Generating cover.png') ###################################### 296 | 297 | cmd = ["convert", "-density", "400", "-flatten", 298 | "./media/cover_sheets/default.pdf", "-gravity", 299 | "None"] 300 | v = 300 301 | 302 | if job.cover_name: 303 | cmd.extend(["-annotate", "+468+%s" % v, job.cover_name]) 304 | cmd.extend(["-annotate", "+2100+1215", job.cover_name]) 305 | v = v + 80 306 | 307 | if job.cover_address: 308 | cmd.extend(["-annotate", "+468+%s" % v, job.cover_address]) 309 | v = v + 80 310 | 311 | if job.cover_city or job.cover_state or job.cover_zip: 312 | cmd.extend(["-annotate", "+468+%s" % v, 313 | "%s, %s %s" % (job.cover_city, job.cover_state, 314 | job.cover_zip)]) 315 | v = v + 80 316 | 317 | if job.cover_country: 318 | cmd.extend(["-annotate", "+468+%s" % v, job.cover_country]) 319 | v = v + 80 320 | 321 | if job.cover_phone: 322 | cmd.extend(["-annotate", "+468+%s" % v, job.cover_phone]) 323 | v = v + 80 324 | 325 | if job.cover_email: 326 | cmd.extend(["-annotate", "+468+%s" % v, job.cover_email]) 327 | v = v + 80 328 | 329 | if job.cover_to_name: 330 | cmd.extend(["-annotate", "+800+1215", job.cover_to_name]) 331 | 332 | if job.cover_subject: 333 | cmd.extend(["-annotate", "+800+1340", job.cover_subject]) 334 | 335 | cmd.extend(["-annotate", "+2100+1340", "%s" % job.num_pages]) 336 | cmd.extend(["-annotate", "+800+1465", "%s" % date.today()]) 337 | 338 | if job.cover_cc: 339 | cmd.extend(["-annotate", "+2100+1465", job.cover_cc]) 340 | 341 | if "urgent" in job.cover_status: 342 | cmd.extend(["-annotate", "+473+1740", "X"]) 343 | 344 | if "review" in job.cover_status: 345 | cmd.extend(["-annotate", "+825+1740", "X"]) 346 | 347 | if "comment" in job.cover_status: 348 | cmd.extend(["-annotate", "+1285+1740", "X"]) 349 | 350 | if "reply" in job.cover_status: 351 | cmd.extend(["-annotate", "+1910+1740", "X"]) 352 | 353 | if "shred" in job.cover_status: 354 | cmd.extend(["-annotate", "+2420+1740", "X"]) 355 | 356 | cmd.extend([ 357 | "-pointsize", 358 | "11", 359 | "./tmp/" + job.access_key + "/cover.png" 360 | ]) 361 | output = check_output(cmd) 362 | except CalledProcessError, e: 363 | return fail('JOBS_COVER_MAIN_FAIL', job, db, str(e)) 364 | 365 | if job.cover_company: 366 | try: 367 | o('Generating company.png') ################################ 368 | 369 | cmd = ["convert", "-density", "400", "-gravity", "Center", 370 | "-background", "black", "-fill", "white", 371 | "-pointsize", "20", "-size", "1400x", 372 | "caption:%s" % job.cover_company, "-bordercolor", 373 | "black", "-border", "30", 374 | "./tmp/" + job.access_key + "/company.png"] 375 | output = check_output(cmd) 376 | except CalledProcessError, e: 377 | return fail('JOBS_COVER_COMPANY_FAIL', job, db, str(e)) 378 | 379 | try: 380 | o('Overlaying company.png on cover.png') ################### 381 | 382 | cmd = ["composite", "-density", "400", "-gravity", 383 | "NorthEast", "-geometry", "+300+200", 384 | "./tmp/" + job.access_key + "/company.png", 385 | "./tmp/" + job.access_key + "/cover.png", 386 | "./tmp/" + job.access_key + "/cover.png"] 387 | output = check_output(cmd) 388 | except CalledProcessError, e: 389 | return fail('JOBS_COVER_OVERLAY_FAIL', job, db, str(e)) 390 | 391 | if job.cover_comments: 392 | try: 393 | o('Generating comments.png') ############################### 394 | 395 | cmd = ["convert", "-density", "400", "-gravity", 396 | "NorthWest", "-background", "white", "-fill", 397 | "black", "-pointsize", "11", "-size", "2437x2000", 398 | "-font", "Liberation-Mono-Regular", 399 | "caption:%s" % job.cover_comments, 400 | "./tmp/" + job.access_key + "/comments.png"] 401 | output = check_output(cmd) 402 | except CalledProcessError, e: 403 | return fail('JOBS_COVER_COMMENTS_FAIL', job, db, str(e)) 404 | 405 | try: 406 | o('Overlaying comments.png on cover.png') ################## 407 | 408 | cmd = ["composite", "-density", "400", "-gravity", "None", 409 | "-geometry", "+468+2000", 410 | "./tmp/" + job.access_key + "/comments.png", 411 | "./tmp/" + job.access_key + "/cover.png", 412 | "./tmp/" + job.access_key + "/cover.png"] 413 | output = check_output(cmd) 414 | except CalledProcessError, e: 415 | return fail('JOBS_COVER_OVERLAY_FAIL', job, db, str(e)) 416 | 417 | try: 418 | o('Converting cover.png to G3 .tiff') ########################## 419 | 420 | cmd = ["convert", "-scale", "50%", 421 | "./tmp/" + job.access_key + "/cover.png", 422 | "fax:./tmp/" + job.access_key + "/cover.tiff"] 423 | output = check_output(cmd) 424 | except CalledProcessError, e: 425 | return fail('JOBS_COVER_TIFF_FAIL', job, db, str(e)) 426 | 427 | files_to_send.append(u'%s/cover.tiff' % path) 428 | 429 | ######################################################################## 430 | # LOAD FILES PHASE 431 | ######################################################################## 432 | 433 | filename = job.filename if job.filename else "fax.txt" 434 | 435 | if os.environ.get('AWS_STORAGE') == "on": 436 | o('Connecting to S3') ################################################## 437 | try: 438 | conn = S3Connection(os.environ.get('AWS_ACCESS_KEY'), 439 | os.environ.get('AWS_SECRET_KEY')) 440 | bucket = conn.get_bucket(os.environ.get('AWS_S3_BUCKET')) 441 | except: 442 | return fail('JOBS_CONNECT_S3_FAIL', job, db) 443 | 444 | try: 445 | for i in range(0, job.num_pages): 446 | 447 | num = ("0%s" % i) if i < 10 else "%s" % i 448 | 449 | o('Download: %s/%s.%s.tiff' % (job.access_key, filename, num)) 450 | 451 | k = Key(bucket) 452 | k.key = 'fax/%s/%s.%s.tiff' %(job.access_key, filename, num) 453 | k.get_contents_to_filename('%s/%s.%s.tiff' % (path, 454 | filename, num)) 455 | 456 | files_to_send.append('%s/%s.%s.tiff' %(path, filename, num)) 457 | except: 458 | return fail('JOBS_DOWNLOAD_S3_FAIL', job, db) 459 | else: 460 | for i in range(0, job.num_pages): 461 | num = ("0%s" % i) if i < 10 else "%s" % i 462 | files_to_send.append('%s/%s.%s.tiff' %(path, filename, num)) 463 | 464 | ######################################################################## 465 | # SEND FAX PHASE 466 | ######################################################################## 467 | if os.environ.get('PHAXIO_SENDING') == 'on': 468 | 469 | import requests 470 | from mimetypes import MimeTypes 471 | 472 | payload = { 473 | "to": "+%s" % job.destination, 474 | "api_key": os.environ.get('PHAXIO_API_KEY'), 475 | "api_secret": os.environ.get('PHAXIO_API_SECRET') 476 | } 477 | if os.environ.get('PHAXIO_OVERRIDE_RECEIVED'): 478 | payload["callback_url"] = os.environ.get('PHAXIO_OVERRIDE_SENT') 479 | 480 | incoming_numbers = session.query(IncomingNumber).filter_by( 481 | account_id= job.account.id) 482 | 483 | if incoming_numbers and incoming_numbers.first(): 484 | payload["caller_id"] = incoming_numbers.first().fax_number 485 | 486 | files = [] 487 | 488 | if job.cover: 489 | 490 | cover_filename = u'%s/cover.png' % path 491 | f1 = open(cover_filename) 492 | files.append( 493 | ('filename[]', ('cover.png', f1, 'image/png')) 494 | ) 495 | 496 | mime = MimeTypes() 497 | mimetype = mime.guess_type(job.filename) 498 | 499 | job_filename = u'%s/%s' % (path, job.filename) 500 | 501 | f2 = open(job_filename) 502 | files.append( 503 | ('filename[]', (job.filename, f2, mimetype)) 504 | ) 505 | 506 | try: 507 | url = "https://api.phaxio.com/v1/send" 508 | r = requests.post(url, data=payload, files=files) 509 | response = json.loads(r.text) 510 | 511 | o('Got response from Twilio...') 512 | o(r.text) 513 | 514 | if response["success"] == False or not "data" in response \ 515 | or not "faxId" in response["data"]: 516 | 517 | bad_number = "Phone number is not formatted correctly" 518 | 519 | if "message" in response and bad_number in response["message"]: 520 | return fail('JOBS_PHAXIO_BAD_NUMBER', job, db, r.text) 521 | else: 522 | return fail('JOBS_PHAXIO_TRANSMIT_FAIL', job, db, r.text) 523 | except: 524 | return fail('JOBS_PHAXIO_TRANSMIT_FAIL', job, db, "bah") 525 | 526 | if f2: 527 | f2.close() 528 | 529 | if job.cover: 530 | f1.close() 531 | 532 | job.external_id = response["data"]["faxId"] 533 | job.debug = r.text 534 | session.commit() 535 | 536 | return "waiting..." 537 | 538 | else: 539 | try: 540 | o('Modem dialing: 1%s' % job.destination) 541 | 542 | cmd = ["efax", "-d", device, "-o1flll ", "-vchewmainrft ", "-l", 543 | caller_id, "-t", "%s" % job.destination] 544 | cmd.extend(files_to_send) 545 | output = check_output(cmd, stderr=STDOUT) 546 | o('%s' % output) 547 | 548 | except CalledProcessError, e: 549 | 550 | output = str(e.output) 551 | 552 | if "No Answer" in output: 553 | if os.environ.get('REQUIRE_PAYMENTS') == 'on': 554 | o('No answer. Charge the customer for wasting our time.') 555 | o('Debiting $%s on account ID %s' % (cost, job.account.id)) 556 | commit_transaction(job, cost, 'no_answer_charge') 557 | 558 | return fail('JOBS_TRANSMIT_NO_ANSWER', job, db, output) 559 | 560 | elif "number busy or modem in use" in output: 561 | o('Line busy.') 562 | return fail('JOBS_TRANSMIT_BUSY', job, db, output) 563 | 564 | else: 565 | o('Transmit error: %s' % output) 566 | return fail('JOBS_TRANSMIT_FAIL', job, db, output) 567 | 568 | o('Job completed without error!') 569 | job.debug = output 570 | 571 | return mark_job_sent(job, cost) 572 | 573 | def mark_job_sent(job, cost, db_session = None): 574 | 575 | from library.mailer import email_success 576 | 577 | if not db_session: 578 | db_session = session 579 | 580 | job.mod_date = datetime.now() 581 | job.end_date = datetime.now() 582 | job.status = 'sent' 583 | db_session.commit() 584 | 585 | o('Deleting data lol.') 586 | job.delete_data(db_session) 587 | 588 | if job.account.email_success: 589 | email_success(job) 590 | 591 | if job.callback_url: 592 | send_job_callback(job, db_session) 593 | 594 | if os.environ.get('REQUIRE_PAYMENTS') == 'on': 595 | o('Debiting $%s on account ID %s' % (cost, job.account.id)) 596 | commit_transaction(job, cost, 'job_complete') 597 | 598 | return True 599 | 600 | def commit_transaction(job, cost, trans_type, note = None): 601 | data = { 602 | 'account_id': job.account.id, 603 | 'amount': cost * -1, 604 | 'job_id': job.id, 605 | 'job_destination': job.destination, 606 | 'ip_address': job.ip_address, 607 | 'initial_balance': job.account.credit, 608 | 'trans_type': trans_type 609 | } 610 | if note: 611 | data['note'] = note 612 | 613 | transaction = Transaction(**data) 614 | session.add(transaction) 615 | session.commit() 616 | 617 | job.account.subtract_credit(cost, session) 618 | 619 | def convert_to_tiff(access_key, filename, flatten = False): 620 | 621 | from subprocess import check_output 622 | 623 | o('Convert %s to .tiff' % filename) ######################################## 624 | 625 | file_prefix, file_extension = os.path.splitext(filename) 626 | 627 | if not flatten: 628 | command = [ 629 | "convert", 630 | "-density", 631 | "400 ", 632 | "-resize", 633 | "1760x2200 ", 634 | "./tmp/" + access_key + "/" + filename, 635 | "fax:./tmp/" + access_key + "/"+ file_prefix +".%02d.tiff" 636 | ] 637 | else: 638 | command = [ 639 | "convert", 640 | "-density", 641 | "400 ", 642 | "-resize", 643 | "1760x2200 ", 644 | "-background", 645 | "White", 646 | "-alpha", 647 | "Background", 648 | "./tmp/" + access_key + "/" + filename, 649 | "fax:./tmp/" + access_key + "/"+ file_prefix +".%02d.tiff" 650 | ] 651 | output = check_output(command) 652 | 653 | def convert_txt_to_ps(access_key, filename): 654 | 655 | from subprocess import check_output 656 | 657 | o('Convert %s to .ps' % filename) ######################################## 658 | 659 | file_prefix, file_extension = os.path.splitext(filename) 660 | 661 | command = [ 662 | "paps", 663 | "--font", 664 | "Liberation Mono", 665 | "--cpi", 666 | " 12", 667 | # "--header", # JL NOTE ~ probably don't want to have this 668 | file_prefix 669 | ] 670 | output = check_output(command, cwd="./tmp/"+access_key) 671 | 672 | f = open('./tmp/' + access_key + '/' + filename, 'wb') 673 | f.write(output) 674 | f.close() 675 | 676 | def save_local_file(access_key, filename, data): 677 | 678 | from werkzeug.utils import secure_filename 679 | 680 | o('Saving local file: %s' % filename) ###################################### 681 | 682 | safe_filename = secure_filename(filename) 683 | 684 | f = open(u'./tmp/' + access_key + u'/' + safe_filename, 'wb') 685 | f.write(data) 686 | f.close() 687 | 688 | return safe_filename 689 | 690 | def fail(ref, job, db, debug=None, db_session=None): 691 | 692 | from library.mailer import email_fail 693 | from rq import Worker 694 | 695 | if not db_session: 696 | db_session = session 697 | 698 | try: 699 | device = Worker.MODEM_DEVICE 700 | except AttributeError: 701 | device = "" 702 | 703 | o('JOB FAILED: %s (%s)' % (ref, debug)) 704 | 705 | error = worker_error(ref) 706 | job.failed = 1 707 | job.fail_code = error["code"] 708 | job.fail_date = datetime.now() 709 | job.mod_date = datetime.now() 710 | job.status = 'failed' 711 | job.debug = error["msg"] if debug == None else debug 712 | db_session.commit() 713 | 714 | if job.account.email_fail: 715 | email_fail(job, error["msg"], error["code"], error["status"], 716 | { 717 | "device": device, 718 | "output": debug 719 | } 720 | ) 721 | 722 | if job.callback_url: 723 | send_job_callback(job, db_session) 724 | 725 | return "FAILED" 726 | 727 | def send_job_callback(job, db_session): 728 | import requests 729 | import traceback 730 | 731 | job.callback_date = datetime.now() 732 | 733 | try: 734 | r = requests.post(job.callback_url, data=job.public_data(), timeout=5) 735 | r.raise_for_status() 736 | o("Sent callback: %s; (HTTP 200)" % job.callback_url) 737 | except: 738 | o("CALLBACK FAIL: %s / %s" % (job.callback_url, traceback.format_exc())) 739 | job.callback_fail = job.callback_fail + 1 740 | 741 | db_session.commit() 742 | 743 | 744 | 745 | --------------------------------------------------------------------------------