(* $Id: netsendmail.ml 1588 2011-04-28 13:59:54Z gerd $
* ----------------------------------------------------------------------
*
*)
open Netchannels
open Netmime
open Mimestring
let sendmail_program = "/usr/lib/sendmail" ;;
let only_usascii_re = Netstring_str.regexp "^[\001-\127]*$";;
let specials_re =
Netstring_str.regexp "[]<>\"\\,()@;:.[/=?]"
(* PCRE: "[\\<\\>\\\"\\\\\\,\\(\\)\\@\\;\\:\\.\\[\\]\\/\\=\\?]" *)
let exists rex s =
try
ignore(Netstring_str.search_forward rex s 0); true
with
Not_found -> false
;;
let ws_re = Netstring_str.regexp "[ \t\r\n]+"
let create_address_tokens
?(in_charset = `Enc_iso88591) ?(out_charset = `Enc_iso88591)
(hr_addr, formal_addr)=
(* Generates addresses like "Gerd Stolpmann <gerd@gerd-stolpmann.de>".
* hr_addr is the "human readable" part, and formal_addr is the formal
* address. hr_addr must be encoded by [charset].
*)
let hr_addr =
Netconversion.recode_string ~in_enc:in_charset ~out_enc:out_charset hr_addr in
let hr_words =
if Netstring_str.string_match only_usascii_re hr_addr 0 <> None then begin
(* Use double quotes to protect meta characters *)
if exists specials_re hr_addr then
[Mimestring.QString hr_addr]
else
List.map
(fun s -> Mimestring.Atom s)
(Netstring_str.split ws_re hr_addr)
end
else
[Mimestring.EncodedWord((Netconversion.string_of_encoding out_charset,""),
"Q",
hr_addr)]
in
(* TODO: Check syntax of formal_addr *)
let formal_words =
[ Mimestring.Special '<';
Mimestring.Atom formal_addr;
Mimestring.Special '>'
] in
(hr_words @ [ Mimestring.Special ' ' ] @ formal_words)
;;
let create_address_list_tokens ?in_charset ?out_charset addrs =
let rec map addrs =
match addrs with
[] -> []
| addr :: (addr' :: _ as addrs') ->
create_address_tokens ?in_charset ?out_charset addr @
[ Mimestring.Special ','; Mimestring.Special ' ' ] @
map addrs'
| [ addr ] ->
create_address_tokens ?in_charset ?out_charset addr
in
map addrs
;;
let format_field_value fieldname tokens =
let val_buf = Buffer.create 80 in
let val_ch = new output_buffer val_buf in
let maxlen = 78 in
let hardmaxlen = 998 in
let initlen = String.length fieldname + 2 in (* String.length ": " = 2 *)
Mimestring.write_value
~maxlen1:(maxlen - initlen)
~maxlen
~hardmaxlen1:(hardmaxlen - initlen)
~hardmaxlen
val_ch
tokens;
Buffer.contents val_buf
;;
let create_text_tokens
?(in_charset = `Enc_iso88591) ?(out_charset = `Enc_iso88591) value =
let value =
Netconversion.recode_string ~in_enc:in_charset ~out_enc:out_charset value in
let words =
if Netstring_str.string_match only_usascii_re value 0 <> None then
List.map (fun s -> Atom s) (Netstring_str.split ws_re value)
else
[ Mimestring.EncodedWord((Netconversion.string_of_encoding out_charset,""),
"Q",
value) ]
in
words
;;
(* Criterions for fields in [fields_to_remove]:
*
* We want that the message appears as a new message. Because of this
* all delivery and transport-related fields are removed. This also
* includes delivery and error notifications, transfer priorities,
* mailing list information, and local status fields.
*
* We keep all content-related fields. Sometimes it is difficult to
* distinguish between content and transfer fields, especially for
* the non-standard fields.
*
* Sources:
* - RFC 2822
* - http://www.dsv.su.se/~jpalme/ietf/mail-headers/mail-headers.html
* (former IETF draft, now expired)
* - http://www.cs.tut.fi/~jkorpela/headers.html
*)
let fields_to_remove =
[ "date";
"from";
"sender";
"reply-to";
"to";
"cc";
"bcc";
"message-id";
"in-reply-to";
"references";
(* but not subject, comments, keywords *)
"resent-date";
"resent-from";
"resent-sender";
"resent-to";
"resent-cc";
"resent-bcc";
"resent-message-id";
"return-path";
"received";
(* non-standard, or other RFCs but frequently used: *)
"alternate-recipient";
"x-rcpt-to";
"x-sender";
"x-x-sender";
"envelope-to";
"x-envelope-to";
"envelope-from";
"x-envelope-from";
"errors-to";
"return-receipt-to";
"read-receipt-to";
"x-confirm-reading-to";
"return-receipt-requested";
"registered-mail-reply-requested-by";
"delivery-date";
"delivered-to";
"x-loop";
"precedence";
"priority"; (* but not "importance" *)
"x-msmail-priority";
"x-priority";
"apparently-to";
"posted-to";
"content-return";
"x400-content-return";
"disposition-notification-options";
"disposition-notification-to";
"generate-delivery-report";
"original-recipient";
"prevent-nondelivery-report";
"mail-reply-to";
"x-uidl";
"x-imap";
"x-mime-autoconverted";
"list-archive";
"list-digest";
"list-help";
"list-id";
"mailing-list";
"x-mailing-list";
"list-owner";
"list-post";
"list-software";
"list-subscribe";
"list-unsubscribe";
"list-url";
"x-listserver";
"x-list-host";
"status";
]
;;
let wrap_mail
?(in_charset = `Enc_iso88591)
?(out_charset = `Enc_iso88591)
?from_addr ?(cc_addrs = []) ?(bcc_addrs = [])
~to_addrs ~subject (msg : complex_mime_message) : complex_mime_message =
let (main_hdr, main_body) = msg in
let main_hdr' =
new basic_mime_header main_hdr#fields in
List.iter main_hdr'#delete_field fields_to_remove;
let set_field_toks f toks =
main_hdr' # update_field f (format_field_value f toks)
in
( match from_addr with
None -> ()
| Some a ->
set_field_toks "From"
(create_address_list_tokens ~in_charset ~out_charset [ a ])
);
set_field_toks "To"
(create_address_list_tokens ~in_charset ~out_charset to_addrs);
if cc_addrs <> [] then
set_field_toks "Cc"
(create_address_list_tokens ~in_charset ~out_charset cc_addrs);
if bcc_addrs <> [] then
set_field_toks "Bcc"
(create_address_list_tokens ~in_charset ~out_charset bcc_addrs);
set_field_toks "Subject"
(create_text_tokens ~in_charset ~out_charset subject);
main_hdr' # update_field "MIME-Version" "1.0";
main_hdr' # update_field "X-Mailer" "OcamlNet (ocamlnet.sourceforge.net)";
main_hdr' # update_field "Date"
(Netdate.mk_mail_date ~zone:Netdate.localzone (Unix.time()));
(main_hdr', main_body)
;;
let create_header
?(in_charset = `Enc_iso88591)
?(out_charset = `Enc_iso88591)
?content_id
?content_description
?content_location
?content_disposition
(ct_type, params) =
let hdr = new basic_mime_header [] in
let set_field_toks f toks =
hdr # update_field f (format_field_value f toks)
in
let toks = Atom ct_type :: param_tokens ~maxlen:60 params in
set_field_toks "Content-type" toks;
(* Set Content-ID: *)
( match content_id with
None -> ()
| Some cid ->
set_field_toks "Content-ID" [Atom ("<" ^ cid ^ ">")]
);
(* Set Content-Description: *)
( match content_description with
None -> ()
| Some d ->
set_field_toks "Content-Description"
(create_text_tokens ~in_charset ~out_charset d);
);
(* Set Content-Location: *)
( match content_location with
None -> ()
| Some loc ->
set_field_toks "Content-Location"
(split_uri loc)
);
(* Set Content-Disposition: *)
( match content_disposition with
None -> ()
| Some (d_main, d_params) ->
set_field_toks "Content-Disposition"
(Atom d_main :: param_tokens ~maxlen:60 d_params)
);
hdr
;;
let wrap_parts
?(in_charset = `Enc_iso88591)
?(out_charset = `Enc_iso88591)
?(content_type = ("multipart/mixed", []))
?content_id
?content_description
?content_location
?content_disposition
elements =
if elements = [] then
failwith "Netsendmail.wrap_parts";
(* Check Content-type: *)
let (ct_type, params) = content_type in
let (main_type, sub_type) = split_mime_type ct_type in
if main_type <> "multipart" && main_type <> "message" then
failwith "Netsendmail.wrap_parts";
let hdr =
create_header
~in_charset ~out_charset ?content_id ?content_description
?content_location ?content_disposition (ct_type, params) in
(hdr, `Parts elements)
;;
let wrap_attachment
?(in_charset = `Enc_iso88591)
?(out_charset = `Enc_iso88591)
?content_id
?content_description
?content_location
?content_disposition
~content_type
body =
let hdr =
create_header
~in_charset ~out_charset ?content_id ?content_description
?content_location ?content_disposition content_type in
(* Set Content-transfer-encoding: *)
let (ct_type, params) = content_type in
let (main_type, sub_type) = split_mime_type ct_type in
let cte =
match main_type with
"text" -> "quoted-printable"
| "multipart"
| "message" -> "binary" (* Don't know better *)
| _ -> "base64"
in
hdr # update_field "Content-transfer-encoding" cte;
(hdr, `Body body)
;;
let compose
?(in_charset = `Enc_iso88591)
?(out_charset = `Enc_iso88591)
?from_addr ?(cc_addrs = []) ?(bcc_addrs = [])
?content_type
?(container_type = ("multipart/mixed" , []))
?(attachments = ([] : complex_mime_message list))
~to_addrs ~subject body : complex_mime_message =
(* First generate/cleanup (hdr,body) for the main text of the message: *)
let body =
if content_type = None then
Netconversion.recode_string
~in_enc:in_charset ~out_enc:out_charset body
else
body
in
(* Set Content-type: *)
let (ct_type, params) =
( match content_type with
None ->
("text/plain", [ "charset",
mk_param (Netconversion.string_of_encoding
out_charset) ])
| Some (ct_type, params) ->
(ct_type, params)
) in
let (main_hdr, main_body) =
wrap_attachment
~in_charset ~out_charset ~content_type:(ct_type, params)
(new memory_mime_body body)
in
(* Generate the container for attachments: *)
let mail_hdr, mail_body =
if attachments = [] then
(main_hdr, main_body)
else (
wrap_parts
~in_charset ~out_charset ~content_type:container_type
( (main_hdr, main_body) :: attachments )
)
in
wrap_mail ~in_charset ~out_charset ?from_addr ~to_addrs ~cc_addrs ~bcc_addrs
~subject (mail_hdr, mail_body)
;;
let sendmail ?(mailer = sendmail_program) ?(crlf = false) message =
let cmd = mailer ^ " -B8BITMIME -t -i" in
with_out_obj_channel
(new output_command cmd)
(fun ch ->
write_mime_message ~crlf ch message;
)
;;