def test_send_template_with_null(self) -> None: """Send null variable should return an error.""" with self.assertRaises(ValueError): mail_send.send_template( 'imt', _Recipient('*****@*****.**', 'Alice', 'NeedsHelp', ''), {'nullVar': None})
def test_send_template_fallback_locale(self) -> None: """Choose the relevant mailjet template for other locales, using fallback.""" mail_send.send_template( 'imt', _Recipient('*****@*****.**', 'Primary', 'Recipient', 'en_UK'), {'custom': 'var'}) sent_emails = mailjetmock.get_all_sent_messages() self.assertEqual(['*****@*****.**'], [m.recipient['Email'] for m in sent_emails]) self.assertEqual({98765}, {m.properties['TemplateID'] for m in sent_emails})
def test_create_email_sent_protos(self) -> None: """Test the create_email_sent_protos function.""" res = mail_send.send_template('imt', _Recipient('*****@*****.**', 'Primary', 'Recipient', ''), {'custom': 'var'}, other_recipients=[ _Recipient('*****@*****.**', 'Secondary', 'Recipient', ''), _Recipient('*****@*****.**', 'Third', 'Recipient', ''), ]) with nowmock.patch() as mock_now: mock_now.return_value = datetime.datetime(2018, 11, 28, 17, 10) protos = list(mail_send.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 test_send_template_not_to_example(self) -> None: """Do not send template to test addresses.""" mail_send.send_template( 'imt', _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.send_template( 'imt', _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_multiple_recipients(self) -> None: """Send template to multiple recipients.""" mail_send.send_template('imt', _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(campaign_id: mailjet_templates.Id, user_profile: 'mail_send._Recipient', template_vars: dict[str, Any]) -> bool: mail_result = mail_send.send_template(campaign_id, user_profile, template_vars, options={ 'TrackOpens': 'disabled', 'TrackClicks': 'disabled', }) 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 dry_run mode.""" res = mail_send.send_template('imt', _Recipient('*****@*****.**', 'Alice', 'NeedsHelp', ''), {}, dry_run=True) self.assertEqual(200, res.status_code) res.raise_for_status() email_sent_proto = mail_send.create_email_sent_proto(res) self.assertTrue(email_sent_proto) assert email_sent_proto self.assertFalse(email_sent_proto.mailjet_message_id) sent_emails = mailjetmock.get_all_sent_messages() self.assertFalse(sent_emails)
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 = proto.create_from_mongo(user_dict.get('profile'), user_profile_pb2.UserProfile) user_id = str(user_dict['_id']) auth_link = token.create_logged_url(user_id) template_vars = { 'authLink': auth_link, 'firstname': user_profile.name, 'productName': product.bob.name, 'productLogoUrl': product.bob.get_config('productLogoUrl', ''), } # TODO(cyrille): Create a static Campaign object and use it. result = mail_send.send_template('send-auth-token', 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_update_confirmation(self, user_dict: dict[str, Any]) -> None: """Sends an email to the user that confirms password change.""" user_id = str(user_dict['_id']) if not user_id: return auth_link = token.create_logged_url(user_id) reset_link = self._get_reset_password_link(user_dict) if not reset_link or not auth_link: return user = proto.create_from_mongo(user_dict.copy(), user_pb2.User) template_vars = dict(campaign.get_default_coaching_email_vars(user), authLink=auth_link, resetPwdLink=reset_link) # TODO(cyrille): Create a static Campaign object and use it. result = mail_send.send_template('send-pwd-update-confirmation', 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_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: # No user with this email address, however we don't want to tell that to a potential # attacker. return reset_token, unused_email = self._create_reset_token_from_user( user_dict) # User is a guest and/or doesn't have a password so we send them a email to login. if not reset_token: self.send_auth_token(user_dict) return user_profile = proto.create_from_mongo(user_dict.get('profile'), user_profile_pb2.UserProfile) reset_link = self._get_reset_password_link(user_dict) if not reset_link: return template_vars = { 'firstname': user_profile.name, 'productName': product.bob.name, 'productLogoUrl': product.bob.get_config('productLogoUrl', ''), 'resetLink': reset_link, } # TODO(cyrille): Create a static Campaign object and use it. result = mail_send.send_template('reset-password', 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_mail( self, user: user_pb2.User, *, database: mongo.NoPiiMongoDatabase, users_database: Optional[mongo.UsersDatabase] = None, eval_database: Optional[mongo.NoPiiMongoDatabase] = None, now: datetime.datetime, action: 'Action' = 'dry-run', dry_run_email: Optional[str] = None, mongo_user_update: Optional[dict[str, Any]] = None, should_log_errors: bool = False ) -> Union[bool, email_pb2.EmailSent]: """Send an email for this campaign.""" with set_time_locale(_LOCALE_MAP.get(user.profile.locale or 'fr')): template_vars = self.get_vars(user, should_log_errors=should_log_errors, database=database, now=now) if not template_vars: return False user_profile = user.profile if action == 'list': user_id = user.user_id logging.info('%s: %s %s', self.id, user_id, user_profile.email) return True if action == 'dry-run': user_profile.email = dry_run_email or '*****@*****.**' logging.info('Template vars:\n%s', template_vars) if action == 'ghost': email_sent = user.emails_sent.add() common_proto.set_date_now(email_sent.sent_at, now) else: res = mail_send.send_template( self.id, user_profile, template_vars, sender_email=self._sender_email, sender_name=template_vars['senderName']) logging.info( 'Email sent to %s', user_profile.email if action == 'dry-run' else user.user_id) res.raise_for_status() maybe_email_sent = mail_send.create_email_sent_proto(res) if not maybe_email_sent: logging.warning( 'Impossible to retrieve the sent email ID:\n' 'Response (%d):\n%s\nUser: %s\nCampaign: %s', res.status_code, res.json(), user.user_id, self.id) return False if action == 'dry-run': return maybe_email_sent email_sent = maybe_email_sent campaign_subject = get_campaign_subject(self.id) try: i18n_campaign_subject = i18n.translate_string( campaign_subject, user_profile.locale) except i18n.TranslationMissingException: i18n_campaign_subject = campaign_subject email_sent.subject = mustache.instantiate(i18n_campaign_subject, template_vars) email_sent.mailjet_template = str( mailjet_templates.MAP[self.id]['mailjetTemplate']) email_sent.is_coaching = self.is_coaching email_sent.campaign_id = self.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 = user.user_id if not user_id or action == 'ghost' or not users_database: return email_sent users_database.user.update_one( {'_id': objectid.ObjectId(user_id)}, (mongo_user_update or {}) | {'$push': { 'emailsSent': json_format.MessageToDict(email_sent), }}) if eval_database: try: eval_database.sent_emails.update_one( {'_id': self.id}, {'$set': { 'lastSent': proto.datetime_to_json_string(now) }}, upsert=True) except pymongo.errors.OperationFailure: # We ignore this error silently: it's probably due to the base not being writeable # (which is the case in our demo servers). And the whole purpose of this update is # to update the monitoring info: if it fails, then the human being checking the # monitoring data will be warned that something is wrong as the data wasn't updated. pass return email_sent
def main(string_args: Optional[list[str]] = None) -> None: """Email Radar counselors.""" parser = argparse.ArgumentParser( description='Export Radar documents to ElasticSearch for Kibana.') parser.add_argument( '--ignore-before', type=str, help= 'If the typeform data contains unrelated old photos, ignore the photos before this ' 'date (2021-03-01 format).') parser.add_argument( '--months-since-last-photo', type=int, default=2, help='Minimum number of months since last photo to send an email.') parser.add_argument('--dry-run', action='store_true', help='Do not send any emails') args = parser.parse_args(string_args) logging.basicConfig(level='INFO' if args.dry_run else 'WARNING') campaign_start_at = now.get() - datetime.timedelta( days=30 * args.months_since_last_photo) ignore_before = datetime.datetime.strptime(args.ignore_before, '%Y-%m-%d') \ if args.ignore_before else None photos_per_young: dict[ str, list[typeform_pb2.Photo]] = collections.defaultdict(list) for photo in typeform.iterate_results(): if ignore_before and photo.submitted_at.ToDatetime() < ignore_before: continue if not photo.hidden.dossier_id: logging.warning('Photo with no dossier ID.') continue photos_per_young[photo.hidden.dossier_id].append(photo) youngs_per_counselor: dict[ str, list[typeform_pb2.Photo]] = collections.defaultdict(list) for dossier_id, photos in photos_per_young.items(): latest_photo_at = max(photo.submitted_at.ToDatetime() for photo in photos) if latest_photo_at >= campaign_start_at: # we have a recent photo. continue try: counselor_email = next(photo.hidden.counselor_email for photo in photos if photo.hidden.counselor_email) except StopIteration: logging.warning('We have no counselor email address for "%s".', dossier_id) continue if '@' not in counselor_email: logging.warning('Wrong email address for counselor: %s', counselor_email) continue latest_photo = typeform_pb2.Photo() latest_photo.hidden.dossier_id = dossier_id latest_photo.submitted_at.FromDatetime(latest_photo_at) youngs_per_counselor[counselor_email].append(latest_photo) for counselor_email, photos in youngs_per_counselor.items(): dossier_ids = [photo.hidden.dossier_id for photo in photos] latest_photo_at = max(photo.submitted_at.ToDatetime() for photo in photos) latest_photo_str = latest_photo_at.strftime('%Y-%m-%d') if args.dry_run: logging.info('Sending an email to %s for %s (latest photo on %s)', counselor_email, dossier_ids, latest_photo_str) continue mail_send.send_template( # TODO(pascal): Fix the interface of this call. campaign_id=typing.cast(mail_send.mailjet_templates.Id, 'open-session'), recipient=_Counselor( email=counselor_email, name='', last_name='', locale='fr', ), template_vars={ 'dateDeLaPhotoPrecedente': latest_photo_str, 'idsJeunes': dossier_ids, 'prenom': '', }, sender_email='*****@*****.**', sender_name='Équipe MILORizons', # TODO(pascal): Get from mailjet.json. template_id=2974499, )