Exemple #1
0
    def test_cant_send_emails_if_not_properly_configured(self) -> None:
        with override_config(EMAIL_HOST=None):
            with self.assertRaises(ImproperlyConfigured) as e:
                EmailMessage("test_campaign", "Subject", "template")
            self.assertEqual(
                str(e.exception), "Email is not enabled in this instance.",
            )

        with override_config(EMAIL_ENABLED=False):
            with self.assertRaises(ImproperlyConfigured) as e:
                EmailMessage("test_campaign", "Subject", "template")
            self.assertEqual(
                str(e.exception), "Email is not enabled in this instance.",
            )
Exemple #2
0
 def test_cant_send_emails_if_not_properly_configured(self) -> None:
     with self.settings(EMAIL_HOST=None):
         with self.assertRaises(ImproperlyConfigured) as e:
             EmailMessage("Subject", "template")
         self.assertEqual(
             str(e.exception), "Email settings not configured! Set at least the EMAIL_HOST environment variable.",
         )
Exemple #3
0
def send_async_migration_complete_email(migration_key: str, time: str) -> None:

    message = EmailMessage(
        campaign_key=f"async_migration_complete_{migration_key}",
        subject=f"Async migration {migration_key} completed",
        template_name="async_migration_status",
        template_context={
            "migration_status_update":
            f"Async migration {migration_key} completed successfully at {time}."
        },
    )

    send_message_to_all_staff_users(message)
Exemple #4
0
def send_canary_email(user_email: str) -> None:
    message = EmailMessage(
        campaign_key=f"canary_email_{uuid.uuid4()}",
        subject="This is a test email of your PostHog instance",
        template_name="canary_email",
        template_context={"site_url": settings.SITE_URL},
    )
    message.add_recipient(email=user_email)
    message.send()
Exemple #5
0
def send_email_subscription_report(
    email: str,
    subscription: Subscription,
    assets: List[ExportedAsset],
    invite_message: Optional[str] = None,
    total_asset_count: Optional[int] = None,
) -> None:
    utm_tags = f"{UTM_TAGS_BASE}&utm_medium=email"

    inviter = subscription.created_by
    is_invite = invite_message is not None
    self_invite = inviter.email == email

    subject = "Posthog Report"
    invite_summary = None

    resource_info = subscription.resource_info
    if not resource_info:
        raise NotImplementedError(
            "This type of subscription resource is not supported")

    subject = f"PostHog {resource_info.kind} report - {resource_info.name}"
    campaign_key = f"{resource_info.kind.lower()}_subscription_report_{subscription.next_delivery_date.isoformat()}"

    unsubscribe_url = absolute_uri(
        f"/unsubscribe?token={get_unsubscribe_token(subscription, email)}&{utm_tags}"
    )

    if is_invite:
        invite_summary = f"This subscription is { subscription.summary }. The next subscription will be sent on { subscription.next_delivery_date.strftime('%A %B %d, %Y')}"
        if self_invite:
            subject = f"You have been subscribed to a PostHog {resource_info.kind}"
        else:
            subject = f"{inviter.first_name or 'Someone'} subscribed you to a PostHog {resource_info.kind}"
        campaign_key = f"{resource_info.kind.lower()}_subscription_new_{uuid.uuid4()}"

    message = EmailMessage(
        campaign_key=campaign_key,
        subject=subject,
        template_name="subscription_report",
        template_context={
            "images": [x.get_public_content_url() for x in assets],
            "resource_noun": resource_info.kind,
            "resource_name": resource_info.name,
            "resource_url": f"{resource_info.url}?{utm_tags}",
            "subscription_url": f"{subscription.url}?{utm_tags}",
            "unsubscribe_url": unsubscribe_url,
            "inviter": inviter if is_invite else None,
            "self_invite": self_invite,
            "invite_message": invite_message,
            "invite_summary": invite_summary,
            "total_asset_count": total_asset_count,
        },
    )
    message.add_recipient(email=email)
    message.send()
Exemple #6
0
def send_async_migration_errored_email(migration_key: str, time: str,
                                       error: str) -> None:

    message = EmailMessage(
        campaign_key=f"async_migration_error_{migration_key}",
        subject=f"Async migration {migration_key} errored",
        template_name="async_migration_error",
        template_context={
            "migration_key": migration_key,
            "time": time,
            "error": error
        },
    )

    send_message_to_all_staff_users(message)
Exemple #7
0
def send_invite(invite_id: str) -> None:
    campaign_key: str = f"invite_email_{invite_id}"
    invite: OrganizationInvite = OrganizationInvite.objects.select_related(
        "created_by").select_related("organization").get(id=invite_id)
    message = EmailMessage(
        campaign_key=campaign_key,
        subject=
        f"{invite.created_by.first_name} invited you to join {invite.organization.name} on PostHog",
        template_name="invite",
        template_context={"invite": invite},
    )
    message.add_recipient(email=invite.target_email)
    message.send()
