Source file email_content.ml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
open! Core
module Multipart : sig
type t = private
{ boundary : Boundary.t
; prologue : Bigstring_shared.t option
; epilogue : Bigstring_shared.t option
; parts : Email.t list
; container_headers : Headers.t
}
[@@deriving sexp_of]
val create_unsafe
: boundary:Boundary.t
-> ?prologue:Bigstring_shared.t
-> ?epilogue:Bigstring_shared.t
-> Email.t list
-> container_headers:Headers.t
-> t
val create
: ?boundary:Boundary.t
-> ?prologue:Bigstring_shared.t
-> ?epilogue:Bigstring_shared.t
-> ?container_headers:Headers.t
-> Email.t list
-> t
val set
: t
-> ?boundary:Boundary.t
-> ?prologue:Bigstring_shared.t option
-> ?epilogue:Bigstring_shared.t option
-> ?parts:Email.t list
-> ?container_headers:Headers.t
-> unit
-> t
include String_monoidable.S with type t := t
end = struct
type t =
{ boundary : Boundary.t
; prologue : Bigstring_shared.t option
; epilogue : Bigstring_shared.t option
; parts : Email.t list
; : Headers.t
}
[@@deriving sexp_of]
let create_unsafe ~boundary ?prologue ?epilogue parts ~ =
{ boundary; prologue; epilogue; parts; container_headers }
;;
let create ?boundary ?prologue ?epilogue ?( = Headers.empty) parts =
let boundary =
Boundary.generate_non_conflicting_boundary
?prologue
~parts:(List.map parts ~f:Email.to_string_monoid)
?epilogue
(Option.value_map
boundary
~default:Boundary.Generator.default
~f:Boundary.Generator.from_existing_boundary)
in
create_unsafe ~boundary ?prologue ?epilogue parts ~container_headers
;;
let set
t
?(boundary = t.boundary)
?(prologue = t.prologue)
?(epilogue = t.epilogue)
?(parts = t.parts)
?( = t.container_headers)
()
=
create ~boundary ?prologue ?epilogue ~container_headers parts
;;
let to_string_monoid t =
Boundary.join_without_checking_for_conflicts
?prologue:t.prologue
~parts:(List.map t.parts ~f:Email.to_string_monoid)
?epilogue:t.epilogue
t.boundary
;;
end
type t =
| Multipart of Multipart.t
| Message of Email.t
| Data of Octet_stream.t
[@@deriving sexp_of]
let rec multipart_of_bigstring_shared ~boundary ~ bstr =
let open Or_error.Let_syntax in
let prologue, parts, epilogue = Boundary.split boundary bstr in
let%map parts =
List.map parts ~f:(fun part ->
Or_error.tag
(Or_error.try_with (fun () ->
Email.of_bigstring (Bigstring_shared.to_bigstring part)))
~tag:(sprintf "failed part:\n%s" (Bigstring_shared.to_string part)))
|> Or_error.all
in
Multipart.create_unsafe ~boundary ?prologue ?epilogue ~container_headers parts
and content_of_bigstring_shared ~ ? bstr =
let open Or_error.Let_syntax in
let parent_media_type = Option.bind container_headers ~f:Media_type.from_headers in
let media_type =
Option.value
(Media_type.from_headers headers)
~default:(Media_type.default ?parent:parent_media_type ())
in
let encoding = Octet_stream.Encoding.of_headers_or_default headers in
let octet_stream = Octet_stream.of_bigstring_shared ~encoding bstr in
let decode octet_stream =
match Octet_stream.decode octet_stream with
| None ->
Or_error.error "Unknown message encoding" encoding Octet_stream.Encoding.sexp_of_t
| Some decoded_bstr -> Ok decoded_bstr
in
match Media_type.multipart_boundary media_type with
| Some boundary ->
let%bind decoded_bstr = decode octet_stream in
let%bind multipart =
multipart_of_bigstring_shared ~boundary ~container_headers:headers decoded_bstr
in
Ok (Multipart multipart)
| None ->
if Media_type.is_message_rfc822 media_type
then (
let%bind decoded_bstr = decode octet_stream in
let%bind email =
Or_error.try_with (fun () ->
Email.of_bigstring (Bigstring_shared.to_bigstring decoded_bstr))
in
Ok (Message email))
else Ok (Data octet_stream)
and parse ? email =
content_of_bigstring_shared
?container_headers
~headers:(Email.headers email)
(Email.raw_content email |> Email_raw_content.to_bigstring_shared)
;;
let to_string_monoid = function
| Multipart multipart -> Multipart.to_string_monoid multipart
| Message message -> Email.to_string_monoid message
| Data octet_stream ->
Octet_stream.encoded_contents octet_stream |> Bigstring_shared.to_string_monoid
;;
let to_bigstring_shared t =
to_string_monoid t |> String_monoid.to_bigstring |> Bigstring_shared.of_bigstring
;;
let to_raw_content t = to_bigstring_shared t |> Email_raw_content.of_bigstring_shared
let rec multipart_map_data ~on_unparsable_content mp ~f =
Multipart.set
mp
~parts:(List.map mp.Multipart.parts ~f:(map_data ~on_unparsable_content ~f))
()
and content_map_data ~on_unparsable_content t ~f =
match t with
| Multipart t -> Multipart (multipart_map_data ~on_unparsable_content t ~f)
| Message message -> Message (map_data ~on_unparsable_content message ~f)
| Data data -> Data (f data)
and map_data ~on_unparsable_content email ~f =
match parse email with
| Ok content ->
let content = content_map_data content ~on_unparsable_content ~f in
Email.set_raw_content
email
(to_bigstring_shared content |> Email_raw_content.of_bigstring_shared)
| Error e ->
(match on_unparsable_content with
| `Skip -> email
| `Raise -> raise_s [%message "[map_data] has unparsable content" (e : Error.t)])
;;
let map_data ?(on_unparsable_content = `Skip) email ~f =
map_data ~on_unparsable_content email ~f
;;
let to_email ~ t =
let =
let media_type =
match t with
| Multipart mp ->
(match Media_type.from_headers headers with
| None -> Some (Media_type.create_multipart "related" ~boundary:mp.boundary)
| Some media_type ->
Some (Media_type.set_multipart_boundary media_type mp.boundary))
| _ -> None
in
match media_type with
| None -> headers
| Some media_type -> Media_type.set_headers headers media_type
in
Email.create
~headers
~raw_content:(to_bigstring_shared t |> Email_raw_content.of_bigstring_shared)
;;
let set_content email t = to_email ~headers:(Email.headers email) t