def test_create_email_sent_protos(self) -> None: """Test the create_email_sent_protos function.""" res = mail.send_template('12345', _Recipient('*****@*****.**', 'Primary', 'Recipient'), {'custom': 'var'}, other_recipients=[ _Recipient('*****@*****.**', 'Secondary', 'Recipient'), _Recipient('*****@*****.**', 'Third', 'Recipient'), ]) with mock.patch(mail.now.__name__ + '.get') as mock_now: mock_now.return_value = datetime.datetime(2018, 11, 28, 17, 10) protos = list(mail.create_email_sent_protos(res)) self.assertEqual(3, len(protos), msg=protos) self.assertEqual(3, len({p.mailjet_message_id for p in protos}), msg=protos) self.assertEqual({datetime.datetime(2018, 11, 28, 17, 10)}, {p.sent_at.ToDatetime() for p in protos}, msg=protos)
def send_auth_token(self, user_dict: Dict[str, Any]) -> None: """Sends an email to the user with an auth token so that they can log in.""" user_profile = typing.cast( user_pb2.UserProfile, proto.create_from_mongo(user_dict.get('profile'), user_pb2.UserProfile)) user_id = str(user_dict['_id']) auth_token = create_token(user_id, is_using_timestamp=True) # TODO(pascal): Factorize with campaign.create_logged_url. auth_link = parse.urljoin( flask.request.url, '/?' + parse.urlencode({ 'userId': user_id, 'authToken': auth_token })) template_vars = { 'authLink': auth_link, 'firstName': user_profile.name, } result = mail.send_template('1140080', user_profile, template_vars) if result.status_code != 200: logging.error('Failed to send an email with MailJet:\n %s', result.text) flask.abort(result.status_code)
def test_send_template_not_to_example(self) -> None: """Do not send template to test addresses.""" mail.send_template('12345', _Recipient('REDACTED', 'Primary', 'Recipient'), {'custom': 'var'}) sent_emails = mailjetmock.get_all_sent_messages() self.assertEqual([], sorted(m.recipient['Email'] for m in sent_emails)) mail.send_template( '12345', _Recipient('*****@*****.**', 'Primary', 'Recipient'), {'custom': 'var'}) sent_emails = mailjetmock.get_all_sent_messages() self.assertEqual([], sorted(m.recipient['Email'] for m in sent_emails))
def test_send_template_with_null(self) -> None: """Send null variable should return a 400 error.""" res = mail.send_template( '12345', _Recipient('*****@*****.**', 'Alice', 'NeedsHelp'), {'nullVar': None}) with self.assertRaises(requests.HTTPError): res.raise_for_status()
def test_send_template_multiple_recipients(self) -> None: """Send template to multiple recipients.""" mail.send_template('12345', _Recipient('*****@*****.**', 'Primary', 'Recipient'), {'custom': 'var'}, other_recipients=[ _Recipient('*****@*****.**', 'Secondary', 'Recipient'), _Recipient('*****@*****.**', 'Third', 'Recipient'), ]) sent_emails = mailjetmock.get_all_sent_messages() self.assertEqual(['*****@*****.**', '*****@*****.**', '*****@*****.**'], sorted(m.recipient['Email'] for m in sent_emails)) self.assertEqual({12345}, {m.properties['TemplateID'] for m in sent_emails})
def _send_email(mail_template: str, user_profile: 'mail._Recipient', template_vars: Dict[str, Any]) -> bool: mail_result = mail.send_template(mail_template, user_profile, template_vars) try: mail_result.raise_for_status() except requests.exceptions.HTTPError as error: logging.error('Failed to send an email with MailJet:\n %s', error) return False return True
def test_dry_run(self) -> None: """Test the create_email_sent_protos function.""" res = mail.send_template('12345', _Recipient('*****@*****.**', 'Alice', 'NeedsHelp'), {}, dry_run=True) self.assertEqual(200, res.status_code) res.raise_for_status() self.assertFalse(mail.create_email_sent_proto(res))
def _send_activation_email(user: user_pb2.User, project: project_pb2.Project, database: pymongo_database.Database, base_url: str) -> None: """Send an email to the user just after we have defined their diagnosis.""" if '@' not in user.profile.email: return # Set locale. locale.setlocale(locale.LC_ALL, 'fr_FR.UTF-8') scoring_project = scoring.ScoringProject(project, user, database, now=now.get()) auth_token = parse.quote( auth.create_token(user.user_id, is_using_timestamp=True)) settings_token = parse.quote( auth.create_token(user.user_id, role='settings')) coaching_email_frequency_name = \ user_pb2.EmailFrequency.Name(user.profile.coaching_email_frequency) data = { 'changeEmailSettingsUrl': f'{base_url}/unsubscribe.html?user={user.user_id}&auth={settings_token}&' f'coachingEmailFrequency={coaching_email_frequency_name}&' f'hl={parse.quote(user.profile.locale)}', 'date': now.get().strftime('%d %B %Y'), 'firstName': user.profile.name, 'gender': user_pb2.Gender.Name(user.profile.gender), 'isCoachingEnabled': 'True' if user.profile.coaching_email_frequency and user.profile.coaching_email_frequency != user_pb2.EMAIL_NONE else '', 'loginUrl': f'{base_url}?userId={user.user_id}&authToken={auth_token}', 'ofJob': scoring_project.populate_template('%ofJobName', raise_on_missing_var=True), } # https://app.mailjet.com/template/636862/build response = mail.send_template('636862', user.profile, data) if response.status_code != 200: logging.warning('Error while sending diagnostic email: %s\n%s', response.status_code, response.text)
def send_email_to_user(user, user_id, base_url): """Sends an email to the user to measure the Net Promoter Score.""" # Renew actions for the day if needed. mail_result = mail.send_template( _MAILJET_TEMPLATE_ID, user.profile, { 'baseUrl': base_url, 'firstName': french.cleanup_firstname(user.profile.name), 'npsFormUrl': '{}/api/nps?user={}&token={}&redirect={}'.format( base_url, user_id, auth.create_token(user_id, 'nps'), parse.quote('{}/retours'.format(base_url)), ), }, dry_run=DRY_RUN, ) mail_result.raise_for_status() return mail_result
def send_reset_password_token(self, email): """Sends an email to user with a reset token so that they can reset their password.""" user_dict = self._user_db.user.find_one({'profile.email': email}) if not user_dict: flask.abort( 403, "Nous n'avons pas d'utilisateur avec cet email : {}".format( email)) user_auth_dict = self._user_db.user_auth.find_one( {'_id': user_dict['_id']}) if not user_auth_dict or not user_auth_dict.get('hashedPassword'): flask.abort( 403, 'Utilisez Facebook ou Google pour vous connecter, comme la première fois.' ) hashed_old_password = user_auth_dict.get('hashedPassword') auth_token = _timestamped_hash( int(time.time()), email + str(user_dict['_id']) + hashed_old_password) user_profile = proto.create_from_mongo(user_dict.get('profile'), user_pb2.UserProfile) reset_link = parse.urljoin( flask.request.url, '/?' + parse.urlencode({ 'email': email, 'resetToken': auth_token })) template_vars = { 'resetLink': reset_link, 'firstName': user_profile.name, } result = mail.send_template('71254', user_profile, template_vars, monitoring_category='reset_password') if result.status_code != 200: logging.error('Failed to send an email with MailJet:\n %s', result.text) flask.abort(result.status_code)
def send_reset_password_token(self, email: str) -> None: """Sends an email to user with a reset token so that they can reset their password.""" user_dict = self._user_collection.find_one( {'hashedEmail': hash_user_email(email)}) if not user_dict: flask.abort( 403, f"Nous n'avons pas d'utilisateur avec cet email : {email}") auth_token, unused_email = self._create_reset_token_from_user( user_dict) if not auth_token: flask.abort( 403, 'Utilisez Facebook ou Google pour vous connecter, comme la première fois.' ) user_profile = typing.cast( user_pb2.UserProfile, proto.create_from_mongo(user_dict.get('profile'), user_pb2.UserProfile)) reset_link = parse.urljoin( flask.request.url, '/?' + parse.urlencode({ 'email': email, 'resetToken': auth_token })) template_vars = { 'resetLink': reset_link, 'firstName': user_profile.name, } result = mail.send_template('71254', user_profile, template_vars) if result.status_code != 200: logging.error('Failed to send an email with MailJet:\n %s', result.text) flask.abort(result.status_code)
def _send_activation_email(user, project, database, base_url): """Send an email to the user just after we have defined their diagnosis.""" advice_modules = {a.advice_id: a for a in _advice_modules(database)} advices = [a for a in project.advices if a.advice_id in advice_modules] if not advices: logging.error( # pragma: no-cover 'Weird: the advices that just got created do not exist in DB.' ) # pragma: no-cover return # pragma: no-cover data = { 'baseUrl': base_url, 'projectId': project.project_id, 'firstName': user.profile.name, 'ofProjectTitle': french.maybe_contract_prefix( 'de ', "d'", french.lower_first_letter( french.genderize_job(project.target_job, user.profile.gender))), 'advices': [{ 'adviceId': a.advice_id, 'title': advice_modules[a.advice_id].title } for a in advices], } response = mail.send_template( # https://app.mailjet.com/template/168827/build '168827', user.profile, data, dry_run=not _EMAIL_ACTIVATION_ENABLED) if response.status_code != 200: logging.warning('Error while sending diagnostic email: %s\n%s', response.status_code, response.text)
def blast_campaign(campaign_id, action, registered_from, registered_to, dry_run_email='', user_hash='', email_policy=EmailPolicy( days_since_any_email=2, days_since_same_campaign_unread=0)): """Send a campaign of personalized emails.""" if action == 'send' and auth.SECRET_SALT == auth.FAKE_SECRET_SALT: raise ValueError('Set the prod SECRET_SALT env var before continuing.') this_campaign = campaign.get_campaign(campaign_id) template_id = this_campaign.mailjet_template selected_users = _USER_DB.user.find( dict( this_campaign.mongo_filters, **{ 'profile.email': { '$not': re.compile(r'@example.com$'), '$regex': re.compile(r'@'), }, 'registeredAt': { '$gt': registered_from, '$lt': registered_to, } })) email_count = 0 email_errors = 0 users_processed_count = 0 users_wrong_hash_count = 0 users_stopped_seeking = 0 email_policy_rejections = 0 no_template_vars_count = 0 for user_dict in selected_users: users_processed_count += 1 user_id = user_dict.pop('_id') user = proto.create_from_mongo(user_dict, user_pb2.User) user.user_id = str(user_id) if user_hash and not user.user_id.startswith(user_hash): users_wrong_hash_count += 1 continue # Do not send emails to users who said they have stopped seeking. if any(status.seeking == user_pb2.STOP_SEEKING for status in user.employment_status): users_stopped_seeking += 1 continue if not email_policy.can_send(campaign_id, user.emails_sent): email_policy_rejections += 1 continue template_vars = this_campaign.get_vars(user, _DB) if not template_vars: no_template_vars_count += 1 continue if action == 'list': logging.info('%s %s', user.user_id, user.profile.email) continue if action == 'dry-run': user.profile.email = dry_run_email if action in ('dry-run', 'send'): res = mail.send_template(template_id, user.profile, template_vars, sender_email=this_campaign.sender_email, sender_name=this_campaign.sender_name) logging.info('Email sent to %s', user.profile.email) if action == 'dry-run': try: res.raise_for_status() except requests.exceptions.HTTPError: raise ValueError( 'Could not send email for vars:\n{}'.format(template_vars)) elif res.status_code != 200: logging.warning('Error while sending an email: %d', res.status_code) email_errors += 1 continue sent_response = res.json() message_id = next(iter(sent_response.get('Sent', [])), {}).get('MessageID', 0) if not message_id: logging.warning('Impossible to retrieve the sent email ID:\n%s', sent_response) if action == 'dry-run': return 1 email_sent = user.emails_sent.add() email_sent.sent_at.GetCurrentTime() email_sent.sent_at.nanos = 0 email_sent.mailjet_template = template_id email_sent.campaign_id = campaign_id email_sent.mailjet_message_id = message_id _USER_DB.user.update_one({'_id': user_id}, { '$set': { 'emailsSent': json_format.MessageToDict(user).get('emailsSent', []), } }) email_count += 1 if email_count % 100 == 0: print('{} emails sent ...'.format(email_count)) logging.info('{:d} users processed.'.format(users_processed_count)) if users_wrong_hash_count: logging.info('{:d} users ignored because of hash selection.'.format( users_wrong_hash_count)) logging.info( '{:d} users have stopped seeking.'.format(users_stopped_seeking)) logging.info('{:d} users ignored because of emailing policy.'.format( email_policy_rejections)) logging.info('{:d} users ignored because of no template vars.'.format( no_template_vars_count)) if action == 'send': report.notify_slack( "Report for {} blast: I've sent {:d} emails (and got {:d} " 'errors).'.format(campaign_id, email_count, email_errors)) return email_count
def send_mail( self, campaign_id: str, user: _UserProto, *, database: pymongo.database.Database, users_database: pymongo.database.Database, now: datetime.datetime, action: 'Action' = 'dry-run', dry_run_email: Optional[str] = None, mongo_user_update: Optional[Dict[str, Any]] = None) -> bool: """Send an email for this campaign.""" template_vars = self._get_vars( user, database=database, users_database=users_database, now=now) if not template_vars: return False collection = self._users_collection if action == 'list': user_id = collection.get_id(user) logging.info('%s: %s %s', campaign_id, user_id, collection.get_profile(user).email) return True if action == 'dry-run': collection.get_profile(user).email = dry_run_email or '*****@*****.**' if action == 'ghost': email_sent = user.emails_sent.add() email_sent.sent_at.FromDatetime(now) email_sent.sent_at.nanos = 0 email_sent.subject = get_campaign_subject(self._mailjet_template) or '' else: res = mail.send_template( self._mailjet_template, collection.get_profile(user), template_vars, sender_email=self._sender_email, sender_name=self._sender_name, campaign_id=campaign_id) logging.info('Email sent to %s', collection.get_profile(user).email) res.raise_for_status() maybe_email_sent = mail.create_email_sent_proto(res) if not maybe_email_sent: logging.warning('Impossible to retrieve the sent email ID:\n%s', res.json()) return False if action == 'dry-run': return True email_sent = maybe_email_sent email_sent.mailjet_template = self._mailjet_template email_sent.campaign_id = campaign_id if mongo_user_update and '$push' in mongo_user_update: # pragma: no-cover raise ValueError( f'$push operations are not allowed in mongo_user_update:\n{mongo_user_update}') user_id = collection.get_id(user) if user_id and action != 'ghost': users_database.get_collection(collection.mongo_collection).update_one( {'_id': objectid.ObjectId(user_id)}, dict(mongo_user_update or {}, **{'$push': { 'emailsSent': json_format.MessageToDict(email_sent), }})) # TODO(pascal): Clean that up or make it work in ghost mode. if self._on_email_sent: self._on_email_sent( user, email_sent=email_sent, template_vars=template_vars, database=database, user_database=users_database) return True