Exemple #8
0
    def create(self, validated_data):

        if getattr(settings, "SAML_ENFORCED", False):
            raise serializers.ValidationError(
                "Password reset is disabled because SAML login is enforced.",
                code="saml_enforced")

        if not is_email_available():
            raise serializers.ValidationError(
                "Cannot reset passwords because email is not configured for your instance. Please contact your administrator.",
                code="email_not_available",
            )

        email = validated_data.pop("email")
        try:
            user = User.objects.get(email=email)
        except User.DoesNotExist:
            user = None

        if user:
            token = default_token_generator.make_token(user)

            message = EmailMessage(
                campaign_key=f"password-reset-{user.uuid}-{timezone.now()}",
                subject=f"Reset your PostHog password",
                template_name="password_reset",
                template_context={
                    "preheader":
                    "Please follow the link inside to reset your password.",
                    "link":
                    f"/reset/{user.uuid}/{token}",
                    "cloud":
                    settings.MULTI_TENANCY,
                    "site_url":
                    settings.SITE_URL,
                    "social_providers":
                    list(user.social_auth.values_list("provider", flat=True)),
                },
            )
            message.add_recipient(email)
            message.send()

        # TODO: Limit number of requests for password reset emails

        return True
Exemple #9
0
def send_member_join(invitee_uuid: str, organization_id: str) -> None:
    invitee: User = User.objects.get(uuid=invitee_uuid)
    organization: Organization = Organization.objects.get(id=organization_id)
    campaign_key: str = f"member_join_email_org_{organization_id}_user_{invitee_uuid}"
    message = EmailMessage(
        campaign_key=campaign_key,
        subject=f"{invitee.first_name} joined you on PostHog",
        template_name="member_join",
        template_context={
            "invitee": invitee,
            "organization": organization
        },
    )
    # Don't send this email to the new member themselves
    members_to_email = organization.members.exclude(email=invitee.email)
    if members_to_email:
        for user in members_to_email:
            message.add_recipient(email=user.email, name=user.first_name)
        message.send()
Exemple #10
0
def send_weekly_email_report() -> None:
    """
    Sends the weekly email report to all users in a team.
    """

    if not is_email_available():
        logger.info(
            "Skipping send_weekly_email_report because email is not properly configured"
        )
        return

    period_start, period_end = get_previous_week()

    last_week_start: datetime.datetime = period_start - datetime.timedelta(7)
    last_week_end: datetime.datetime = period_end - datetime.timedelta(7)

    for team in Team.objects.all():

        event_data_set = Event.objects.filter(
            team=team,
            timestamp__gte=period_start,
            timestamp__lte=period_end,
        )

        active_users = PersonDistinctId.objects.filter(
            distinct_id__in=event_data_set.values(
                "distinct_id").distinct(), ).distinct()
        active_users_count: int = active_users.count()

        if active_users_count == 0:
            # TODO: Send an email prompting fix to no active users
            continue

        last_week_users = PersonDistinctId.objects.filter(
            distinct_id__in=Event.objects.filter(
                team=team,
                timestamp__gte=last_week_start,
                timestamp__lte=last_week_end,
            ).values("distinct_id").distinct(), ).distinct()
        last_week_users_count: int = last_week_users.count()

        two_weeks_ago_users = PersonDistinctId.objects.filter(
            distinct_id__in=Event.objects.filter(
                team=team,
                timestamp__gte=last_week_start - datetime.timedelta(7),
                timestamp__lte=last_week_end - datetime.timedelta(7),
            ).values("distinct_id").distinct(),
        ).distinct()  # used to compute delta in churned users
        two_weeks_ago_users_count: int = two_weeks_ago_users.count()

        not_last_week_users = PersonDistinctId.objects.filter(
            pk__in=active_users.difference(last_week_users, ).values_list(
                "pk",
                flat=True,
            ))  # users that were present this week but not last week

        churned_count = last_week_users.difference(active_users).count()
        churned_ratio: Optional[float] = (churned_count /
                                          last_week_users_count if
                                          last_week_users_count > 0 else None)
        last_week_churn_ratio: Optional[float] = (
            two_weeks_ago_users.difference(last_week_users).count() /
            two_weeks_ago_users_count
            if two_weeks_ago_users_count > 0 else None)
        churned_delta: Optional[float] = (
            churned_ratio / last_week_churn_ratio -
            1 if last_week_churn_ratio else None  # type: ignore
        )

        message = EmailMessage(
            f"PostHog weekly report for {period_start.strftime('%b %d, %Y')} to {period_end.strftime('%b %d')}",
            "weekly_report",
            {
                "preheader":
                f"Your PostHog weekly report is ready! Your team had {compact_number(active_users_count)} active users last week! 🎉",
                "team":
                team.name,
                "period_start":
                period_start,
                "period_end":
                period_end,
                "active_users":
                active_users_count,
                "active_users_delta":
                active_users_count / last_week_users_count -
                1 if last_week_users_count > 0 else None,
                "user_distribution": {
                    "new":
                    not_last_week_users.filter(
                        person__created_at__gte=period_start).count() /
                    active_users_count,
                    "retained":
                    active_users.intersection(last_week_users).count() /
                    active_users_count,
                    "resurrected":
                    not_last_week_users.filter(
                        person__created_at__lt=period_start).count() /
                    active_users_count,
                },
                "churned_users": {
                    "abs": churned_count,
                    "ratio": churned_ratio,
                    "delta": churned_delta
                },
            },
        )

        for user in team.organization.members.all():
            # TODO: Skip "unsubscribed" users
            message.add_recipient(user.email, user.first_name)

        # TODO: Schedule retry on failed attempt
        message.send()
