Exemple #1
0
    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})
Exemple #2
0
    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})
Exemple #3
0
    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)
Exemple #4
0
    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))
Exemple #5
0
    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})
Exemple #6
0
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
Exemple #7
0
    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)
Exemple #8
0
    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)
Exemple #9
0
    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)
Exemple #10
0
    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)
Exemple #11
0
    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
Exemple #12
0
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,
        )