Пример #1
0
    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)
Пример #2
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 = 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)
Пример #3
0
    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))
Пример #4
0
    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()
Пример #5
0
    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})
Пример #6
0
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
Пример #7
0
    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))
Пример #8
0
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)
Пример #9
0
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
Пример #10
0
    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)
Пример #11
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:
            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)
Пример #12
0
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)
Пример #13
0
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
Пример #14
0
    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