Exemple #11
0
def _send_weekly_email_report_for_team(team_id: int) -> None:
    """
    Sends the weekly email report to all users in a team.
    """

    period_start, period_end = get_previous_week()
    last_week_start: datetime.datetime = period_start - datetime.timedelta(7)
    last_week_end: datetime.datetime = period_end - datetime.timedelta(7)

    campaign_key: str = f"weekly_report_for_team_{team_id}_on_{period_start.strftime('%Y-%m-%d')}"

    team = Team.objects.get(pk=team_id)

    event_data_set = Event.objects.filter(
        team=team,
        timestamp__gte=period_start,
        timestamp__lte=period_end,
    )

    active_users = PersonDistinctId.objects.filter(
        distinct_id__in=event_data_set.values(
            "distinct_id").distinct(), ).distinct()
    active_users_count: int = active_users.count()

    if active_users_count == 0:
        # TODO: Send an email prompting fix to no active users
        return

    last_week_users = PersonDistinctId.objects.filter(
        distinct_id__in=Event.objects.filter(
            team=team,
            timestamp__gte=last_week_start,
            timestamp__lte=last_week_end,
        ).values("distinct_id").distinct(), ).distinct()
    last_week_users_count: int = last_week_users.count()

    two_weeks_ago_users = PersonDistinctId.objects.filter(
        distinct_id__in=Event.objects.filter(
            team=team,
            timestamp__gte=last_week_start - datetime.timedelta(7),
            timestamp__lte=last_week_end - datetime.timedelta(7),
        ).values("distinct_id").distinct(),
    ).distinct()  # used to compute delta in churned users
    two_weeks_ago_users_count: int = two_weeks_ago_users.count()

    not_last_week_users = PersonDistinctId.objects.filter(
        pk__in=active_users.difference(last_week_users, ).values_list(
            "pk",
            flat=True,
        ))  # users that were present this week but not last week

    churned_count = last_week_users.difference(active_users).count()
    churned_ratio: Optional[float] = (churned_count / last_week_users_count
                                      if last_week_users_count > 0 else None)
    last_week_churn_ratio: Optional[float] = (
        two_weeks_ago_users.difference(last_week_users).count() /
        two_weeks_ago_users_count if two_weeks_ago_users_count > 0 else None)
    churned_delta: Optional[float] = (
        churned_ratio / last_week_churn_ratio -
        1 if last_week_churn_ratio else None  # type: ignore
    )

    message = EmailMessage(
        campaign_key=campaign_key,
        subject=
        f"PostHog weekly report for {period_start.strftime('%b %d, %Y')} to {period_end.strftime('%b %d')}",
        template_name="weekly_report",
        template_context={
            "preheader":
            f"Your PostHog weekly report is ready! Your team had {compact_number(active_users_count)} active users last week! 🎉",
            "team":
            team.name,
            "period_start":
            period_start,
            "period_end":
            period_end,
            "active_users":
            active_users_count,
            "active_users_delta":
            active_users_count / last_week_users_count -
            1 if last_week_users_count > 0 else None,
            "user_distribution": {
                "new":
                not_last_week_users.filter(
                    person__created_at__gte=period_start).count() /
                active_users_count,
                "retained":
                active_users.intersection(last_week_users).count() /
                active_users_count,
                "resurrected":
                not_last_week_users.filter(
                    person__created_at__lt=period_start).count() /
                active_users_count,
            },
            "churned_users": {
                "abs": churned_count,
                "ratio": churned_ratio,
                "delta": churned_delta
            },
        },
    )

    for user in team.organization.members.all():
        # TODO: Skip "unsubscribed" users
        message.add_recipient(email=user.email, name=user.first_name)

    message.send()
Exemple #12
0
def send_message_to_all_staff_users(message: EmailMessage) -> None:
    for user in User.objects.filter(is_active=True, is_staff=True):
        message.add_recipient(email=user.email, name=user.first_name)

    message.send()