def send_confirmation(addr, user_id): g.log = g.log.new(address=addr, user_id=user_id) g.log.info('Sending email confirmation for new address on account.') addr = addr.lower().strip() if not IS_VALID_EMAIL(addr): g.log.info('Failed. Invalid address.') raise ValueError(u'Cannot send confirmation. ' '{} is not a valid email.'.format(addr)) message = u'email={email}&user_id={user_id}'.format( email=addr, user_id=user_id) digest = hmac.new( settings.NONCE_SECRET, message.encode('utf-8'), hashlib.sha256 ).hexdigest() link = url_for('confirm-account-email', digest=digest, email=addr, _external=True) res = send_email( to=addr, subject='Confirm email for your account at %s' % settings.SERVICE_NAME, text=render_template('email/confirm-account.txt', email=addr, link=link), html=render_template_string(TEMPLATES.get('confirm-account.html'), email=addr, link=link), sender=settings.ACCOUNT_SENDER ) if not res[0]: g.log.info('Failed to send email.', reason=res[1], code=res[2]) return False else: return True
def request_unconfirm_form(form_id): ''' This endpoints triggers a confirmation email that directs users to the GET version of unconfirm_form. ''' form = Form.query.get(form_id) unconfirm_url = url_for('unconfirm_form', form_id=form.id, digest=form.unconfirm_digest(), _external=True) send_email( to=form.email, subject='Unsubscribe from form at {}'.format(form.host), html=render_template_string( TEMPLATES.get('unsubscribe-confirmation.html'), url=unconfirm_url, email=form.email, host=form.host), text=render_template('email/unsubscribe-confirmation.txt', url=unconfirm_url, email=form.email, host=form.host), sender=settings.DEFAULT_SENDER, ) return render_template( 'info.html', title='Link sent to your address', text="We've sent an email to {} with a link to finish " "unsubscribing.".format(form.email)), 200
def request_unconfirm_form(form_id): ''' This endpoints triggers a confirmation email that directs users to the GET version of unconfirm_form. ''' form = Form.query.get(form_id) unconfirm_url = url_for( 'unconfirm_form', form_id=form.id, digest=form.unconfirm_digest(), _external=True ) send_email( to=form.email, subject='Unsubscribe from form at {}'.format(form.host), html=render_template_string(TEMPLATES.get('unsubscribe-confirmation.html'), url=unconfirm_url, email=form.email, host=form.host), text=render_template('email/unsubscribe-confirmation.txt', url=unconfirm_url, email=form.email, host=form.host), sender=settings.DEFAULT_SENDER, ) return render_template('info.html', title='Link sent to your address', text="We've sent an email to {} with a link to finish " "unsubscribing.".format(form.email)), 200
def send_downgrade_email(customer_email): send_email(to=customer_email, subject='Successfully downgraded from {} {}'.format( settings.SERVICE_NAME, settings.UPGRADED_PLAN_NAME), text=render_template('email/downgraded.txt'), html=render_template_string(TEMPLATES.get('downgraded.html')), sender=settings.DEFAULT_SENDER)
def send_downgrade_email(customer_email): send_email( to=customer_email, subject='Successfully downgraded from {} {}'.format(settings.SERVICE_NAME, settings.UPGRADED_PLAN_NAME), text=render_template('email/downgraded.txt'), html=render_template_string(TEMPLATES.get('downgraded.html')), sender=settings.DEFAULT_SENDER )
def stripe_webhook(): payload = request.data.decode('utf-8') g.log.info('Received webhook from Stripe') sig_header = request.headers.get('STRIPE_SIGNATURE') event = None try: if settings.TESTING: event = request.get_json() else: event = stripe.Webhook.construct_event( payload, sig_header, settings.STRIPE_WEBHOOK_SECRET) if event[ 'type'] == 'customer.subscription.deleted': # User subscription has expired customer_id = event['data']['object']['customer'] customer = stripe.Customer.retrieve(customer_id) if len(customer.subscriptions.data) == 0: user = User.query.filter_by(stripe_id=customer_id).first() user.plan = Plan.free DB.session.add(user) DB.session.commit() g.log.info('Downgraded user from webhook.', account=user.email) send_downgrade_email.delay(user.email) elif event['type'] == 'invoice.payment_failed': # User payment failed customer_id = event['data']['object']['customer'] customer = stripe.Customer.retrieve(customer_id) g.log.info('User payment failed', account=customer.email) send_email( to=customer.email, subject='[ACTION REQUIRED] Failed Payment for {} {}'.format( settings.SERVICE_NAME, settings.UPGRADED_PLAN_NAME), text=render_template('email/payment-failed.txt'), html=render_template_string( TEMPLATES.get('payment-failed.html')), sender=settings.DEFAULT_SENDER) return 'ok' except ValueError as e: g.log.error('Webhook failed for customer', json=event, error=e) return 'Failure, developer please check logs', 500 except stripe.error.SignatureVerificationError as e: g.log.error('Webhook failed Stripe signature verification', json=event, error=e) return '', 400 except Exception as e: g.log.error('Webhook failed for unknown reason', json=event, error=e) return '', 500
def send_password_reset(self): g.log.info('Sending password reset.', account=self.email) digest = self.reset_password_digest() link = url_for('reset-password', digest=digest, email=self.email, _external=True) res = send_email( to=self.email, subject='Reset your %s password!' % settings.SERVICE_NAME, text=render_template('email/reset-password.txt', addr=self.email, link=link), html=render_template_string(TEMPLATES.get('reset-password.html'), add=self.email, link=link), sender=settings.ACCOUNT_SENDER ) if not res[0]: g.log.info('Failed to send email.', reason=res[1], code=res[2]) return False else: return True
def render_content(ext): data, keys = None, None if store_data: if type(store_data) in (ImmutableMultiDict, ImmutableOrderedMultiDict): data, _ = http_form_to_dict(store_data) store_first_submission(nonce, data) else: store_first_submission(nonce, store_data) params = dict(email=self.email, host=self.host, nonce_link=link, keys=keys) if ext == 'html': return render_template_string(TEMPLATES.get('confirm.html'), **params) elif ext == 'txt': return render_template('email/confirm.txt', **params)
def stripe_webhook(): payload = request.data.decode('utf-8') g.log.info('Received webhook from Stripe') sig_header = request.headers.get('STRIPE_SIGNATURE') event = None try: if settings.TESTING: event = request.get_json() else: event = stripe.Webhook.construct_event( payload, sig_header, settings.STRIPE_WEBHOOK_SECRET) if event['type'] == 'customer.subscription.deleted': # User subscription has expired customer_id = event['data']['object']['customer'] customer = stripe.Customer.retrieve(customer_id) if len(customer.subscriptions.data) == 0: user = User.query.filter_by(stripe_id=customer_id).first() user.upgraded = False DB.session.add(user) DB.session.commit() g.log.info('Downgraded user from webhook.', account=user.email) send_downgrade_email.delay(user.email) elif event['type'] == 'invoice.payment_failed': # User payment failed customer_id = event['data']['object']['customer'] customer = stripe.Customer.retrieve(customer_id) g.log.info('User payment failed', account=customer.email) send_email(to=customer.email, subject='[ACTION REQUIRED] Failed Payment for {} {}'.format(settings.SERVICE_NAME, settings.UPGRADED_PLAN_NAME), text=render_template('email/payment-failed.txt'), html=render_template_string(TEMPLATES.get('payment-failed.html')), sender=settings.DEFAULT_SENDER) return 'ok' except ValueError as e: g.log.error('Webhook failed for customer', json=event, error=e) return 'Failure, developer please check logs', 500 except stripe.error.SignatureVerificationError as e: g.log.error('Webhook failed Stripe signature verification', json=event, error=e) return '', 400 except Exception as e: g.log.error('Webhook failed for unknown reason', json=event, error=e) return '', 500
def render_content(ext): data, keys = None, None if store_data: if type(store_data) in ( ImmutableMultiDict, ImmutableOrderedMultiDict): data, _ = http_form_to_dict(store_data) store_first_submission(nonce, data) else: store_first_submission(nonce, store_data) params = dict( email=self.email, host=self.host, nonce_link=link, keys=keys ) if ext == 'html': return render_template_string(TEMPLATES.get('confirm.html'), **params) elif ext == 'txt': return render_template('email/confirm.txt', **params)
def send(self, data, keys, referrer): ''' Sends form to user's email. Assumes sender's email has been verified. ''' subject = data.get('_subject') or \ 'New submission from %s' % referrer_to_path(referrer) reply_to = (data.get( '_replyto', data.get('email', data.get('Email')) ) or '').strip() cc = data.get('_cc', None) next = next_url(referrer, data.get('_next')) spam = data.get('_gotcha', None) format = data.get('_format', None) # turn cc emails into array if cc: cc = [email.strip() for email in cc.split(',')] # prevent submitting empty form if not any(data.values()): return {'code': Form.STATUS_EMAIL_EMPTY} # return a fake success for spam if spam: g.log.info('Submission rejected.', gotcha=spam) return {'code': Form.STATUS_EMAIL_SENT, 'next': next} # validate reply_to, if it is not a valid email address, reject if reply_to and not IS_VALID_EMAIL(reply_to): g.log.info('Submission rejected. Reply-To is invalid.', reply_to=reply_to) return { 'code': Form.STATUS_REPLYTO_ERROR, 'address': reply_to, 'referrer': referrer } # increase the monthly counter request_date = datetime.datetime.now() self.increase_monthly_counter(basedate=request_date) # increment the forms counter self.counter = Form.counter + 1 # if submission storage is disabled and form is upgraded, don't store submission if self.disable_storage and self.upgraded: pass else: DB.session.add(self) # archive the form contents sub = Submission(self.id) sub.data = {key: data[key] for key in data if key not in KEYS_NOT_STORED} DB.session.add(sub) # commit changes DB.session.commit() # sometimes we'll delete all archived submissions over the limit if random.random() < settings.EXPENSIVELY_WIPE_SUBMISSIONS_FREQUENCY: records_to_keep = settings.ARCHIVED_SUBMISSIONS_LIMIT total_records = DB.session.query(func.count(Submission.id)) \ .filter_by(form_id=self.id) \ .scalar() if total_records > records_to_keep: newest = self.submissions.with_entities(Submission.id).limit(records_to_keep) DB.engine.execute( delete(table('submissions')). \ where(Submission.form_id == self.id). \ where(~Submission.id.in_(newest)) ) # url to request_unconfirm_form page unconfirm = url_for('request_unconfirm_form', form_id=self.id, _external=True) # check if the forms are over the counter and the user is not upgraded overlimit = False monthly_counter = self.get_monthly_counter() monthly_limit = settings.MONTHLY_SUBMISSIONS_LIMIT \ if self.id > settings.FORM_LIMIT_DECREASE_ACTIVATION_SEQUENCE \ else settings.GRANDFATHER_MONTHLY_LIMIT if monthly_counter > monthly_limit and not self.upgraded: overlimit = True if monthly_counter == int(monthly_limit * 0.9) and not self.upgraded: # send email notification send_email( to=self.email, subject="Formspree Notice: Approaching submission limit.", text=render_template('email/90-percent-warning.txt', unconfirm_url=unconfirm, limit=monthly_limit ), html=render_template_string( TEMPLATES.get('90-percent-warning.html'), unconfirm_url=unconfirm, limit=monthly_limit ), sender=settings.DEFAULT_SENDER ) now = datetime.datetime.utcnow().strftime('%I:%M %p UTC - %d %B %Y') if not overlimit: g.log.info('Submitted.') text = render_template('email/form.txt', data=data, host=self.host, keys=keys, now=now, unconfirm_url=unconfirm) # check if the user wants a new or old version of the email if format == 'plain': html = render_template('email/plain_form.html', data=data, host=self.host, keys=keys, now=now, unconfirm_url=unconfirm) else: html = render_template_string(TEMPLATES.get('form.html'), data=data, host=self.host, keys=keys, now=now, unconfirm_url=unconfirm) else: g.log.info('Submission rejected. Form over quota.', monthly_counter=monthly_counter) # send an overlimit notification for the first x overlimit emails # after that, return an error so the user can know the website owner is not # going to read his message. if monthly_counter <= monthly_limit + settings.OVERLIMIT_NOTIFICATION_QUANTITY: subject = 'Formspree Notice: Your submission limit has been reached.' text = render_template('email/overlimit-notification.txt', host=self.host, unconfirm_url=unconfirm, limit=monthly_limit) html = render_template_string(TEMPLATES.get('overlimit-notification.html'), host=self.host, unconfirm_url=unconfirm, limit=monthly_limit) else: return {'code': Form.STATUS_OVERLIMIT} # if emails are disabled and form is upgraded, don't send email notification if self.disable_email and self.upgraded: return {'code': Form.STATUS_NO_EMAIL, 'next': next} else: result = send_email( to=self.email, subject=subject, text=text, html=html, sender=settings.DEFAULT_SENDER, reply_to=reply_to, cc=cc, headers={ 'List-Unsubscribe-Post': 'List-Unsubscribe=One-Click', 'List-Unsubscribe': '<' + url_for( 'unconfirm_form', form_id=self.id, digest=self.unconfirm_digest(), _external=True ) + '>' } ) if not result[0]: g.log.warning('Failed to send email.', reason=result[1], code=result[2]) if result[1].startswith('Invalid replyto email address'): return { 'code': Form.STATUS_REPLYTO_ERROR, 'address': reply_to, 'referrer': referrer } return { 'code': Form.STATUS_EMAIL_FAILED, 'mailer-code': result[2], 'error-message': result[1] } return {'code': Form.STATUS_EMAIL_SENT, 'next': next}