def test_does_not_generates_list_ids_for_unregistered_types(self): message = ( MessageBuilder( subject="Test", body="hello world", html_body="<b>hello world</b>", reference=object(), ) .get_built_messages(["*****@*****.**"])[0] .message() ) assert "List-Id" not in message
def generate_fetch_commits_error_email(release, repo, error_message): new_context = { "release": release, "error_message": error_message, "repo": repo } return MessageBuilder( subject="Unable to Fetch Commits", context=new_context, template="sentry/emails/unable-to-fetch-commits.txt", html_template="sentry/emails/unable-to-fetch-commits.html", )
def generate_invalid_identity_email(identity, commit_failure=False): new_context = { "identity": identity, "auth_url": absolute_uri(reverse("socialauth_associate", args=[identity.provider])), "commit_failure": commit_failure, } return MessageBuilder( subject="Unable to Fetch Commits" if commit_failure else "Action Required", context=new_context, template="sentry/emails/identity-invalid.txt", html_template="sentry/emails/identity-invalid.html", )
def post(self, request, organization): """ Email the organization owners asking them to install an integration. ```````````````````````````````````````````````````````````````````` When a non-owner user views integrations in the integrations directory, they lack the ability to install them themselves. POSTing to this API alerts users with permission that there is demand for this integration. :param string providerSlug: Unique string that identifies the integration. :param string providerType: One of: first_party, plugin, sentry_app. :param string message: Optional message from the requester to the owners. """ provider_type = request.data.get("providerType") provider_slug = request.data.get("providerSlug") message_option = request.data.get("message", "").strip() try: provider_name = get_provider_name(provider_type, provider_slug) except RuntimeError as error: return Response({"detail": force_text(error)}, status=400) requester = request.user owners_list = organization.get_owners() # If for some reason the user had permissions all along, silently fail. if requester.id in [user.id for user in owners_list]: return Response({"detail": "User can install integration"}, status=200) msg = MessageBuilder( subject="Your team member requested the %s integration on Sentry" % provider_name, template="sentry/emails/requests/organization-integration.txt", html_template="sentry/emails/requests/organization-integration.html", type="organization.integration.request", context={ "integration_link": get_url(organization, provider_type, provider_slug), "integration_name": provider_name, "message": message_option, "organization_name": organization.name, "requester_name": requester.name or requester.username, "requester_link": absolute_uri( f"/settings/{organization.slug}/members/{requester.id}/" ), "settings_link": absolute_uri( reverse("sentry-organization-settings", args=[organization.slug]) ), }, ) msg.send([user.email for user in owners_list]) return Response(status=201)
def send(self): if not self.should_email(): return participants = self.get_participants() if not participants: return activity = self.activity project = self.project group = self.group context = self.get_base_context() context.update(self.get_context()) template = self.get_template() html_template = self.get_html_template() email_type = self.get_email_type() headers = self.get_headers() for user, reason in participants.items(): if group: context.update({ "reason": GroupSubscriptionReason.descriptions.get( reason, "are subscribed to this issue"), "unsubscribe_link": generate_signed_link( user.id, "sentry-account-email-unsubscribe-issue", kwargs={"issue_id": group.id}, ), }) user_context = self.get_user_context(user) if user_context: user_context.update(context) else: user_context = context msg = MessageBuilder( subject=self.get_subject_with_prefix(), template=template, html_template=html_template, headers=headers, type=email_type, context=user_context, reference=activity, reply_reference=group, ) msg.add_users([user.id], project=project) msg.send_async()
def generate_delete_fail_email(self, error_message): from sentry.utils.email import MessageBuilder new_context = { 'repo': self, 'error_message': error_message, } return MessageBuilder( subject='Unable to Delete Repository', context=new_context, template='sentry/emails/unable-to-delete-repo.txt', html_template='sentry/emails/unable-to-delete-repo.html', )
def call_to_action(self, org: Organization, user: User, member: OrganizationMember): # send invite to setup 2fa email_context = {"url": member.get_invite_link(), "organization": org} subject = "{} {} Mandatory: Enable Two-Factor Authentication".format( options.get("mail.subject-prefix"), org.name.capitalize()) message = MessageBuilder( subject=subject, template="sentry/emails/setup_2fa.txt", html_template="sentry/emails/setup_2fa.html", type="user.setup_2fa", context=email_context, ) message.send_async([member.email])
def build_message(self, context, status, user_id): display = self.status_display[status] return MessageBuilder( subject="[{}] {} - {}".format(context["status"], context["incident_name"], self.project.slug), template="sentry/emails/incidents/trigger.txt", html_template="sentry/emails/incidents/trigger.html", type=f"incident.alert_rule_{display.lower()}", context=context, headers={ "X-SMTPAPI": json.dumps({"category": "metric_alert_email"}) }, )
def send_request_email(self): from sentry.models import OrganizationMember from sentry.utils.email import MessageBuilder user = self.member.user email = user.email organization = self.team.organization context = { "email": email, "name": user.get_display_name(), "organization": organization, "team": self.team, "url": absolute_uri( reverse( "sentry-organization-members-requests", kwargs={"organization_slug": organization.slug}, )), } if self.requester: context.update({"requester": self.requester.get_display_name()}) msg = MessageBuilder( subject="Sentry Access Request", template="sentry/emails/request-team-access.txt", html_template="sentry/emails/request-team-access.html", type="team.access.request", context=context, ) global_roles = [ r.id for r in roles.with_scope("org:write") if r.is_global ] team_roles = [r.id for r in roles.with_scope("team:write")] # find members which are either team scoped or have access to all teams member_list = OrganizationMember.objects.filter( Q(role__in=global_roles) | Q(teams=self.team, role__in=team_roles), organization=self.team.organization, user__isnull=False, ).select_related("user") msg.send_async([m.user.email for m in member_list])
def email_failure(self, message): from sentry.utils.email import MessageBuilder msg = MessageBuilder( subject="Unable to Export Data", context={ "error_message": message, "payload": json.dumps(self.payload, indent=2, sort_keys=True), }, type="organization.export-data", template="sentry/emails/data-export-failure.txt", html_template="sentry/emails/data-export-failure.html", ) msg.send_async([self.user.email]) self.delete()
def send_notification_as_email( notification: ActivityNotification, user: User, context: Mapping[str, Any] ) -> None: msg = MessageBuilder( subject=notification.get_subject_with_prefix(), template=notification.get_template(), html_template=notification.get_html_template(), headers=notification.get_headers(), type=notification.get_email_type(), context=context, reference=notification.activity, reply_reference=notification.group, ) msg.add_users([user.id], project=notification.project) msg.send_async()
def generate_delete_fail_email(self, error_message): from sentry.utils.email import MessageBuilder new_context = { "repo": self, "error_message": error_message, "provider_name": self.get_provider().name, } return MessageBuilder( subject="Unable to Delete Repository Webhooks", context=new_context, template="sentry/emails/unable-to-delete-repo.txt", html_template="sentry/emails/unable-to-delete-repo.html", )
def _send_mail(self, subject, body, html_body=None, project=None, headers=None, fail_silently=False): send_to = self.get_send_to(project) if not send_to: return subject_prefix = self.get_option('subject_prefix', project) or self.subject_prefix msg = MessageBuilder( subject='%s%s' % (subject_prefix, subject), body=body, html_body=html_body, headers=headers, ) msg.send(send_to, fail_silently=fail_silently)
def generate_security_email( account: "User", type: str, actor: "User", ip_address: str, context: Optional[Mapping[str, Any]] = None, current_datetime: Optional[datetime] = None, ) -> MessageBuilder: if current_datetime is None: current_datetime = timezone.now() subject = "Security settings changed" if type == "mfa-removed": assert "authenticator" in context template = "sentry/emails/mfa-removed.txt" html_template = "sentry/emails/mfa-removed.html" elif type == "mfa-added": assert "authenticator" in context template = "sentry/emails/mfa-added.txt" html_template = "sentry/emails/mfa-added.html" elif type == "password-changed": template = "sentry/emails/password-changed.txt" html_template = "sentry/emails/password-changed.html" elif type == "recovery-codes-regenerated": template = "sentry/emails/recovery-codes-regenerated.txt" html_template = "sentry/emails/recovery-codes-regenerated.html" elif type == "api-token-generated": template = "sentry/emails/api-token-generated.txt" html_template = "sentry/emails/api-token-generated.html" else: raise ValueError(f"unknown type: {type}") new_context = { "account": account, "actor": actor, "ip_address": ip_address, "datetime": current_datetime, } if context: new_context.update(context) return MessageBuilder( subject=subject, context=new_context, template=template, html_template=html_template, type=type, )
def send_notification(self): from sentry.utils.email import MessageBuilder, group_id_to_email if not self.group_id: return if self.type not in (Activity.NOTE, Activity.ASSIGNED): return send_to = self.get_recipients() if not send_to: return author = self.user.first_name or self.user.username subject_prefix = self.project.get_option('subject_prefix', settings.EMAIL_SUBJECT_PREFIX) if subject_prefix: subject_prefix = subject_prefix.rstrip() + ' ' subject = '%s%s' % (subject_prefix, self.group.get_email_subject()) context = { 'data': self.data, 'author': author, 'group': self.group, 'link': self.group.get_absolute_url(), } headers = { 'X-Sentry-Reply-To': group_id_to_email(self.group.id), } template_name = self.get_type_display() msg = MessageBuilder( subject=subject, context=context, template='sentry/emails/activity/{}.txt'.format(template_name), html_template='sentry/emails/activity/{}.html'.format( template_name), headers=headers, reference=self, reply_reference=self.group, ) msg.add_users(send_to, project=self.project) msg.send_async()
def send_request_email(self): from sentry.models import OrganizationMember from sentry.utils.email import MessageBuilder user = self.member.user email = user.email organization = self.team.organization context = { 'email': email, 'name': user.get_display_name(), 'organization': organization, 'team': self.team, 'url': absolute_uri( reverse('sentry-organization-members', kwargs={ 'organization_slug': organization.slug, }) + '?ref=access-requests'), } msg = MessageBuilder( subject='Sentry Access Request', template='sentry/emails/request-team-access.txt', html_template='sentry/emails/request-team-access.html', context=context, ) roles_capable = [r.id for r in roles.with_scope('team:write')] non_global_roles = [ r for r in roles_capable if not roles.get(r).is_global or roles.get(r).has_scope('member:write') ] # find members which are either team scoped or have access to all teams member_list = OrganizationMember.objects.filter( Q(role__in=non_global_roles) | Q(teams=self.team, role__in=roles_capable), organization=self.team.organization, user__isnull=False, ).select_related('user') msg.send_async([m.user.email for m in member_list])
def _remove_2fa_non_compliant_member(member, org, actor=None, actor_key=None, ip_address=None): user = member.user logging_data = { 'organization_id': org.id, 'user_id': user.id, 'member_id': member.id } try: member.email = member.get_email() member.user = None member.save() except (AssertionError, IntegrityError): logger.warning('Could not remove 2FA noncompliant user from org', extra=logging_data) else: logger.info('2FA noncompliant user removed from org', extra=logging_data) AuditLogEntry.objects.create( actor=actor, actor_key=actor_key, ip_address=ip_address, event=AuditLogEntryEvent.MEMBER_PENDING, data=member.get_audit_log_data(), organization=org, target_object=org.id, target_user=user, ) # send invite to setup 2fa email_context = {'url': member.get_invite_link(), 'organization': org} subject = u'{} {} Mandatory: Enable Two-Factor Authentication'.format( options.get('mail.subject-prefix'), org.name.capitalize(), ) message = MessageBuilder( subject=subject, template='sentry/emails/setup_2fa.txt', html_template='sentry/emails/setup_2fa.html', type='user.setup_2fa', context=email_context, ) message.send_async([member.email])
def email_failure(self, message): from sentry.utils.email import MessageBuilder msg = MessageBuilder( subject="We couldn't export your data.", context={ "creation": self.format_date(self.date_added), "error_message": message, "payload": json.dumps(self.payload, indent=2, sort_keys=True), }, type="organization.export-data", template="sentry/emails/data-export-failure.txt", html_template="sentry/emails/data-export-failure.html", ) msg.send_async([self.user.email]) metrics.incr("dataexport.end", instance="failure") self.delete()
def generate_invalid_identity_email(identity, commit_failure=False): new_context = { 'identity': identity, 'auth_url': absolute_uri(reverse('socialauth_associate', args=[identity.provider])), 'commit_failure': commit_failure, } return MessageBuilder( subject='Action Required', context=new_context, template='sentry/emails/identity-invalid.txt', html_template='sentry/emails/identity-invalid.html', )
def send_recover_mail(self): from sentry.utils.email import MessageBuilder context = { 'user': self.user, 'domain': urlparse(settings.SENTRY_URL_PREFIX).hostname, 'url': absolute_uri(reverse( 'sentry-account-recover-confirm', args=[self.user.id, self.hash] )), } msg = MessageBuilder( subject='%sPassword Recovery' % (settings.EMAIL_SUBJECT_PREFIX,), template='sentry/emails/recover_account.txt', context=context, ) msg.send_async([self.user.email])
def send(self): if not self.should_email(): return users = self.get_participants() if not users: return activity = self.activity project = self.project group = self.group context = self.get_base_context() context.update(self.get_context()) subject_prefix = self._get_subject_prefix() subject = (u'{}{}'.format( subject_prefix, self.get_subject(), )).encode('utf-8') template = self.get_template() html_template = self.get_html_template() email_type = self.get_email_type() headers = self.get_headers() for user in users: if group: context['unsubscribe_link'] = generate_signed_link( user.id, 'sentry-account-email-unsubscribe-issue', kwargs={'issue_id': group.id}, ) msg = MessageBuilder( subject=subject, template=template, html_template=html_template, headers=headers, type=email_type, context=context, reference=activity, reply_reference=group, ) msg.add_users([user.id], project=project) msg.send_async()
def send_sso_link_email(self): from sentry.utils.email import MessageBuilder context = { 'email': self.email, 'organization_name': self.organization.name, 'url': absolute_uri(reverse('sentry-auth-organization', kwargs={ 'organization_slug': self.organization.slug, })), } msg = MessageBuilder( subject='Action Required for %s' % (self.organization.name,), template='sentry/emails/auth-link-identity.txt', html_template='sentry/emails/auth-link-identity.html', context=context, ) msg.send_async([self.get_email()])
def _send_mail(self, subject, template=None, html_template=None, body=None, project=None, headers=None, context=None, fail_silently=False): send_to = self.get_send_to(project) if not send_to: return subject_prefix = self.get_option('subject_prefix', project) or self.subject_prefix msg = MessageBuilder( subject='%s%s' % (subject_prefix, subject), template=template, html_template=html_template, body=body, headers=headers, context=context, ) msg.add_users(send_to, project=project) return msg.send(fail_silently=fail_silently)
def send_recover_mail(self): from sentry import options from sentry.http import get_server_hostname from sentry.utils.email import MessageBuilder context = { 'user': self.user, 'domain': get_server_hostname(), 'url': self.get_absolute_url(), } msg = MessageBuilder( subject='%sPassword Recovery' % (options.get('mail.subject-prefix'), ), template='sentry/emails/recover_account.txt', html_template='sentry/emails/recover_account.html', type='user.password_recovery', context=context, ) msg.send_async([self.user.email])
def test_explicit_reply_to(self): msg = MessageBuilder( subject='Test', body='hello world', html_body='<b>hello world</b>', headers={'X-Sentry-Reply-To': '*****@*****.**'}, ) msg.send(['*****@*****.**']) assert len(mail.outbox) == 1 out = mail.outbox[0] assert out.to == ['*****@*****.**'] assert out.subject == 'Test' assert out.extra_headers['Reply-To'] == '*****@*****.**' assert out.body == 'hello world' assert len(out.alternatives) == 1 assert out.alternatives[0] == ( '<!DOCTYPE html>\n<html><body><b>hello world</b></body></html>', 'text/html', )
def send_sso_link_email(self, actor, provider): from sentry.utils.email import MessageBuilder link_args = {"organization_slug": self.organization.slug} context = { "organization": self.organization, "actor": actor, "provider": provider, "url": absolute_uri(reverse("sentry-auth-organization", kwargs=link_args)), } msg = MessageBuilder( subject="Action Required for %s" % (self.organization.name,), template="sentry/emails/auth-link-identity.txt", html_template="sentry/emails/auth-link-identity.html", type="organization.auth_link", context=context, ) msg.send_async([self.get_email()])
def send_delete_confirmation(self, audit_log_entry, countdown): from sentry import options from sentry.utils.email import MessageBuilder owners = self.get_owners() context = { "organization": self, "audit_log_entry": audit_log_entry, "eta": timezone.now() + timedelta(seconds=countdown), "url": absolute_uri(reverse("sentry-restore-organization", args=[self.slug])), } MessageBuilder( subject="{}Organization Queued for Deletion".format(options.get("mail.subject-prefix")), template="sentry/emails/org_delete_confirm.txt", html_template="sentry/emails/org_delete_confirm.html", type="org.confirm_delete", context=context, ).send_async([o.email for o in owners])
def test_raw_content(self): msg = MessageBuilder( subject='Test', body='hello world', html_body='<b>hello world</b>', headers={'X-Test': 'foo'}, ) msg.send(['*****@*****.**']) assert len(mail.outbox) == 1 out = mail.outbox[0] assert out.to == ['*****@*****.**'] assert out.subject == 'Test' assert out.extra_headers['X-Test'] == 'foo' assert out.body == 'hello world' assert len(out.alternatives) == 1 assert out.alternatives[0] == ( '<!DOCTYPE html>\n<html><body><b>hello world</b></body></html>', 'text/html', )
def send_sso_link_email(self, actor, provider): from sentry.utils.email import MessageBuilder link_args = {'organization_slug': self.organization.slug} context = { 'organization': self.organization, 'actor': actor, 'provider': provider, 'url': absolute_uri(reverse('sentry-auth-organization', kwargs=link_args)), } msg = MessageBuilder( subject='Action Required for %s' % (self.organization.name, ), template='sentry/emails/auth-link-identity.txt', html_template='sentry/emails/auth-link-identity.html', type='organization.auth_link', context=context, ) msg.send_async([self.get_email()])
def generate_security_email(account, type, actor, ip_address, context=None, current_datetime=None): if current_datetime is None: current_datetime = timezone.now() subject = 'Security settings changed' if type == 'mfa-removed': assert 'authenticator' in context template = 'sentry/emails/mfa-removed.txt' html_template = 'sentry/emails/mfa-removed.html' elif type == 'mfa-added': assert 'authenticator' in context template = 'sentry/emails/mfa-added.txt' html_template = 'sentry/emails/mfa-added.html' elif type == 'password-changed': template = 'sentry/emails/password-changed.txt' html_template = 'sentry/emails/password-changed.html' elif type == 'recovery-codes-regenerated': template = 'sentry/emails/recovery-codes-regenerated.txt' html_template = 'sentry/emails/recovery-codes-regenerated.html' elif type == 'api-token-generated': template = 'sentry/emails/api-token-generated.txt' html_template = 'sentry/emails/api-token-generated.html' else: raise ValueError(u'unknown type: {}'.format(type)) new_context = { 'account': account, 'actor': actor, 'ip_address': ip_address, 'datetime': current_datetime, } if context: new_context.update(context) return MessageBuilder( subject=subject, context=new_context, template=template, html_template=html_template, type=type )