Documentation
¶
Overview ¶
Package notification is the application half of the Notification context: the consumer that turns events into records, and the delivery that turns a record into a message.
Neither is a use case, and both are deliberately absent from the catalogue in domain-model.md §5. The catalogue is the list of things a person, an agent or a rule can ask for (arc42 §4), and "notify everybody about this event, now" is not one of them - the way to influence what is sent is the preference, not a call. Registering them would put a "send email to everybody" action on three channels, which is a button nobody should be given.
Index ¶
- Constants
- type AccessReader
- type AddressLink
- type AddressMinter
- type DeliverNotification
- type Entries
- type GrantReader
- type Members
- type OpeningNotice
- type PlaceReader
- type PrivateReach
- type ProviderFinder
- type Queue
- type RecordEmergencyAccess
- type RecordInvitation
- type RecordNotifications
- type RecordPasswordOpening
- type RecordPrivateHubTrashed
- type RecordReminder
- type RecordRetentionWarning
- type RecordRoleGranted
- type RecordRuleDisabled
- type RecordWayInChanged
- type RecordWebhookDisabled
- type RedemptionMinter
- type ResetLink
- type ResetMinter
- type RetentionWarning
- type RuleReader
- type SendAddressConfirmation
- type SendPasswordOpening
- type SendPasswordReset
- type SendWayInChanged
- type Signals
- type SubscriptionReader
- type WayInNotice
- type WorkspaceReader
Constants ¶
const ( OpeningEventOpened = "OPENED" OpeningEventClosed = "CLOSED" )
The two events a notice is about.
const ConsumerName = "notification"
ConsumerName is how this subscriber is known in the deduplication and in the logs.
A constant, and stable across versions: renaming it makes every event it has already seen look new, which for this subscriber means sending every notification of the retention window again (core/port/eventbus).
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type AccessReader ¶
type AccessReader interface {
Find(ctx context.Context, id shared.ID) (work.EmergencyAccess, string, error)
}
AccessReader answers an emergency access with its hub's name - the work repository's Find.
type AddressLink ¶
AddressLink is what the minter answers; an empty address is "nothing to send".
type AddressMinter ¶
type AddressMinter interface {
MintAddressLink(ctx context.Context, tenantID, requestID shared.ID) (AddressLink, error)
}
AddressMinter is the identity service's seam: one method and one value, so this package points at nothing the identity service owns.
type DeliverNotification ¶
type DeliverNotification struct {
Notifications repository.Notifications
Preferences repository.Preferences
Accounts identityrepo.Accounts
Items Entries
// Rules and Subscriptions name the subjects that are not an entry: the rule the
// message says was switched off, the subscription it says is no longer called. Optional -
// nil leaves the title out, which is the withheld sentence rather than a wrong one.
Rules RuleReader
Subscriptions SubscriptionReader
// Accesses names the emergency access a member is told of. Optional like the two above: nil
// leaves the hub, the opener's words and the times out, which the withheld sentence survives.
Accesses AccessReader
// Grants and Places name the role somebody was given: the grant, and the hub or collection it
// is on (an entry is read through Items). Grants nil leaves the role out of the message - the
// membership's withheld sentence; Places nil leaves the place's name out.
Grants GrantReader
Places PlaceReader
Mail mail.Sender
Renderer i18n.Renderer
UnitOfWork persistence.UnitOfWork
Clock clock.Clock
// Workspaces answers the workspace's own default language, the second link of the chain a
// recipient's locale is resolved down (i18n-l10n.md §2) - an invited person has not chosen
// one yet, by definition, and the invitation is the first thing they read. Optional: nil
// skips to the installation's default.
Workspaces WorkspaceReader
// FallbackLocale is the installation's default, the last link of the same chain.
FallbackLocale string
// BaseURL is where this installation lives, so an email can carry a link somebody can click. A
// relative path in an email is a dead link.
BaseURL string
// Redemptions mints the credential an invitation mail carries: minted here, at
// delivery, so the queue's payload stays identifiers-only and the plaintext exists exactly
// once, in the message on its way out. Nil composes the plain link, which is what an
// installation without the sign-in flow sent before the flow existed.
Redemptions RedemptionMinter
// Signals is the observability slice. Optional, like everywhere else in this package.
Signals Signals
}
DeliverNotification sends one notification.
It owns its transactions, because it cannot live inside one: rendering is cheap and sending is not, and a transaction held open across an SMTP conversation is what observability-reliability.md §8 forbids. Three parts, in the order the media reconciliation uses for the same reason:
- Read what the message is about - the record, the recipient, the entry - in one transaction.
- Render and send, outside any transaction.
- Write the outcome back, in a transaction of its own.
Safe to run twice, which is what lets it declare itself detached (queue.Detached), and the second run is not free of consequence: a process that died between the send and the write-back leaves a message sent and a record that still says pending, and the retry sends it again. That is the honest trade of at-least-once for email, and it is the right side of it - a duplicate notification is a nuisance, and a lost one is the thing somebody needed to know.
func (DeliverNotification) Execute ¶
func (d DeliverNotification) Execute( ctx context.Context, tenantID, notificationID shared.ID, lastAttempt bool, ) error
Execute sends the notification the job names, and reports whether it worked.
`lastAttempt` is the queue's judgement rather than this service's: the attempt budget lives on the job, and a record that counted its own would be a second budget disagreeing with the first.
type Entries ¶
Entries and Members are the slices of the work repositories this package reads, and they are slices for the reason Queue is: a consumer that held the whole item repository could complete an entry, move it, or write its children, and none of that is anything a notification does. What is declared here is the entire reach of the notification path into the work management context - one read of an entry, one read of its member list.
type GrantReader ¶
GrantReader answers a membership by its identifier - the identity repository's Find.
type OpeningNotice ¶
OpeningNotice is one queued notice, as the job carries it.
type PlaceReader ¶
PlaceReader answers a hub or a collection by its identifier - the container repository's Find.
type PrivateReach ¶
type PrivateReach interface {
Hidden(ctx context.Context, actor appshared.ActorContext, path []identity.Scope) (bool, error)
}
PrivateReach is the authoriser's question about a private hub, asked about each person a notification would go to (access.Service.Hidden).
type ProviderFinder ¶
type ProviderFinder interface {
Find(ctx context.Context, id shared.ID) (identity.IdentityProvider, error)
}
ProviderFinder answers whether a provider still exists, which is all a disconnection's notice asks of it.
type Queue ¶
Queue is the slice of the job queue this package needs. Narrow rather than the whole port, so that what a consumer can do to the queue is visible in one line: it can ask for work, and it cannot claim, complete or fail anything.
type RecordEmergencyAccess ¶
type RecordEmergencyAccess struct {
Notifications repository.Notifications
Accounts identityrepo.Accounts
Preferences repository.Preferences
Jobs Queue
Clock clock.Clock
IDs clock.IDGenerator
}
RecordEmergencyAccess tells every member of a private hub that the workspace's owner opened it (ADR-0073 §4).
It runs inside the transaction that opens the hub, so the access and every member's record commit together or not at all: nobody reads the hub before its members are told. A record is written for every member whatever happens to the mail - suppressed for somebody without an address - because the record is also what the web app shows them (identity.md §22).
The category is never switched off; the preference still decides whether the mail names the hub. Delivered under its own job kind (queue.KindEmergencyAccessEmail).
func (RecordEmergencyAccess) EmergencyAccessOpened ¶
func (r RecordEmergencyAccess) EmergencyAccessOpened( ctx context.Context, access work.EmergencyAccess, members []shared.ID, ) error
EmergencyAccessOpened writes one record per member and queues the deliveries.
type RecordInvitation ¶
type RecordInvitation struct {
Notifications repository.Notifications
Accounts identityrepo.Accounts
Jobs Queue
Clock clock.Clock
IDs clock.IDGenerator
Signals Signals
}
RecordInvitation turns the queued invitation job into a message.
An invitation is queued in the transaction that creates the account, so that the seat and the message exist together or neither does. This is the other end of that queue - and it is deliberately not a second delivery path. The job writes a record and queues the same `notification.deliver` job every other notification uses, so there is one place that renders an email and one place that sends it.
It runs inside the runner's transaction, which is what makes it exactly-once: the record, the delivery job and the invitation job's own completion commit together (core/port/queue). That is why this handler is not detached and the delivery is.
func (RecordInvitation) Execute ¶
func (r RecordInvitation) Execute( ctx context.Context, tenantID, accountID, invitedBy shared.ID, ) error
Execute writes the invitation's record and queues its delivery.
The preference is not consulted, and there is no branch here that could consult it: the invitation is the one category no preference may switch off (domain.Category.Suppressible), and the setting that would switch it off sits behind the door this message unlocks.
type RecordNotifications ¶
type RecordNotifications struct {
Notifications repository.Notifications
Preferences repository.Preferences
Accounts identityrepo.Accounts
Items Entries
ItemMembers Members
Jobs Queue
Clock clock.Clock
IDs clock.IDGenerator
// Signals is the observability slice. Optional: the consumer runs without it, which is what
// keeps a metrics adapter from being a dependency of the notification path.
Signals Signals
// Reach drops a recipient who does not reach the private hub the entry is in - a former
// assignee, somebody on its member list who has since left the hub (D6, ADR-0073 §1): no record,
// no mail. Nil only in a build that has no private hubs to keep.
Reach PrivateReach
}
RecordNotifications is the outbox consumer: it decides who is to be told about an event and writes one record per person.
It runs inside the dispatcher's transaction, which is the whole reliability argument (ADR-0007): the records it writes, the delivery jobs it queues and the consumption record that says this event was handled all commit together. A process that dies halfway leaves none of them, and the event is handed over again.
It reaches nothing outside the database, and that is the rule rather than an accident. The email is a job, because an SMTP server inside a transaction holds a database connection for as long as somebody else's machine feels like taking (observability-reliability.md §8) - and because the comment that caused the notification must commit whether or not anybody can be told about it.
func (RecordNotifications) Deliver ¶
Deliver writes one record per person the event concerns.
An error stops the dispatcher's whole batch, which is why the only errors it produces are real ones: a recipient whose account has gone is not an error but a record nobody can be sent, and a preference that says no is not an error but a record that says why.
func (RecordNotifications) Name ¶
func (r RecordNotifications) Name() string
Name identifies the subscriber in the deduplication and in the logs.
func (RecordNotifications) Wants ¶
func (r RecordNotifications) Wants(eventType event.Type) bool
Wants reports whether this event concerns anybody.
The three in `categories`. An invitation is not here, because it is not an event: it is queued in the transaction that creates the account (RecordInvitation), rather than announced by an event invented for a message that already has a delivery path.
type RecordPasswordOpening ¶
type RecordPasswordOpening struct {
Memberships identityrepo.Memberships
Jobs Queue
}
RecordPasswordOpening queues the notices. It implements the control plane's OpeningNotices.
func (RecordPasswordOpening) PasswordClosed ¶
func (r RecordPasswordOpening) PasswordClosed( ctx context.Context, tenantID shared.ID, ended string, ) error
PasswordClosed tells every administrator that the opening ended, and how.
func (RecordPasswordOpening) PasswordOpened ¶
func (r RecordPasswordOpening) PasswordOpened( ctx context.Context, tenantID shared.ID, until time.Time, ) error
PasswordOpened tells every administrator that the password is open until `until`.
type RecordPrivateHubTrashed ¶
type RecordPrivateHubTrashed struct {
Notifications repository.Notifications
Accounts identityrepo.Accounts
Preferences repository.Preferences
Jobs Queue
Clock clock.Clock
IDs clock.IDGenerator
}
RecordPrivateHubTrashed tells the workspace's owners that a private hub lost its last member and went to the trash (ADR-0073 §5).
A membership message about nothing: no entry, no hub and no person. The hub's name is for its members only, and there are none left; the person may have just been erased by the very act that left the hub empty. What the owners can act on is that it is there, and the administration's list of private hubs shows it with its size and the day it goes.
The ordinary path otherwise: the preference is honoured, the send is a job. It runs inside the caller's transaction, so the message and the deletion it reports commit together.
func (RecordPrivateHubTrashed) PrivateHubTrashed ¶
func (r RecordPrivateHubTrashed) PrivateHubTrashed( ctx context.Context, tenantID shared.ID, recipients []shared.ID, ) error
PrivateHubTrashed writes one record per recipient and queues the deliveries.
type RecordReminder ¶
type RecordReminder struct {
Notifications repository.Notifications
Preferences repository.Preferences
Accounts identityrepo.Accounts
Jobs Queue
Clock clock.Clock
IDs clock.IDGenerator
Signals Signals
}
RecordReminder turns a reminder that has come due into records and the jobs that send them.
Deliberately not a second delivery path, for the reason the invitation is not one: it writes the same record every other notification writes and queues the same `notification.deliver` job, so there is one place that renders an email and one place that sends it. What is different is only where the recipients come from - a reminder names them, or means the assignee and the entry's members - and that is the firing service's question, not this one's.
It runs inside the firing pass's transaction, which is what makes it exactly-once: the records, the delivery jobs, the reminder's own PENDING → SENT transition and the job's completion commit together. A process that dies halfway leaves none of them, and the reminder is still pending when the job is claimed again.
func (RecordReminder) Execute ¶
func (r RecordReminder) Execute( ctx context.Context, tenantID, itemID shared.ID, recipients []shared.ID, ) error
Execute writes one record per recipient and queues what is to be sent.
The preference is consulted, unlike the invitation's: somebody may switch reminders off, and the suppressed record is what tells them afterwards why they heard nothing. Nobody is named as the actor - a reminder is caused by the clock rather than by a person - which is also what keeps the domain's self-caused rule from suppressing a reminder somebody set for themselves.
type RecordRetentionWarning ¶
type RecordRetentionWarning struct {
Notifications repository.Notifications
Accounts identityrepo.Accounts
Memberships identityrepo.Memberships
Members Members
Preferences repository.Preferences
Jobs Queue
Clock clock.Clock
IDs clock.IDGenerator
Signals Signals
// Reach drops an administrator - of the workspace above all - who does not reach the private
// hub the entry is in (D6, ADR-0073 §1): the warning names the entry.
Reach PrivateReach
}
RecordRetentionWarning tells the people who can stop it that a rule is about to act (data-retention.md §6).
The ordinary notification path rather than a channel of its own: the preference is honoured, the record is deduplicated on the entry, and the send is a job like every other. What is different is only what caused it - not somebody's action and no domain event, but a retention pass concluding that an object's period has run out.
Why it carries no event ¶
`event_id` is null, which the column allows "for the invitation, which is not an event". A marking is the third of those: it is a decision the engine made about a row, and there is no envelope that says so. What makes it happen once is the marking itself - an entry is marked once and the phase that marks it does not see it again.
Who is told ¶
The rule says, in the vocabulary §2 gives it: the entry's own members, the administrators of the container it sits in, the administrators of the workspace. The first is a list the entry carries; the other two are the role matrix's answer, read along the entry's path, and neither of them is a list a rule's author can widen - a recipient outside the closed set is refused where the rule is written.
func (RecordRetentionWarning) Warn ¶
func (r RecordRetentionWarning) Warn(ctx context.Context, warning RetentionWarning) error
Warn writes one record per person and queues the deliveries.
It runs inside the caller's transaction - the sweep's marking write - so the warnings and the marking they are about commit together. A warning that committed without the marking would tell somebody their work is going when nothing had decided that; a marking that committed without its warnings would take the entry through its grace period in silence, which is the failure §6 is about.
One person who is both an entry member and an administrator is told once. The record is per person and per entry, not per reason they qualify.
type RecordRoleGranted ¶
type RecordRoleGranted struct {
Notifications repository.Notifications
Accounts identityrepo.Accounts
Preferences repository.Preferences
Jobs Queue
Clock clock.Clock
IDs clock.IDGenerator
Signals Signals
}
RecordRoleGranted tells a person that they were given a role (UC-NOT-01 check 9).
A grant is not an event - membership changes are in the audit trail, not the outbox (domain-model.md §4) - so the record is written in the grant's own transaction, as the emergency access writes its notices: the role and the record commit together or neither does, and once the grant is there nothing can lose the message. The record names the grant and nothing else; the mail reads the role, the scope and the place's name from it when it is sent, and a grant revoked before then takes its record along.
MEMBERSHIP's switch and its title preference decide, as for being added to an entry: both are "you are now part of this" to the person reading it.
func (RecordRoleGranted) RoleGranted ¶
func (r RecordRoleGranted) RoleGranted(ctx context.Context, grant identity.Grant, actorID shared.ID) error
RoleGranted writes the person's record and queues the delivery.
A grant to a group tells nobody. Check 9 is about the person given a role; a group's members did not change, the group's reach did, and a grant to a large group would mail every member at once about a change none of them asked about - joining a group mails nobody either.
type RecordRuleDisabled ¶
type RecordRuleDisabled struct {
Notifications repository.Notifications
Accounts identityrepo.Accounts
Preferences repository.Preferences
Jobs Queue
Clock clock.Clock
IDs clock.IDGenerator
Signals Signals
}
RecordRuleDisabled tells the writer of an automation rule that it has switched itself off after a run of failures (automation.md §2).
The same path RecordWebhookDisabled takes, and for the same reasons: the preference is honoured, the record is deduplicated, and the send is a job like every other. A rule that stopped acting and told nobody is a rule whose owner discovers the silence weeks later, by which time the work it was supposed to do has not been done and nobody knows since when.
The recipient is the rule's **author**, not the account it runs as. A service account has nobody behind it to read a message; the person who wrote the rule is the one who can fix it.
It carries no event, exactly as the webhook's does. The trigger is the fifth consecutive failed run, and there is no envelope that says so - the column allows a null. The deduplication index therefore does not apply, which is right: a rule disables itself once, so the caller is already the thing that happens once.
func (RecordRuleDisabled) RuleDisabled ¶
func (r RecordRuleDisabled) RuleDisabled( ctx context.Context, rule automation.Rule, at time.Time, ) error
RuleDisabled records the message and queues its delivery.
It runs inside the engine's transaction, so the record and the rule's new state commit together. A notification that committed without the disabling would tell somebody their rule had stopped while it carried on.
type RecordWayInChanged ¶
type RecordWayInChanged struct {
Jobs Queue
}
RecordWayInChanged queues the notice. It implements the identity writers' WayInNotices.
func (RecordWayInChanged) WayInChanged ¶
func (r RecordWayInChanged) WayInChanged( ctx context.Context, tenantID, accountID shared.ID, change identity.WayInChange, ) error
WayInChanged queues one notice for the account, inside the caller's transaction.
type RecordWebhookDisabled ¶
type RecordWebhookDisabled struct {
Notifications repository.Notifications
Accounts identityrepo.Accounts
Preferences repository.Preferences
Jobs queue.Queue
Clock clock.Clock
IDs clock.IDGenerator
Signals Signals
}
RecordWebhookDisabled tells the owner of a webhook subscription that this system has stopped calling their server (automation.md §3.1).
The ordinary notification path rather than a new channel: the preference is honoured, the record is deduplicated, and the send is a job like every other. What is different is only what caused it - nobody's action and no domain event, but a run of failures the system concluded something from.
Why it carries no event ¶
`event_id` is null, which the column allows "for the invitation, which is not an event". A disabled subscription is the second of those: the trigger is the eighth failed attempt of the third dead-lettered delivery, and there is no envelope that says so. The deduplication index therefore does not apply, which is correct here - the aggregate disables itself once, so the caller is already the thing that happens once.
func (RecordWebhookDisabled) WebhookDisabled ¶
func (r RecordWebhookDisabled) WebhookDisabled( ctx context.Context, tenantID, recipientID, subscriptionID shared.ID, ) error
WebhookDisabled records the message and queues its delivery.
It runs inside the caller's transaction - the delivery adapter's short write - so the record and the subscription's new state commit together. A notification that committed without the disabling would tell somebody their integration had stopped while it carried on.
type RedemptionMinter ¶
type RedemptionMinter interface {
MintRedemptionToken(ctx context.Context, tenantID, accountID shared.ID) (secret.Secret, error)
}
RedemptionMinter is the identity service's seam. The empty secret means the account is no longer waiting, and the mail carries the plain link.
type ResetLink ¶
type ResetLink struct {
Token secret.Secret
HasPassword bool
// First is a link to set a first password, under ADR-0077 §3's fallback.
First bool
// Connect is a link to connect the workspace's provider, where the password is off (ADR-0078 §1).
Connect bool
Address string
Locale string
}
ResetLink is what the minter answers. Declared here rather than imported so that this package points at nothing the identity service owns - the seam is one method and one value.
type ResetMinter ¶
type ResetMinter interface {
MintResetToken(ctx context.Context, tenantID, accountID shared.ID) (ResetLink, error)
}
ResetMinter is the identity service's seam. It answers the plaintext token, the address, and whether this account signs in with a password at all.
type RetentionWarning ¶
type RetentionWarning struct {
TenantID shared.ID
// ItemID is the entry the warning is about, and what the message links to.
ItemID shared.ID
// Path is the entry's chain - tenant, hub, collection - which is what the administrators are
// resolved along.
Path []identity.Scope
// Recipients is the rule's audience, in the closed set of data-retention.md §2.
Recipients []lifecycle.Recipient
}
RetentionWarning is one marked entry and what the rule said about warning people.
type RuleReader ¶
RuleReader answers a rule by its identifier - the automation repository's Find.
type SendAddressConfirmation ¶
type SendAddressConfirmation struct {
Links AddressMinter
Mail mail.Sender
Renderer i18n.Renderer
// Workspaces and FallbackLocale are the recipient's language chain, SendPasswordReset's.
Workspaces WorkspaceReader
UnitOfWork persistence.UnitOfWork
FallbackLocale string
BaseURL string
}
SendAddressConfirmation is the job handler.
type SendPasswordOpening ¶
type SendPasswordOpening struct {
Accounts identityrepo.Accounts
// Workspaces answers the workspace's name for the message, and its default language - the
// second link of §2's chain.
Workspaces WorkspaceReader
Mail mail.Sender
Renderer i18n.Renderer
UnitOfWork persistence.UnitOfWork
FallbackLocale string
// BaseURL is where this installation lives: the message links to the sign-in settings.
BaseURL string
}
SendPasswordOpening is the job handler's application half: it reads who and where, and sends.
func (SendPasswordOpening) Execute ¶
func (s SendPasswordOpening) Execute(ctx context.Context, tenantID shared.ID, notice OpeningNotice) error
Execute sends one notice. An account that is gone, or has no address, is finished business: there is nothing a retry would find.
type SendPasswordReset ¶
type SendPasswordReset struct {
Resets ResetMinter
Mail mail.Sender
Renderer i18n.Renderer
// Workspaces and FallbackLocale are §2's chain, DeliverNotification's verbatim: the account's
// own language, otherwise the workspace's default, otherwise the installation's.
Workspaces WorkspaceReader
UnitOfWork persistence.UnitOfWork
FallbackLocale string
// BaseURL is where this installation lives. A relative path in an email is a dead link.
BaseURL string
}
SendPasswordReset is the job handler.
type SendWayInChanged ¶
type SendWayInChanged struct {
Accounts identityrepo.Accounts
// Workspaces answers the workspace's name for the message, and its default language.
Workspaces WorkspaceReader
// Providers is read for a disconnection: a provider that still exists was not removed after
// all - the removal that queued the notice failed - and nothing is sent.
Providers ProviderFinder
Mail mail.Sender
Renderer i18n.Renderer
UnitOfWork persistence.UnitOfWork
FallbackLocale string
// BaseURL is where this installation lives: the message links back to it.
BaseURL string
}
SendWayInChanged is the job handler's application half: it reads who and where, and sends.
func (SendWayInChanged) Execute ¶
func (s SendWayInChanged) Execute(ctx context.Context, tenantID shared.ID, notice WayInNotice) error
Execute sends one notice. An account that is gone, or has no address, is finished business.
type Signals ¶
type Signals interface {
NotificationRecorded(ctx context.Context, category, channel, state string)
NotificationSent(ctx context.Context, category, channel string, seconds float64)
NotificationFailed(ctx context.Context, category, channel, reason string)
}
Signals is the slice of the metrics adapter this package reports through (observability-reliability.md §3.2).
The labels are the closed sets the domain defines - the category, the channel, the state and the suppression reasons - so there is no way for an unbounded label to reach a metric from here.
type SubscriptionReader ¶
type SubscriptionReader interface {
Find(ctx context.Context, id shared.ID) (integrationrepo.StoredSubscription, error)
}
SubscriptionReader answers a subscription by its identifier - the integration repository's Find.
type WayInNotice ¶
type WayInNotice struct {
AccountID shared.ID
Change identity.WayInChange
}
WayInNotice is one queued notice, as the job carries it.
Source Files
¶
- Decide.go
- DeliverNotification.go
- PasswordOpening.go
- RecordEmergencyAccess.go
- RecordInvitation.go
- RecordNotifications.go
- RecordPrivateHubTrashed.go
- RecordReminder.go
- RecordRetentionWarning.go
- RecordRoleGranted.go
- RecordRuleDisabled.go
- RecordWebhookDisabled.go
- SendAddressConfirmation.go
- SendPasswordReset.go
- WayInChanged.go