primitivedotdev 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- primitive/__init__.py +157 -0
- primitive/_compat.py +9 -0
- primitive/errors.py +191 -0
- primitive/models_generated.py +874 -0
- primitive/py.typed +0 -0
- primitive/schema.py +16 -0
- primitive/schemas/email_received_event.schema.json +1063 -0
- primitive/types.py +250 -0
- primitive/validation.py +155 -0
- primitive/webhook.py +635 -0
- primitivedotdev-0.1.1.dist-info/METADATA +183 -0
- primitivedotdev-0.1.1.dist-info/RECORD +13 -0
- primitivedotdev-0.1.1.dist-info/WHEEL +4 -0
primitive/__init__.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
from .errors import (
|
|
2
|
+
PAYLOAD_ERRORS,
|
|
3
|
+
RAW_EMAIL_ERRORS,
|
|
4
|
+
VERIFICATION_ERRORS,
|
|
5
|
+
PrimitiveWebhookError,
|
|
6
|
+
RawEmailDecodeError,
|
|
7
|
+
WebhookPayloadError,
|
|
8
|
+
WebhookValidationError,
|
|
9
|
+
WebhookVerificationError,
|
|
10
|
+
)
|
|
11
|
+
from .schema import email_received_event_json_schema
|
|
12
|
+
from .types import (
|
|
13
|
+
AuthConfidence,
|
|
14
|
+
AuthVerdict,
|
|
15
|
+
Content,
|
|
16
|
+
Delivery,
|
|
17
|
+
DkimResult,
|
|
18
|
+
DkimSignature,
|
|
19
|
+
DmarcPolicy,
|
|
20
|
+
DmarcResult,
|
|
21
|
+
Download,
|
|
22
|
+
Email,
|
|
23
|
+
EmailAddress,
|
|
24
|
+
EmailAnalysis,
|
|
25
|
+
EmailAuth,
|
|
26
|
+
EmailReceivedEvent,
|
|
27
|
+
EventType,
|
|
28
|
+
ForwardAnalysis,
|
|
29
|
+
ForwardOriginalSender,
|
|
30
|
+
ForwardResult,
|
|
31
|
+
ForwardResultAttachmentAnalyzed,
|
|
32
|
+
ForwardResultAttachmentSkipped,
|
|
33
|
+
ForwardResultInline,
|
|
34
|
+
ForwardVerdict,
|
|
35
|
+
ForwardVerification,
|
|
36
|
+
Headers,
|
|
37
|
+
KnownWebhookEvent,
|
|
38
|
+
ParsedData,
|
|
39
|
+
ParsedDataComplete,
|
|
40
|
+
ParsedDataFailed,
|
|
41
|
+
ParsedError,
|
|
42
|
+
ParsedStatus,
|
|
43
|
+
RawContent,
|
|
44
|
+
RawContentDownloadOnly,
|
|
45
|
+
RawContentInline,
|
|
46
|
+
Smtp,
|
|
47
|
+
Spamassassin,
|
|
48
|
+
SpfResult,
|
|
49
|
+
UnknownEvent,
|
|
50
|
+
ValidateEmailAuthResult,
|
|
51
|
+
WebhookAttachment,
|
|
52
|
+
WebhookEvent,
|
|
53
|
+
WebhookVersion,
|
|
54
|
+
)
|
|
55
|
+
from .validation import (
|
|
56
|
+
ValidationFailure,
|
|
57
|
+
ValidationResult,
|
|
58
|
+
ValidationSuccess,
|
|
59
|
+
safe_validate_email_received_event,
|
|
60
|
+
validate_email_received_event,
|
|
61
|
+
)
|
|
62
|
+
from .webhook import (
|
|
63
|
+
LEGACY_CONFIRMED_HEADER,
|
|
64
|
+
LEGACY_SIGNATURE_HEADER,
|
|
65
|
+
PRIMITIVE_CONFIRMED_HEADER,
|
|
66
|
+
PRIMITIVE_SIGNATURE_HEADER,
|
|
67
|
+
WEBHOOK_VERSION,
|
|
68
|
+
confirmed_headers,
|
|
69
|
+
decode_raw_email,
|
|
70
|
+
get_download_time_remaining,
|
|
71
|
+
handle_webhook,
|
|
72
|
+
is_download_expired,
|
|
73
|
+
is_email_received_event,
|
|
74
|
+
is_raw_included,
|
|
75
|
+
parse_json_body,
|
|
76
|
+
parse_webhook_event,
|
|
77
|
+
sign_webhook_payload,
|
|
78
|
+
validate_email_auth,
|
|
79
|
+
verify_raw_email_download,
|
|
80
|
+
verify_webhook_signature,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
__all__ = [
|
|
84
|
+
"AuthConfidence",
|
|
85
|
+
"AuthVerdict",
|
|
86
|
+
"Content",
|
|
87
|
+
"Delivery",
|
|
88
|
+
"DkimResult",
|
|
89
|
+
"DkimSignature",
|
|
90
|
+
"DmarcPolicy",
|
|
91
|
+
"DmarcResult",
|
|
92
|
+
"Download",
|
|
93
|
+
"Email",
|
|
94
|
+
"EmailAddress",
|
|
95
|
+
"EmailAnalysis",
|
|
96
|
+
"EmailAuth",
|
|
97
|
+
"EmailReceivedEvent",
|
|
98
|
+
"EventType",
|
|
99
|
+
"ForwardAnalysis",
|
|
100
|
+
"ForwardOriginalSender",
|
|
101
|
+
"ForwardResult",
|
|
102
|
+
"ForwardResultAttachmentAnalyzed",
|
|
103
|
+
"ForwardResultAttachmentSkipped",
|
|
104
|
+
"ForwardResultInline",
|
|
105
|
+
"ForwardVerdict",
|
|
106
|
+
"ForwardVerification",
|
|
107
|
+
"Headers",
|
|
108
|
+
"KnownWebhookEvent",
|
|
109
|
+
"LEGACY_CONFIRMED_HEADER",
|
|
110
|
+
"LEGACY_SIGNATURE_HEADER",
|
|
111
|
+
"PAYLOAD_ERRORS",
|
|
112
|
+
"PRIMITIVE_CONFIRMED_HEADER",
|
|
113
|
+
"PRIMITIVE_SIGNATURE_HEADER",
|
|
114
|
+
"ParsedData",
|
|
115
|
+
"ParsedDataComplete",
|
|
116
|
+
"ParsedDataFailed",
|
|
117
|
+
"ParsedError",
|
|
118
|
+
"ParsedStatus",
|
|
119
|
+
"PrimitiveWebhookError",
|
|
120
|
+
"RAW_EMAIL_ERRORS",
|
|
121
|
+
"RawContent",
|
|
122
|
+
"RawContentDownloadOnly",
|
|
123
|
+
"RawContentInline",
|
|
124
|
+
"RawEmailDecodeError",
|
|
125
|
+
"Smtp",
|
|
126
|
+
"Spamassassin",
|
|
127
|
+
"SpfResult",
|
|
128
|
+
"UnknownEvent",
|
|
129
|
+
"ValidateEmailAuthResult",
|
|
130
|
+
"VERIFICATION_ERRORS",
|
|
131
|
+
"ValidationFailure",
|
|
132
|
+
"ValidationResult",
|
|
133
|
+
"ValidationSuccess",
|
|
134
|
+
"WEBHOOK_VERSION",
|
|
135
|
+
"WebhookAttachment",
|
|
136
|
+
"WebhookEvent",
|
|
137
|
+
"WebhookVersion",
|
|
138
|
+
"WebhookPayloadError",
|
|
139
|
+
"WebhookValidationError",
|
|
140
|
+
"WebhookVerificationError",
|
|
141
|
+
"confirmed_headers",
|
|
142
|
+
"decode_raw_email",
|
|
143
|
+
"email_received_event_json_schema",
|
|
144
|
+
"get_download_time_remaining",
|
|
145
|
+
"handle_webhook",
|
|
146
|
+
"is_download_expired",
|
|
147
|
+
"is_email_received_event",
|
|
148
|
+
"is_raw_included",
|
|
149
|
+
"parse_json_body",
|
|
150
|
+
"parse_webhook_event",
|
|
151
|
+
"safe_validate_email_received_event",
|
|
152
|
+
"sign_webhook_payload",
|
|
153
|
+
"validate_email_auth",
|
|
154
|
+
"validate_email_received_event",
|
|
155
|
+
"verify_raw_email_download",
|
|
156
|
+
"verify_webhook_signature",
|
|
157
|
+
]
|
primitive/_compat.py
ADDED
primitive/errors.py
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any, Literal
|
|
5
|
+
|
|
6
|
+
VERIFICATION_ERRORS = {
|
|
7
|
+
"INVALID_SIGNATURE_HEADER": {
|
|
8
|
+
"message": "Missing or malformed Primitive-Signature header",
|
|
9
|
+
"suggestion": "Check that you're reading the correct header (Primitive-Signature) and it's being passed correctly from your web framework.",
|
|
10
|
+
},
|
|
11
|
+
"TIMESTAMP_OUT_OF_RANGE": {
|
|
12
|
+
"message": "Timestamp is too old (possible replay attack)",
|
|
13
|
+
"suggestion": "This could indicate a replay attack, network delay, or server clock drift. Check your server's time is synced.",
|
|
14
|
+
},
|
|
15
|
+
"SIGNATURE_MISMATCH": {
|
|
16
|
+
"message": "Signature doesn't match expected value",
|
|
17
|
+
"suggestion": "Verify the webhook secret matches and you're using the raw request body (not re-serialized JSON).",
|
|
18
|
+
},
|
|
19
|
+
"MISSING_SECRET": {
|
|
20
|
+
"message": "No webhook secret was provided",
|
|
21
|
+
"suggestion": "Pass your webhook secret from the Primitive dashboard. Check that the environment variable is set.",
|
|
22
|
+
},
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
PAYLOAD_ERRORS = {
|
|
26
|
+
"PAYLOAD_NULL": {
|
|
27
|
+
"message": "Webhook payload is null",
|
|
28
|
+
"suggestion": "Ensure you're passing the parsed JSON body, not null. Check your framework's body parsing middleware.",
|
|
29
|
+
},
|
|
30
|
+
"PAYLOAD_UNDEFINED": {
|
|
31
|
+
"message": "Webhook payload is undefined",
|
|
32
|
+
"suggestion": "The payload was not provided. Make sure you're passing the request body to the handler.",
|
|
33
|
+
},
|
|
34
|
+
"PAYLOAD_WRONG_TYPE": {
|
|
35
|
+
"message": "Webhook payload must be an object",
|
|
36
|
+
"suggestion": "The payload should be a parsed JSON object. Check that you're not passing a string or other primitive.",
|
|
37
|
+
},
|
|
38
|
+
"PAYLOAD_IS_ARRAY": {
|
|
39
|
+
"message": "Webhook payload is an array, expected object",
|
|
40
|
+
"suggestion": "Primitive webhooks are single event objects, not arrays. Check the payload structure.",
|
|
41
|
+
},
|
|
42
|
+
"PAYLOAD_MISSING_EVENT": {
|
|
43
|
+
"message": "Webhook payload missing 'event' field",
|
|
44
|
+
"suggestion": "All webhook payloads must have an 'event' field. This may not be a valid Primitive webhook.",
|
|
45
|
+
},
|
|
46
|
+
"PAYLOAD_UNKNOWN_EVENT": {
|
|
47
|
+
"message": "Unknown webhook event type",
|
|
48
|
+
"suggestion": "This event type is not recognized. You may need to update your SDK or handle unknown events gracefully.",
|
|
49
|
+
},
|
|
50
|
+
"PAYLOAD_EMPTY_BODY": {
|
|
51
|
+
"message": "Request body is empty",
|
|
52
|
+
"suggestion": "The request body was empty. Ensure the webhook is sending data and your framework is parsing it correctly.",
|
|
53
|
+
},
|
|
54
|
+
"JSON_PARSE_FAILED": {
|
|
55
|
+
"message": "Failed to parse JSON body",
|
|
56
|
+
"suggestion": "The request body is not valid JSON. Check the raw body content and Content-Type header.",
|
|
57
|
+
},
|
|
58
|
+
"INVALID_ENCODING": {
|
|
59
|
+
"message": "Invalid body encoding",
|
|
60
|
+
"suggestion": "The request body encoding is not supported. Primitive webhooks use UTF-8 encoded JSON.",
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
RAW_EMAIL_ERRORS = {
|
|
65
|
+
"NOT_INCLUDED": {
|
|
66
|
+
"message": "Raw email content not included inline",
|
|
67
|
+
"suggestion": "Use the download URL at event.email.content.download.url to fetch the raw email.",
|
|
68
|
+
},
|
|
69
|
+
"INVALID_BASE64": {
|
|
70
|
+
"message": "Raw email content is not valid base64",
|
|
71
|
+
"suggestion": "The raw email data is malformed. Fetch the raw email from the download URL or regenerate the webhook payload.",
|
|
72
|
+
},
|
|
73
|
+
"HASH_MISMATCH": {
|
|
74
|
+
"message": "SHA-256 hash verification failed",
|
|
75
|
+
"suggestion": "The raw email data may be corrupted. Try downloading from the URL instead.",
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
WebhookVerificationErrorCode = Literal[
|
|
80
|
+
"INVALID_SIGNATURE_HEADER",
|
|
81
|
+
"TIMESTAMP_OUT_OF_RANGE",
|
|
82
|
+
"SIGNATURE_MISMATCH",
|
|
83
|
+
"MISSING_SECRET",
|
|
84
|
+
]
|
|
85
|
+
WebhookPayloadErrorCode = Literal[
|
|
86
|
+
"PAYLOAD_NULL",
|
|
87
|
+
"PAYLOAD_UNDEFINED",
|
|
88
|
+
"PAYLOAD_WRONG_TYPE",
|
|
89
|
+
"PAYLOAD_IS_ARRAY",
|
|
90
|
+
"PAYLOAD_MISSING_EVENT",
|
|
91
|
+
"PAYLOAD_UNKNOWN_EVENT",
|
|
92
|
+
"PAYLOAD_EMPTY_BODY",
|
|
93
|
+
"JSON_PARSE_FAILED",
|
|
94
|
+
"INVALID_ENCODING",
|
|
95
|
+
]
|
|
96
|
+
WebhookValidationErrorCode = Literal["SCHEMA_VALIDATION_FAILED"]
|
|
97
|
+
RawEmailDecodeErrorCode = Literal["NOT_INCLUDED", "INVALID_BASE64", "HASH_MISMATCH"]
|
|
98
|
+
WebhookErrorCode = (
|
|
99
|
+
WebhookVerificationErrorCode
|
|
100
|
+
| WebhookPayloadErrorCode
|
|
101
|
+
| WebhookValidationErrorCode
|
|
102
|
+
| RawEmailDecodeErrorCode
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class PrimitiveWebhookError(Exception):
|
|
107
|
+
code: str
|
|
108
|
+
suggestion: str
|
|
109
|
+
|
|
110
|
+
def __str__(self) -> str:
|
|
111
|
+
return (
|
|
112
|
+
f"{self.__class__.__name__} [{self.code}]: {self.args[0]}\n\n"
|
|
113
|
+
f"Suggestion: {self.suggestion}"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
def to_json(self) -> dict[str, Any]:
|
|
117
|
+
return {
|
|
118
|
+
"name": self.__class__.__name__,
|
|
119
|
+
"code": self.code,
|
|
120
|
+
"message": self.args[0],
|
|
121
|
+
"suggestion": self.suggestion,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class WebhookVerificationError(PrimitiveWebhookError):
|
|
126
|
+
def __init__(
|
|
127
|
+
self,
|
|
128
|
+
code: WebhookVerificationErrorCode,
|
|
129
|
+
message: str | None = None,
|
|
130
|
+
suggestion: str | None = None,
|
|
131
|
+
) -> None:
|
|
132
|
+
definition = VERIFICATION_ERRORS[code]
|
|
133
|
+
super().__init__(message or definition["message"])
|
|
134
|
+
self.code = code
|
|
135
|
+
self.suggestion = suggestion or definition["suggestion"]
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class WebhookPayloadError(PrimitiveWebhookError):
|
|
139
|
+
def __init__(
|
|
140
|
+
self,
|
|
141
|
+
code: WebhookPayloadErrorCode,
|
|
142
|
+
message: str | None = None,
|
|
143
|
+
suggestion: str | None = None,
|
|
144
|
+
cause: Exception | None = None,
|
|
145
|
+
) -> None:
|
|
146
|
+
definition = PAYLOAD_ERRORS[code]
|
|
147
|
+
super().__init__(message or definition["message"])
|
|
148
|
+
self.code = code
|
|
149
|
+
self.suggestion = suggestion or definition["suggestion"]
|
|
150
|
+
self.__cause__ = cause
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@dataclass(slots=True)
|
|
154
|
+
class ValidationIssue:
|
|
155
|
+
path: str
|
|
156
|
+
message: str
|
|
157
|
+
validator: str
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class WebhookValidationError(PrimitiveWebhookError):
|
|
161
|
+
def __init__(
|
|
162
|
+
self,
|
|
163
|
+
field: str,
|
|
164
|
+
message: str,
|
|
165
|
+
suggestion: str,
|
|
166
|
+
validation_errors: list[ValidationIssue],
|
|
167
|
+
) -> None:
|
|
168
|
+
super().__init__(message)
|
|
169
|
+
self.code = "SCHEMA_VALIDATION_FAILED"
|
|
170
|
+
self.field = field
|
|
171
|
+
self.suggestion = suggestion
|
|
172
|
+
self.validation_errors = validation_errors
|
|
173
|
+
self.additional_error_count = max(0, len(validation_errors) - 1)
|
|
174
|
+
|
|
175
|
+
def to_json(self) -> dict[str, Any]:
|
|
176
|
+
return {
|
|
177
|
+
"name": self.__class__.__name__,
|
|
178
|
+
"code": self.code,
|
|
179
|
+
"field": self.field,
|
|
180
|
+
"message": self.args[0],
|
|
181
|
+
"suggestion": self.suggestion,
|
|
182
|
+
"additionalErrorCount": self.additional_error_count,
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class RawEmailDecodeError(PrimitiveWebhookError):
|
|
187
|
+
def __init__(self, code: RawEmailDecodeErrorCode, message: str | None = None) -> None:
|
|
188
|
+
definition = RAW_EMAIL_ERRORS[code]
|
|
189
|
+
super().__init__(message or definition["message"])
|
|
190
|
+
self.code = code
|
|
191
|
+
self.suggestion = definition["suggestion"]
|