% -*- Erlang -*- % File: mail.erl (~jb/mail.erl) % Author: Johan Bevemyr % Created: Sat Oct 25 10:59:24 2003 % Purpose: % RFC 822 % RFC 1939 % RFC 2048 -module('mail'). -author('jb@trut.bluetail.com'). -export([parse_headers/1, list/2, list/3, ploop/5,pop_request/4, diff/2, session_manager_init/0, check_cookie/1, check_session/1, login/2, display_login/2, stat/3, showmail/2, compose/1, compose/7, send/6, send/2, get_val/3, logout/1, base64_2_str/1, retr/4, delete/2, send_attachment/2, send_attachment_plain/2, wrap_text/2, getopt/3, decode/1]). -include("../../../include/yaws_api.hrl"). -include("defs.hrl"). -record(info, { nr, size, headers }). -record(mail, { from="", from_fmt="", from_fmt_lc="", to="", cc="", bcc="", subject="", subject_fmt="", subject_fmt_lc="", date="", date_pst=date(), date_fmt="", content_type, transfer_encoding, content_disposition, other = [] }). -record(pstate, { port, user, pass, cmd, acc = [], from, lines, reply=[], more=true, remain, dotstate=0 }). -record(satt, { num, filename, ctype, data}). -record(session, { user, passwd, cookie, listing, sorting=rev_nr, attachments = [] %% list of #satt{} records }). -define(RETRYTIMEOUT, 300). -define(RETRYCOUNT, 5). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % build_toolbar(Entries) -> {table, [{bgcolor,"c0c0c0"},{cellpadding,0},{cellspacing,0},{border,0}], [{tr,[],{td, [{colspan,20},{height,1},{bgcolor,white}], {img, [{src,"spacer.gif"}, {width,1},{height,1}, {alt,""}, {border,0}],[]}}}, {tr,[], build_toolbar(Entries, -1)}, {tr,[],{td, [{colspan,20},{height,1},{bgcolor,gray}], {img, [{src,"spacer.gif"}, {width,1},{height,1}, {alt,""}, {border,0}],[]}}}, {tr,[],{td, [{colspan,20},{height,1}], {img, [{src,"spacer.gif"}, {width,1},{height,1}, {alt,""}, {border,0}],[]}}}]}. build_toolbar([], Used) -> Percent = integer_to_list(100-Used)++"%", [{td, [nowrap,{width,Percent},{valign,middle},{align,left}],[]}]; build_toolbar([{[],Url,Cmd}|Rest], Used) -> if Used == -1 -> []; true -> [{td, [nowrap,{width,"1%"},{valign,middle},{align,left}], {img, [{src,"tool-div.gif"},{width,2},{height,16}, {alt,""},{border,0},{hspace,2}]}}] end ++ [{td, [nowrap,{width,"2%"},{valign,middle},{align,left}], [{a, [{class,nolink}, {href,Url}], {font, [{size,2},{color,"#000000"},{title,Cmd}],Cmd}}]} | build_toolbar(Rest, Used+3)]; build_toolbar([{Gif,Url,Cmd}|Rest], Used) -> (if Used == -1 -> []; true -> [{td, [nowrap,{width,"1%"},{valign,middle},{align,left}], {img, [{src,"tool-div.gif"},{width,2},{height,16}, {alt,""},{border,0},{hspace,2}]}}] end ++ [{td, [nowrap,{width,"2%"},{valign,middle},{align,left}], {a, [{class,nolink}, {href,Url}], [{img, [{src,Gif},{vspace,2},{width,20}, {height,20},{alt,Cmd},{border,0}],[]}]} }, {td, [nowrap,{width,"2%"},{valign,middle},{align,left}], [{a, [{class,nolink}, {href,Url}], {font, [{size,2},{color,"#000000"},{title,Cmd}], Cmd}}]} | build_toolbar(Rest, Used+4)]). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % % delete(Session, ToDelete) -> tick_session(Session#session.cookie), Req = [del(M) || M <- ToDelete], pop_request(Req, popserver(), Session#session.user, Session#session.passwd), {redirect_local, {rel_path, "mail.yaws?refresh=true"}}. -record(send, {param, last = false, encoding, estate="", boundary="", from="", to="", cc="", bcc="", subject="", message="", attached="", port, session, line_start=true }). send(Session, A) -> State = prepare_send_state(A#arg.state, Session), case yaws_api:parse_multipart_post(A) of {cont, Cont, Res} -> case catch sendChunk(Res, State) of {done, Result} -> Result; {cont, NewState} -> {get_more, Cont, NewState}; {error, Reason} -> {ehtml, format_error("Failed to send email. Reason: "++ to_string(Reason))} end; {result, Res} -> case catch sendChunk(Res, State#send{last=true}) of {done, Result} -> Result; {cont, _} -> {ehtml,format_error("Failed to send email.")}; {error, Reason} -> {ehtml, format_error("Failed to send email. Reason: "++ to_string(Reason))} end end. prepare_send_state(undefined, Session) -> #send{session=Session}; prepare_send_state(State, Session) -> State#send{session=Session}. sendChunk([{part_body, Data}|Rest], State) -> sendChunk([{body, Data}|Rest], State); sendChunk([], State) when State#send.last/=true -> {cont, State}; sendChunk([], S0) when S0#send.last==true, S0#send.boundary/=[] -> if S0#send.estate /= "" -> smtp_send_b64_final(S0); true -> ok end, S = S0#send{estate=""}, smtp_send_part(S, ["\r\n--",S#send.boundary,"--\r\n"]), smtp_close(S), {done, {redirect_local, {rel_path, "mail.yaws"}}}; sendChunk([], State) when State#send.last==true, State#send.boundary==[] -> smtp_send_part(State, ["\r\n.\r\n"]), {done, {redirect_local, {rel_path, "mail.yaws"}}}; sendChunk([{head, {"to", _Opts}}|Rest], State) -> sendChunk(Rest, State#send{param=to}); sendChunk([{head, {"cc", _Opts}}|Rest], State) -> sendChunk(Rest, State#send{param=cc}); sendChunk([{head, {"bcc", _Opts}}|Rest], State) -> sendChunk(Rest, State#send{param=bcc}); sendChunk([{head, {"subject", _Opts}}|Rest], State) -> sendChunk(Rest, State#send{param=subject}); sendChunk([{head, {"html_subject", _Opts}}|Rest], State) -> sendChunk(Rest, State#send{param=ignore}); sendChunk([{head, {"message", _Opts}}|Rest], S) -> RTo = parse_addr(S#send.to), RCc = parse_addr(S#send.cc), RBcc = parse_addr(S#send.bcc), Recipients = RTo ++ RCc ++ RBcc, {ok, Port} = smtp_init(smtpserver(), S#send.session, Recipients), S2 = S#send{port=Port}, MailDomain = maildomain(), Session = S#send.session, CommonHeaders = [mail_header("To: ", S#send.to), mail_header("From: ", Session#session.user++"@"++MailDomain), mail_header("Cc: ", S#send.cc), mail_header("Bcc: ", S#send.bcc), mail_header("Subject: ", S#send.subject)], {Headers,S3} = case S#send.attached of "no" -> {CommonHeaders ++ [mail_header("Content-Type: ", "text/plain"), mail_header("Content-Transfer-Encoding: ", "8bit")], S2}; "yes" -> Boundary="--Next_Part("++boundary_date()++")--", {CommonHeaders ++ [mail_header("Mime-Version: ", "1.0"), mail_header("Content-Type: ", "Multipart/Mixed;\r\n boundary=\""++ Boundary++"\""), mail_header("Content-Transfer-Encoding: ", "8bit")], S2#send{boundary=Boundary}} end, smtp_send_part(S3, [Headers,"\r\n"]), case S3#send.attached of "yes" -> smtp_send_part(S3, ["--",S3#send.boundary,"\r\n", mail_header("Content-Type: ", "Text/Plain; charset=us-ascii"), mail_header("Content-Transfer-Encoding: ", "8bit"), "\r\n"]); "no" -> ok end, sendChunk(Rest, S3#send{param=message}); sendChunk([{head, {"attached", _Opts}}|Rest], State) -> sendChunk(Rest, State#send{param=attached}); sendChunk([{head, {File, _Opts}}|Rest], S) when S#send.attached=="no" -> sendChunk(Rest, S#send{param=ignore}); sendChunk([{head, {File, Opts}}|Rest], S0) when S0#send.attached=="yes" -> % io:format("attachment head\n"), if S0#send.estate /= "" -> smtp_send_b64_final(S0); true -> ok end, S = S0#send{estate=""}, FilePath = getopt(filename, Opts), case FilePath of [_|_] -> FileName = basename(FilePath), ContentType = content_type(FileName), smtp_send_part(S, ["\r\n--",S#send.boundary,"\r\n", mail_header("Content-Type: ", ContentType), mail_header("Content-Transfer-Encoding: ", "base64"), mail_header("Content-Disposition: ", "attachment; filename=\""++ FileName++"\""), "\r\n" ]), sendChunk(Rest, S#send{param=file}); _ -> sendChunk(Rest, S#send{param=ignore}) end; sendChunk([{body, Data}|Rest], S) -> case S#send.param of to -> sendChunk(Rest, S#send{to=S#send.to++Data}); cc -> sendChunk(Rest, S#send{cc=S#send.cc++Data}); bcc -> sendChunk(Rest, S#send{bcc=S#send.bcc++Data}); subject -> sendChunk(Rest, S#send{subject=S#send.subject++Data}); attached -> sendChunk(Rest, S#send{attached=S#send.attached++Data}); message -> NewS = smtp_send_part_message(S, Data), sendChunk(Rest, NewS); ignore -> sendChunk(Rest, S); file -> %io:format("sending body chunk\n"), NewS = smtp_send_b64(S, Data), sendChunk(Rest, NewS) end. send(Session, To, Cc, Bcc, Subject, Msg) -> tick_session(Session#session.cookie), RTo = parse_addr(To), RCc = parse_addr(Cc), RBcc = parse_addr(Bcc), Recipients = RTo ++ RCc ++ RBcc, Date = date_and_time_to_string(yaws:date_and_time()), MailDomain = maildomain(), Headers = [mail_header("To: ", To), mail_header("From: ", Session#session.user++"@"++MailDomain), mail_header("Cc: ", Cc), mail_header("Bcc: ", Bcc), mail_header("Subject: ", Subject), mail_header("Content-Type: ", "text/plain"), mail_header("Content-Transfer-Encoding: ", "8bit")], Message = io_lib:format("~sDate: ~s\r\n\r\n~s\r\n.\r\n", [Headers, Date, Msg]), case smtp_send(smtpserver(), Session, Recipients, Message) of ok -> {redirect_local, {rel_path,"mail.yaws"}}; {error, Reason} -> (dynamic_headers() ++ compose(Session, Reason, To, Cc, Bcc, Subject, Msg)) end. mail_header(_Key, []) -> []; mail_header(Key, Val) -> Key++Val++"\r\n". compose(Session) -> compose(Session, "","","","","",""). compose(Session, Reason, To, Cc, Bcc, Subject, Msg) -> tick_session(Session#session.cookie), (dynamic_headers()++ [{ehtml, [{script,[{src,"mail.js"}],[]}, {style, [{type,"text/css"}], "A:link { color: 0;text-decoration: none}\n" "A:visited { color: 0;text-decoration: none}\n" "A:active { color: 0;text-decoration: none}\n" "textarea { background-color: #fff; border: 1px solid 00f; }\n" "DIV.tag-body { background: white; }\n"}, % {script, [{type,"text/javascript"}], % "_editor_url='/htmlarea/';\n" % "_editor_lagn='se';\n"}, % {script, [{type,"text/javascript"},{src,"/htmlarea/htmlarea.js"}],""}, % {script, [{type,"text/javascript"}], % "var editor = null;\n" % "function initEditor() {\n" % "editor = new HTMLArea('html_message');\n" % "editor.generate();\n" % "return false;\n}"}, % {script,[{type,"text/javascript"},{defer,"1"}], %% "HTMLArea.replace('html_message');\n"}, % "HTMLArea.replaceAll();\n"}, {body,[{bgcolor,silver},{marginheight,0},{link,"#000000"}, {topmargin,0},{leftmargin,0},{rightmargin,0}, {marginwidth,0}, % {onload, "initEditor();document.compose.to.focus();"}], {onload, "document.compose.to.focus();"}], [{form, [{name,compose},{action,"send.yaws"},{method,post}, {enctype,"multipart/form-data"} ], [{table, [{border,0},{bgcolor,"c0c0c0"},{cellspacing,0}, {width,"100%"}], {tr,[],{td,[{nowrap,true},{align,left},{valign,middle}], {font, [{size,6},{color,black}], "Yaws WebMail at "++maildomain()}}}}, build_toolbar([{"tool-send.gif", "javascript:setComposeCmd('send');","Send"}, {"", "mail.yaws", "Close"}]), {input,[{type,hidden},{name,attached},{value,"no"}],[]}, {table, [{width,645},{border,0},{bgcolor,silver},{cellspacing,0}, {cellpadding,0}], if Reason == [] -> []; true -> [ {tr,[],[{td,[{colspan,2},{height,35},{align,left}, {valign,top}], {font,[{color,red},{size,2},nowrap], ["Error: ",Reason]}}]} ] end ++ [{tr,[],[{td,[{height,0},{align,left},{valign,top}],[]}, {td,[{height,0},{align,left},{valign,top}],[]}]}, {tr,[],[{td,[{height,35},{align,left},{valign,top}], {font,[{color,"#000000"},{size,2},nowrap], " To: "}}, {td,[{height,35},{align,left},{valign,top}], {input,[{name,to},{type,text},{size,66}, {check,value,quote(To)}]}}]}, {tr,[],[{td,[{height,0},{align,left},{valign,top}],[]}, {td,[{height,0},{align,left},{valign,top}],[]}]}, {tr,[],[{td,[{height,35},{align,left},{valign,top}], {font,[{color,"#000000"},{size,2},nowrap], " Cc: "}}, {td,[{height,35},{align,left},{valign,top}], {input,[{name,cc},{type,text},{size,66}, {check,value,quote(Cc)}]}}]}, {tr,[],[{td,[{height,0},{align,left},{valign,top}],[]}, {td,[{height,0},{align,left},{valign,top}],[]}]}, {tr,[],[{td,[{height,35},{align,left},{valign,top}], {font,[{color,"#000000"},{size,2},nowrap], " Bcc: "}}, {td,[{height,35},{align,left},{valign,top}], {input,[{name,bcc},{type,text},{size,66}, {check,value,quote(Bcc)}]}} ]}, {tr,[],[{td,[{height,35},{align,left},{valign,top},nowrap], {font,[{color,"#000000"},{size,2}], " Subject: "}}, {td,[{colspan,3},{align,left},{valign,top}], {input,[{name,subject},{type,text},{size,66}, {check,value,quote(Subject)}]}}]} ] }, {input,[{type,hidden},{name,message},{value,""}],[]}, {table,[{width,645},{border,0},{cellspacing,0},{cellpadding,0}], {tr,[], [ build_tabs(["Message","Attachments"]), {'div', [{id, "tab-body:0"},{style,"display: block;"}], {table, [{bgcolor,silver},{border,0},{cellspacing,0}, {cellpadding,0}], {tr,[], {td,[{align,left},{valign,top}], [{textarea, [{wrap,virtual}, {name,html_message}, {id,html_message}, {cols,80},{rows,24}], Msg}, % {a, [{href,"javascript:alert(editor.getHTML());"}],"html"}, % " ", % {a, [{href,"javascript:document.compose.foo.innerHTML=editor.getHTML();alert(document.compose.foo.value);"}],"debug"}, % " ", % {a, [{href,"javascript:filur();"}],"debug"}, "" ] } } } }, {'div', [{id, "tab-body:1"},{style,"display: none;"}], {table, [{bgcolor,silver},{border,0},{cellspacing,0}, {cellpadding,0}], {tr,[], {td,[{align,left},{valign,top}], ["Attached files:", {table,[], file_attachements(10) } ] } } } } ] } }, % {textarea, [{wrap,virtual}, % {name,foo}, % {id,foo}, % {cols,80},{rows,24}], % ""}, {input,[{type,hidden},{name,cmd},{value,""}],[]} ] } ] } ] }]). file_attachements(0) -> []; file_attachements(N) -> [file_attachement(N)|file_attachements(N-1)]. file_attachement(N) -> I = integer_to_list(N), {tr,[], [{td,[],"File: "}, {td,[], {input, [{type,"file"},{name,"file"++I},{size,"30"}],[]}} ] }. build_tabs(Tabs) -> [{script,[{type,"text/javascript"}], ["tabCount = ",integer_to_list(length(Tabs)),";\n"]}, {'div', [{align,"left"}], {table,[{border,"0"}, {cellspacing,"0"}, {cellpadding,"0"}], {tr,[], build_tab(Tabs,0)}}}, {'div',[{align,"left"}], {table,[{width,645},{border,0},{cellspacing,0},{cellpadding,0}], {tr,[],{td,[{height,8},{background,"tab-hr.gif"}],[]}}}} ]. build_tab([],_) -> []; build_tab([T|Ts], N=0) -> I = integer_to_list(N), [{td,[{width,6}], {img,[{src,"tab-left_active.gif"}, {border,0}, {id,"tab-left:"++I}],[]}}, {td,[{align,"center"}, {style,"cursor: pointer; background: url(tab-bg_active.gif)"}, {onClick,"changeActiveTab("++I++")"}, {id,"tab-bg:"++I}], T}, {td, [{width,6}], {img,[{src,"tab-right_active.gif"}, {border,0}, {id,"tab-right:"++I}],[]}}| build_tab(Ts,N+1)]; build_tab([T|Ts], N) -> I = integer_to_list(N), [{td,[{width,6}], {img,[{src,"tab-left_inactive.gif"}, {border,0}, {id,"tab-left:"++I}],[]}}, {td,[{align,"center"}, {style,"cursor: pointer; background: url(tab-bg_inactive.gif)"}, {onClick,"changeActiveTab("++I++")"}, {id,"tab-bg:"++I}], T}, {td, [{width,6}], {img,[{src,"tab-right_inactive.gif"}, {border,0}, {id,"tab-right:"++I}],[]}}| build_tab(Ts,N+1)]. showmail(Session, MailNr) -> showmail(Session, MailNr, ?RETRYCOUNT). showmail(Session, MailNr, 0) -> {ehtml,format_error("Mailbox locked by other mail session.")} ; showmail(Session, MailNr, Count) -> MailStr = integer_to_list(MailNr), tick_session(Session#session.cookie), Formated = case retr(popserver(), Session#session.user, Session#session.passwd, MailNr) of {error, Reason} -> case string:str(lowercase(Reason), "lock") of 0 -> format_error(to_string(Reason)); N -> sleep(?RETRYTIMEOUT), showmail(Session, MailNr, Count-1) end; Message -> format_message(Session, Message, MailNr, "1") end, (dynamic_headers() ++ [{ehtml, [{script,[{src,"mail.js"}], []}, {style, [{type,"text/css"}], ".conts { visibility:hidden }\n" "A:link { color: 0;text-decoration: none}\n" "A:visited { color: 0;text-decoration: none}\n" "A:active { color: 0;text-decoration: none}\n" "DIV.msg-body { background: white; }\n" }, {body,[{bgcolor,silver},{marginheight,0},{topmargin,0},{leftmargin,0}, {rightmargin,0},{marginwidth,0}], [{table, [{border,0},{bgcolor,"c0c0c0"},{cellspacing,0}, {width,"100%"}], {tr,[],{td,[{nowrap,true},{align,left},{valign,middle}], {font, [{size,6},{color,black}], "WebMail at "++maildomain()}}}}] ++ Formated } ]}]). list(Session, {Refresh,Sort}) -> list_msg(Session, Refresh, Sort, ?RETRYCOUNT). list_msg(Session, Refresh, Sort, 0) -> {ehtml,format_error("Mailbox locked by other mail process.")}; list_msg(Session, Refresh, Sort, Count) -> tick_session(Session#session.cookie), OldList = Session#session.listing, Listing = if Refresh == true -> list(popserver(), Session#session.user, Session#session.passwd); OldList == undefined -> list(popserver(), Session#session.user, Session#session.passwd); true -> OldList end, Sorting = case Sort of undefined -> Session#session.sorting; _ -> set_sorting(Session#session.cookie, Sort), Sort end, case Listing of {error, Reason} -> case string:str(lowercase(Reason), "lock") of 0 -> {ehtml,format_error(to_string(Reason))}; N -> sleep(?RETRYTIMEOUT), list_msg(Session, Refresh, Sort, Count-1) end; H when Refresh == true -> set_listing(Session#session.cookie, H), {redirect_local, {rel_path, "mail.yaws"}}; H -> if H /= OldList -> set_listing(Session#session.cookie, H); true -> ok end, (dynamic_headers()++ [{ehtml, [{script,[], "function setCmd(val) { \n" " if (val == 'delete') {\n" " var res = confirm('Are you sure you want" " to delete the selected emails?');\n" " if (!res) { \n" " return;\n" " }\n" " }\n" " document.list.cmd.value=val;\n" " document.list.submit();\n" "}" }, {style,[{type,"text/css"}], "A:link { color: black; text-decoration: none}\n" "A:visited { color: black; text-decoration: none}\n" "A:active { color: black; text-decoration: none}\n" ".AList { color: black; text-decoration: none}\n" ".Head { border-right:1px solid white}"}, {form, [{name,list},{action,"listop.yaws"},{method,post}], [{table, [{border,0},{bgcolor,"c0c0c0"}, {cellspacing,0},{width,"100%"}], {tr,[],{td,[{nowrap,true},{align,left},{valign,middle}], {font, [{size,6},{color,black}], "WebMail at "++maildomain()}}}}, build_toolbar([{"tool-newmail.gif","compose.yaws", "New Message"}, {"tool-delete.gif", "javascript:setCmd('delete')", "Delete"}, {"","mail.yaws?refresh=true","Refresh"}, {"","logout.yaws","Logout"}]), {table, [{border,0},{bgcolor,"666666"},{cellspacing,0}, {width,"100%"}], {tr,[],{td,[{nowrap,true},{align,left},{valign,middle}], {font, [{size,2},{color,"#ffffff"}], "Inbox for "++Session#session.user}}}}, {table, [{border,0},{cellspacing,0},{cellpadding,1}, {width,"100%"}], [{tr, [{bgcolor,"c0c0c0"},{valign,middle}], [{th,[{align,left},{valign,middle},{class,head}], {font,[{size,2},{color,black}], sort_href("nr",Sorting,"Nr")}}, {th,[{class,head}], {img,[{src,"view-mark.gif"},{width,13}, {height,13}],[]}}, {th,[{align,left},{valign,middle},{class,head}], {font,[{size,2},{color,black}], sort_href("from",Sorting,"From")}}, {th,[{align,left},{valign,middle},{class,head}], {font,[{size,2},{color,black}], sort_href("subject",Sorting,"Subject")}}, {th,[{align,left},{valign,middle},{class,head}], {font,[{size,2},{color,black}], sort_href("date",Sorting,"Date")}}, {th,[{align,left},{valign,middle},{class,head}], {font,[{size,2},{color,black}], sort_href("size",Sorting,"Size")}}]}] ++ format_summary(H,Sorting)}, {input,[{type,hidden},{name,cmd},{value,""}],[]} ]}]}]) end. sort_href(Sort, Cur, Text) when atom(Cur) -> sort_href(Sort, atom_to_list(Cur), Text); sort_href(Sort, Sort, Text) -> [{a, [{href,"mail.yaws?sort=rev_"++Sort}], Text}, {img, [{src,"up.gif"}]}]; sort_href(Sort, "rev_"++Sort, Text) -> [{a, [{href,"mail.yaws?sort="++Sort}], Text}, {img, [{src,"down.gif"}]}]; sort_href(Sort, Cur, Text) -> {a, [{href,"mail.yaws?sort="++Sort}], Text}. format_summary(Hs,Sorting) -> SHs = sort_summary(Hs, Sorting), [format_summary_line(H) || H <- SHs]. sort_summary(Hs, Sorting) -> lists:sort(fun(A,B) -> summary_compare(A,B,Sorting) end, Hs). summary_compare(A, B, rev_from) -> not(summary_compare(A, B, from)); summary_compare(A, B, rev_date) -> not(summary_compare(A, B, date)); summary_compare(A, B, rev_subject) -> not(summary_compare(A, B, subject)); summary_compare(A, B, rev_nr) -> not(summary_compare(A, B, nr)); summary_compare(A, B, rev_size) -> not(summary_compare(A, B, size)); summary_compare(A,B,size) -> Sa = A#info.size, Sb = B#info.size, if Sa < Sb -> true; Sa > Sb -> false; true -> summary_compare(A,B,date) end; summary_compare(A,B,from) -> Ha = A#info.headers, Hb = B#info.headers, if Ha#mail.from_fmt_lc < Hb#mail.from_fmt_lc -> true; Ha#mail.from_fmt_lc == Hb#mail.from_fmt_lc -> summary_compare(A,B,date); true -> false end; summary_compare(A,B,subject) -> Ha = A#info.headers, Hb = B#info.headers, Sa = Ha#mail.subject_fmt_lc, Sb = Hb#mail.subject_fmt_lc, if Sa < Sb -> true; Sa > Sb -> false; true -> summary_compare(A,B,date) end; summary_compare(A,B,date) -> Ha = A#info.headers, Hb = B#info.headers, Ha#mail.date_pst < Hb#mail.date_pst; summary_compare(A,B,_Nr) -> A#info.nr < B#info.nr. strip_re(" "++Subject) -> strip_re(Subject); strip_re("re:"++Subject) -> strip_re(Subject); strip_re("aw:"++Subject) -> strip_re(Subject); strip_re("ang."++Subject) -> strip_re(Subject); strip_re(Subject) -> Subject. format_summary_line(I) -> H = I#info.headers, {tr, [{align,center},{valign,top}], [{td, [{nowrap,true},{align,left},{valign,top},{class,"List"}], {a, [{href,"showmail.yaws?nr="++integer_to_list(I#info.nr)}], {font,[{size,2},{color,black}],{b,[],integer_to_list(I#info.nr)}}}}, {td, [{nowrap,true},{align,center},{valign,top},{class,"List"}], {input, [{type,checkbox},{name,I#info.nr},{value,yes}],[]}}, {td, [{nowrap,true},{align,left},{valign,top},{class,"List"}], {a, [{href,"showmail.yaws?nr="++integer_to_list(I#info.nr)}], {font,[{size,2},{color,black}],{b,[],H#mail.from_fmt}}}}, {td, [{nowrap,true},{align,left},{valign,top},{class,"List"}], {a, [{href,"showmail.yaws?nr="++integer_to_list(I#info.nr)}], {font,[{size,2},{color,black}],{b,[],H#mail.subject_fmt}}}}, {td, [{nowrap,true},{align,left},{valign,top},{class,"List"}], {a, [{href,"showmail.yaws?nr="++integer_to_list(I#info.nr)}], {font,[{size,2},{color,black}], {b,[],H#mail.date_fmt}}}}, {td, [{nowrap,true},{align,left},{valign,top},{class,"List"}], {a, [{href,"showmail.yaws?nr="++integer_to_list(I#info.nr)}], {font,[{size,2},{color,black}],{b,[],integer_to_list(I#info.size)}}}} ]}. format_from(From0) -> From = lists:flatten(From0), case string:chr(From,$<) of 0 -> string:strip(From); N -> NewF=string:strip(unquote(decode(string:substr(From,1,N-1)))), if NewF == [] -> From; true -> NewF end end. parse_addr(AddrStr) -> Addrs = token_addrs(AddrStr, [], false), Op = fun(From) -> case {string:chr(From,$<),string:chr(From,$>)} of {S,E} when S>0, E>0 -> string:substr(From,S,(E-S)+1); _ -> string:strip(From) end end, Fs = [Op(F) || F <- Addrs]. token_addrs([], [], _) -> []; token_addrs([], Acc, _) -> [lists:reverse(Acc)]; token_addrs([C=$"|R], Acc, true) -> token_addrs(R, [C|Acc], false); token_addrs([C=$"|R], Acc, false) -> token_addrs(R, [C|Acc], true); token_addrs([C=$,|R], Acc, false) -> [lists:reverse(Acc)|token_addrs(R, [], false)]; token_addrs([C|R], Acc, InQuote) -> token_addrs(R, [C|Acc], InQuote). decode(Text) -> decode(Text, []). decode([], Acc) -> lists:reverse(Acc); decode([$=,$?|Rest], Acc) -> decode_scan(Rest, Acc); decode([C|Cs], Acc) -> decode(Cs, [C|Acc]). decode_scan([], Acc) -> lists:reverse(Acc); decode_scan([$?,$b,$?|Rest], Acc) -> decode_b64(Rest,Acc); decode_scan([$?,$B,$?|Rest], Acc) -> decode_b64(Rest,Acc); decode_scan([$?,$q,$?|Rest], Acc) -> decode_q(Rest,Acc); decode_scan([$?,$Q,$?|Rest], Acc) -> decode_q(Rest, Acc); decode_scan([$?,_,$?|Rest], Acc) -> decode(Rest, Acc); decode_scan([_|Rest], Acc) -> decode_scan(Rest, Acc). decode_q([], Acc) -> lists:reverse(Acc); decode_q([$?,$=|Rest], Acc) -> decode(Rest, Acc); decode_q([$=,H1,H2|Rest], Acc) -> case catch yaws:hex_to_integer([H1,H2]) of {'EXIT',_} -> decode_q(Rest, [H2,H1,$=|Acc]); C -> decode_q(Rest, [C|Acc]) end; decode_q([C|Cs], Acc) -> decode_q(Cs, [C|Acc]). decode_b64([],Acc) -> Str = lists:reverse(Acc), case catch base64_2_str(Str) of {'EXIT',_} -> Str; Dec -> Dec end; decode_b64([$?,$=|Rest],Acc) -> Str = lists:reverse(Acc), case catch base64_2_str(Str) of {'EXIT',_} -> Str++decode(Rest); Dec -> Dec ++ decode(Rest) end; decode_b64([C|Rest], Acc) -> decode_b64(Rest,[C|Acc]). unquote([]) -> []; unquote([$"|R]) -> unquote(R); unquote([C|R]) -> [C|unquote(R)]. quote([]) -> []; quote([$"|Cs]) -> ["""|quote(Cs)]; quote([C|Cs]) -> [C|quote(Cs)]. display_login(A, Status) -> (dynamic_headers() ++ [{ehtml, [{body, [{onload,"document.f.user.focus();"}], [{table, [{border,0},{bgcolor,"c0c0c0"},{cellspacing,0}, {width,"100%"}], {tr,[],{td,[{nowrap,true},{align,left},{valign,middle}], {font, [{size,6},{color,black}], "WebMail at "++maildomain()}}}}, io_lib:format("

Your login status is: ~s

", [Status]), {form, [{method,post}, {name,f}, {action, "login.yaws"}, {autocomplete,"off"}], {table,[{cellspacing, "5"}], [{tr, [], [{td, [], {p, [], "Username:"}}, {td, [], {input, [{name, user}, {type, text}, {size, "20"}]}} ]}, {tr, [], [{td, [], {p, [], "Password:"}}, {td, [], {input, [{name, password}, {type, password}, {size, "20"}]}}]}, {tr, [], {td, [{align, "right"}, {colspan, "2"}], {input, [{type, submit}, {value, "Login"}]}}} ]}}] }] }]). logout(Session) -> logout_cookie(Session#session.cookie), (dynamic_headers() ++ [{redirect_local, {rel_path,"mail.yaws"}}]). login(User, Password) -> case stat(popserver(), strip(User), strip(Password)) of {ok, _} -> {ok, new_session(User, Password)}; {error, Reason} -> {error, Reason} end. check_session(A) -> H = A#arg.headers, case yaws_api:find_cookie_val("mailsession", H#headers.cookie) of [] -> display_login(A, "not logged in"); CVal -> case mail:check_cookie(CVal) of error -> display_login(A, "not logged in"); Session -> {ok, Session} end end. strip(Str) -> lists:filter(fun(C)->not(lists:member(C,"\r\n"))end,Str). dynamic_headers() -> [yaws_api:set_content_type("text/html"), {header, {cache_control, "no-store"}}, {header, "Expires: -1"}]. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% %% session server %% tick_session(Cookie) -> session_server(), mail_session_manager ! {tick_session, Cookie}. new_session(User, Password) -> session_server(), mail_session_manager ! {new_session, #session{user=User,passwd=Password}, self()}, receive {session_manager, Cookie} -> Cookie end. check_cookie(Cookie) -> session_server(), mail_session_manager ! {get_session, Cookie, self()}, receive {session_manager, {ok, Session}} -> Session; {session_manager, error} -> error end. set_listing(Cookie, Listing) -> session_server(), mail_session_manager ! {set_listing, Cookie, self(), Listing}, receive {session_manager, listing_added} -> ok; {session_manager, error} -> error end. set_sorting(Cookie, Sorting) -> session_server(), mail_session_manager ! {set_sorting, Cookie, self(), Sorting}, receive {session_manager, sorting_added} -> ok; {session_manager, error} -> error end. logout_cookie(Cookie) -> session_server(), mail_session_manager ! {del_session, Cookie}. session_server() -> case whereis(mail_session_manager) of undefined -> Pid = proc_lib:spawn(?MODULE, session_manager_init, []), register(mail_session_manager, Pid); _ -> done end. session_manager_init() -> {X,Y,Z} = seed(), random:seed(X, Y, Z), session_manager([], now(), read_config()). session_manager(C0, LastGC0, Cfg) -> %% Check GC first to avoid GC starvation. GCDiff = diff(LastGC0,now()), {LastGC, C} = if GCDiff > 5000 -> C2 = session_manager_gc(C0, Cfg), {now(), C2}; true -> {LastGC0, C0} end, receive {get_session, Cookie, From} -> case lists:keysearch(Cookie, 1, C) of {value, {_,Session,_}} -> From ! {session_manager, {ok, Session}}; false -> From ! {session_manager, error} end, session_manager(C, LastGC, Cfg); {new_session, Session, From} -> Cookie = integer_to_list(random:uniform(1 bsl 50)), From ! {session_manager, Cookie}, session_manager([{Cookie, Session#session{cookie=Cookie}, now()}|C], LastGC, Cfg); {tick_session, Cookie} -> case lists:keysearch(Cookie, 1, C) of {value, {Cookie,Session,_}} -> session_manager( lists:keyreplace(Cookie,1,C, {Cookie,Session,now()}), LastGC, Cfg); false -> session_manager(C, LastGC, Cfg) end; {del_session, Cookie} -> C3 = lists:keydelete(Cookie, 1, C), session_manager(C3, LastGC, Cfg); {From, cfg , Req} -> sm_reply(Req, From, Cfg), session_manager(C, LastGC, Cfg); {set_listing, Cookie, From, Listing} -> case lists:keysearch(Cookie, 1, C) of {value, {_,Session,_}} -> S2 = Session#session{listing=Listing}, From ! {session_manager, listing_added}, session_manager(lists:keyreplace( Cookie, 1, C, {Cookie, S2, now()}), LastGC, Cfg); false -> io:format("Error, no session found! ~p\n", [Cookie]), From ! {session_manager, error}, session_manager(C, LastGC, Cfg) end; {set_sorting, Cookie, From, Sorting} -> case lists:keysearch(Cookie, 1, C) of {value, {_,Session,_}} -> S2 = Session#session{sorting=Sorting}, From ! {session_manager, sorting_added}, session_manager(lists:keyreplace( Cookie, 1, C, {Cookie, S2, now()}), LastGC, Cfg); false -> io:format("Error, no session found! ~p\n", [Cookie]), From ! {session_manager, error}, session_manager(C, LastGC, Cfg) end; {session_set_attach_data, From, Cookie, Fname, Ctype, Data} -> case lists:keysearch(Cookie, 1, C) of {value, {_,Session,_}} -> Atts = Session#session.attachments, [A|As] = add_att(Fname, Ctype, Data, Atts), From ! {session_manager, A#satt.num}, S2 = Session#session{attachments = [A|As]}, session_manager(lists:keyreplace( Cookie,1,C, {Cookie,S2,now()}), LastGC, Cfg); false -> session_manager(C, LastGC, Cfg) end; {session_get_attach_data, From, Cookie, Num} -> case lists:keysearch(Cookie, 1, C) of {value, {_,Session,_}} -> Atts = Session#session.attachments, case lists:keysearch(Num, #satt.num, Atts) of false -> From ! {session_manager, error}; {value, A} -> From ! {session_manager, A} end; false -> ignore end, session_manager(C, LastGC, Cfg) after 5000 -> %% garbage collect sessions C3 = session_manager_gc(C, Cfg), session_manager(C3, now(), Cfg) end. add_att(Fname, Ctype, Data, Atts) -> case lists:keysearch(Fname, #satt.filename, Atts) of false -> [#satt{num = length(Atts) + 1, filename = Fname, ctype = Ctype, data = Data} | Atts]; {value, A} when A#satt.data == Data -> [A | lists:keydelete(A#satt.num, #satt.num, Atts)]; {value, A} -> [#satt{num = length(Atts) + 1, filename = Fname, ctype = Ctype, data = Data} | Atts] end. session_manager_gc(C, Cfg) -> lists:zf(fun(Entry={Cookie,Session,Time}) -> Diff = diff(Time,now()), TTL = Cfg#cfg.ttl, if Diff > TTL -> false; true -> {true, Entry} end end, C). sm_reply(ttl, From, Cfg) -> From ! {session_manager, Cfg#cfg.ttl}; sm_reply(popserver, From, Cfg) -> From ! {session_manager, Cfg#cfg.popserver}; sm_reply(smtpserver, From, Cfg) -> From ! {session_manager, Cfg#cfg.smtpserver}; sm_reply(maildomain, From, Cfg) -> From ! {session_manager, Cfg#cfg.maildomain}; sm_reply(sendtimeout, From, Cfg) -> From ! {session_manager, Cfg#cfg.sendtimeout}. req(Req) -> session_server(), mail_session_manager ! {self(), cfg, Req}, receive {session_manager, Reply} -> Reply after 10000 -> exit("No reply from session manager") end. % ttl() -> req(ttl). popserver() -> req(popserver). smtpserver() -> req(smtpserver). maildomain() -> req(maildomain). sendtimeout() -> req(sendtimeout). diff({M1,S1,_}, {M2,S2,_}) -> (M2-M1)*1000000+(S2-S1). seed() -> case (catch list_to_binary( os:cmd("dd if=/dev/urandom ibs=12 count=1 2>/dev/null"))) of <> -> {X, Y, Z}; _ -> now() end. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% retr(Server, User, Password, Nr) -> Req = [ret(Nr)], case pop_request(Req, Server, User, Password) of [{ok,Msg}] -> dot_unescape(Msg); [{error, Reason}] -> {error, Reason} end. parse_message(Msg) -> split_head_body(Msg, []). split_head_body(Msg, Acc) -> case get_next_line(Msg) of {error, Reason} -> {error, Reason}; {[], Rest} -> {lists:reverse(Acc), Rest}; {Line, Rest} -> split_head_body(Rest, [Line|Acc]) end. get_next_line(Data) -> %% io:format("Data = ~p\n", [Data]), get_next_line(Data,[]). get_next_line([D|Ds], Acc) -> case split_reply(D,[]) of more -> get_next_line(Ds, [D|Acc]); {Pre, Rest} when Acc==[] -> {Pre, [Rest|Ds]}; {Pre, Rest} -> {lists:flatten(lists:reverse([Pre|Acc])), [Rest|Ds]} end. stat(Server, User, Password) -> case pop_request([{"STAT",sl}], Server, User, Password) of [{ok, Stat}] -> {ok, Stat}; {error, Reason} -> {error, Reason} end. list(Server, User, Password) -> case pop_request([{"LIST",ml}], Server, User, Password) of [{ok, Stats}] -> Info = lists:reverse([info(S) || S <- Stats]), Req = [top(I#info.nr) || I <- Info], case pop_request(Req, Server, User, Password) of {error, Reason} -> {error, Reason}; Res -> Hdrs = lists:map(fun({ok,Ls}) -> parse_headers(Ls) end, Res), add_hdrs(Info,Hdrs) end; {error, Reason} -> {error, Reason} end. add_hdrs([], []) -> []; add_hdrs([I|Is], [H|Hs]) -> [I#info{headers=H}|add_hdrs(Is,Hs)]. info(Str) -> [NrStr,SizeStr|_] = string:tokens(Str, " \t"), #info{nr=to_int(NrStr),size=to_int(SizeStr)}. top(I) -> {"TOP "++integer_to_list(I)++" 0", ml}. ret(I) -> {"RETR "++integer_to_list(I), sized}. del(I) -> {"DELE "++I, sl}. to_int(Str) -> to_int(Str, 0). to_int([D|Ds], Acc) when D >= $0, D =< $9-> to_int(Ds, Acc*10+D-$0); to_int(_, Acc) -> Acc. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% parse_headers(Lines) -> parse_headers(Lines, #mail{}). parse_headers([], Headers) -> Headers; parse_headers([L1,[$\t|L2]|Lines], Headers) -> parse_headers([L1++" "++L2|Lines], Headers); parse_headers([L1,[$ |L2]|Lines], Headers) -> parse_headers([L1++" "++L2|Lines], Headers); parse_headers([Line|Lines], Headers) -> case string:chr(Line, $:) of 0 -> Headers; N -> Key = lowercase(string:strip(string:sub_string(Line, 1, N-1))), Value = if length(Line) > N+1 -> string:strip(string:sub_string(Line, N+2)); true -> [] end, NewH = add_header(Key, Value, Headers), parse_headers(Lines, NewH) end. parse_header_value(Header) -> [Key|Options] = string:tokens(Header, ";"), Opts = [parse_key_value(O) || O <- Options], {Key,Opts}. parse_key_value(O) -> parse_key_value(O, []). parse_key_value([], Acc) -> {string:strip(lists:reverse(Acc)), []}; parse_key_value([$=|Rest], Acc) -> Value = unquote(string:strip(Rest)), Key = lowercase(string:strip(lists:reverse(Acc))), {Key, Value}; parse_key_value([C|Cs], Acc) -> parse_key_value(Cs, [C|Acc]). lowercase(Str) -> [lowercase_ch(S) || S <- Str]. lowercase_ch(C) when C>=$A, C=<$Z -> C + 32; lowercase_ch(C) -> C. add_header("content-transfer-encoding", Value, H) -> H#mail{transfer_encoding = lowercase(Value)}; add_header("content-type", Value, H) -> H#mail{content_type = parse_header_value(Value)}; add_header("content-disposition", Value, H) -> H#mail{content_disposition = parse_header_value(Value)}; add_header("from", Value, H) -> FromFmt = format_from(Value), H#mail{from = Value, from_fmt = FromFmt, from_fmt_lc = lowercase(FromFmt)}; add_header("to", Value, H) -> H#mail{to = Value}; add_header("cc", Value, H) -> H#mail{cc = Value}; add_header("bcc", Value, H) -> H#mail{bcc = Value}; add_header("subject", Value, H) -> SubjectFmt = lists:flatten(decode(Value)), H#mail{subject = Value, subject_fmt = SubjectFmt, subject_fmt_lc = strip_re(lowercase(SubjectFmt))}; add_header("date", Value, H) -> DatePst = parse_date(Value), H#mail{date = Value, date_pst = DatePst, date_fmt = format_date(DatePst)}; add_header(Other, Value, H) -> H#mail{other = [{Other,Value}| H#mail.other]}. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% pop_request(Command, Server, User, Password) -> proc_lib:spawn_link(?MODULE, ploop, [Command, Server, User, Password, self()]), receive {pop_response, Response} -> Response end. %% %% first authenticate then run a bunch of commands %% ploop(Command, Server, User, Password, From) -> case gen_tcp:connect(Server, 110, [{active, false}, {reuseaddr,true}, binary]) of {ok, Port} -> State = #pstate{port=Port, user=User, pass=Password, cmd=Command, from=From}, ploop(init, State); _ -> {error, "Failed to contact mail server."} end. % ploop(init, State) -> case receive_reply(State) of {ok, Reply, State2} -> psend("USER " ++ State#pstate.user, State#pstate.port), ploop(user, State2); {error, Reason, State2} -> State#pstate.from ! {pop_response, {error, Reason}}, pop_close(State#pstate.port); {more, State2} -> ploop(init, State2) end; ploop(user, State) -> case receive_reply(State) of {ok, Reply, State2} -> psend("PASS " ++ State#pstate.pass, State#pstate.port), ploop(pass, State2); {error, Reason, State2} -> State#pstate.from ! {pop_response, {error, Reason}}, pop_close(State#pstate.port); {more, State2} -> ploop(user, State2) end; ploop(pass, State) -> case receive_reply(State) of {ok, Reply, State2} -> next_cmd(State); {error, Reason, State2} -> State#pstate.from ! {pop_response, {error, Reason}}, pop_close(State#pstate.port); {more, State2} -> ploop(pass, State2) end; ploop(sl, State) -> case receive_reply(State) of {ok, Reply, State2} -> next_cmd(State2#pstate{reply=[{ok,Reply}|State2#pstate.reply]}); {error, Reason, State2} -> next_cmd(State2#pstate{reply=[{error,Reason}| State2#pstate.reply]}); {more, State2} -> ploop(sl, State2) end; ploop(close, State) -> case receive_reply(State) of {ok, Reply, State2} -> ploop(close, State2); {error, _, State2} -> next_cmd(State2); {more, State2} -> ploop(close, State2) end; ploop(sized, State) -> case receive_reply(State) of {ok, Reply, State2} -> case to_int(Reply) of 0 -> ploop(sized_cont, State2#pstate{remain=dot,dotstate=0, lines=[]}); Size -> ploop(sized_cont, State2#pstate{remain=Size,lines=[]}) end; {error, Reason, State2} -> next_cmd(State2#pstate{reply=[{error,Reason}| State2#pstate.reply]}); {more, State2} -> ploop(ml, State2) end; ploop(sized_cont, State) -> case receive_data(State) of {error, Reason, State2} -> next_cmd(State2#pstate{reply=[{error,Reason}| State2#pstate.reply]}); {more, State2} -> ploop(sized_cont, State2); {done, State2} -> Data = lists:reverse(State2#pstate.lines), next_cmd(State2#pstate{reply=[{ok, Data}|State2#pstate.reply]}) end; ploop(ml, State) -> case receive_reply(State) of {ok, Reply, State2} -> ploop(ml_cont, State2#pstate{lines=[]}); {error, Reason, State2} -> next_cmd(State2#pstate{reply=[{error,Reason}| State2#pstate.reply]}); {more, State2} -> ploop(ml, State2) end; ploop(ml_cont, State) -> case receive_reply(State) of {line, Line, State2} -> Lines = State2#pstate.lines, ploop(ml_cont, State2#pstate{lines=[Line|Lines]}); {error, Reason, State2} -> next_cmd(State2#pstate{reply=[{error,Reason}| State2#pstate.reply]}); {more, State2} -> ploop(ml_cont, State2); {done, State2} -> Lines = lists:reverse(State2#pstate.lines), next_cmd(State2#pstate{reply=[{ok, Lines}|State2#pstate.reply]}) end. %% next_cmd(State=#pstate{cmd=Cmd,reply=Reply}) when Cmd==quit -> State#pstate.from ! {pop_response, lists:reverse(Reply)}, gen_tcp:close(State#pstate.port); next_cmd(State=#pstate{cmd=Cmd}) when Cmd==[]-> psend("QUIT", State#pstate.port), ploop(close, State#pstate{cmd=quit}); next_cmd(State=#pstate{cmd=[Cmd|Cmds]}) -> {C,S} = Cmd, psend(C, State#pstate.port), ploop(S, State#pstate{cmd=Cmds}). %% pop_close(Port) -> psend("quit", Port), gen_tcp:close(Port). %% psend(Str, Port) -> gen_tcp:send(Port, Str++"\r\n"). %% receive_reply(State=#pstate{port=Port,acc=Acc,more=false}) -> check_reply(State#pstate.acc, State); receive_reply(State=#pstate{port=Port,acc=Acc,more=true}) -> Res = gen_tcp:recv(Port, 0), case Res of {ok, Bin} -> NAcc = Acc++binary_to_list(Bin), check_reply(NAcc, State); {error, closed} -> {error, "closed", State}; Err -> {error, Err, State} end. %% receive_data(State=#pstate{port=Port,acc=Acc,more=false,remain=Remain}) -> if Remain == dot -> %% look for .\r\n case find_dot(Acc, State#pstate.dotstate) of {more, DotState} -> State2 = State#pstate{acc=[], dotstate=DotState, lines=[Acc|State#pstate.lines], more=true}, {more, State2}; {ok, DotState, Lines, NAcc} -> State2 = State#pstate{acc=NAcc, dotstate=DotState, lines=[Lines|State#pstate.lines], more=false}, {done, State2} end; Remain =< length(Acc) -> {Lines, NAcc} = split_at(Acc, Remain), State2 = State#pstate{acc=NAcc,lines=[Lines|State#pstate.lines], remain=0,more=false}, {done, State2}; true -> Rem = Remain - length(Acc), State2 = State#pstate{acc=[],lines=[Acc|State#pstate.lines], remain=Rem, more=true}, {more, State2} end; receive_data(State=#pstate{port=Port,acc=Acc,more=true}) when length(Acc)>0 -> receive_data(State#pstate{more=false}); receive_data(State=#pstate{port=Port,acc=[],more=true,remain=Remain}) -> Res = gen_tcp:recv(Port, 0), case Res of {ok, Bin} -> Acc = binary_to_list(Bin), if Remain == dot -> case find_dot(Acc, State#pstate.dotstate) of {more, DotState} -> State2 = State#pstate{acc=[], dotstate=DotState, lines=[Acc|State#pstate.lines], more=true}, {more, State2}; {ok, DotState, Lines, NAcc} -> State2 = State#pstate{acc=NAcc, dotstate=DotState, lines=[Lines|State#pstate.lines], more=false}, {done, State2} end; Remain =< length(Acc) -> {Lines, NAcc} = split_at(Acc, Remain), State2 = State#pstate{acc=NAcc, lines=[Lines|State#pstate.lines], remain=0,more=false}, {done, State2}; true -> Rem = Remain - length(Acc), State2 = State#pstate{acc=[], lines=[Acc|State#pstate.lines], remain=Rem, more=true}, {more, State2} end; Err -> {error, Err, State} end. %% check_reply(Str, State) -> case split_reply(Str, []) of {"+OK" ++ Res, Rest} -> NewS = State#pstate{acc=Rest,more=false}, {ok, Res, NewS}; {"-ERR" ++ Res, Rest} -> NewS = State#pstate{acc=Rest,more=false}, {error, Res, NewS}; {".", Rest} -> NewS = State#pstate{acc=Rest,more=false}, {done, NewS}; {"."++Line, Rest} -> NewS = State#pstate{acc=Rest,more=false}, {line, Line, NewS}; {Line, Rest} -> NewS = State#pstate{acc=Rest,more=false}, {line, Line, NewS}; more -> {more, State#pstate{acc=Str, more=true}} end. %% split_reply("\r\n"++Rest, Pre) -> {lists:reverse(Pre), Rest}; split_reply([H|T], Pre) -> split_reply(T, [H|Pre]); split_reply("", Pre) -> more. %% split_at(L,N) -> split_at(L,N,[]). split_at(L,0,Acc) -> {lists:reverse(Acc),L}; split_at([C|Cs], N, Acc) -> split_at(Cs, N-1, [C|Acc]). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% get_val(Key, L, Default) -> case lists:keysearch(Key, 1, L) of {value, {_, undefined}} -> Default; {value, {_, Val}} -> Val; _ -> Default end. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% smtp_init(Server, Session, Recipients) -> {ok, Port} = gen_tcp:connect(Server, 25, [{active, false}, {reuseaddr,true}, binary]), smtp_expect(220, Port, "SMTP server does not respond"), smtp_put("MAIL FROM: " ++ Session#session.user++"@"++maildomain(), Port), smtp_expect(250, Port, "Sender not accepted by mail server"), send_recipients(Recipients,Port), smtp_put("DATA", Port), smtp_expect(354, Port, "Message not accepted by mail server."), {ok, Port}. smtp_close(State) -> smtp_put(".", State#send.port), smtp_expect(250, State#send.port, "Message not accepted by mail server."), gen_tcp:close(State#send.port), ok. smtp_send_part(State, Data) -> gen_tcp:send(State#send.port, Data). smtp_send_part_message(State, Data) -> {LastNL, Escaped} = dot_escape(Data, State#send.line_start), gen_tcp:send(State#send.port, Escaped), State#send{line_start=LastNL}. %% Add an . at all lines starting with a dot. dot_escape(Data, NL) -> dot_escape(Data, NL, []). dot_escape([], NL, Acc) -> {NL, lists:reverse(Acc)}; dot_escape([$.|Rest], true, Acc) -> dot_escape(Rest, false, [$.,$.|Acc]); dot_escape([$\n|Rest], _, Acc) -> dot_escape(Rest, true, [$\n|Acc]); dot_escape([C|Rest], _, Acc) -> dot_escape(Rest, false, [C|Acc]). %% dot_unescape(Data) -> {_,Dt} = dot_unescape(Data, true, []), Dt. dot_unescape([], NL, Acc) -> {NL, lists:reverse(Acc)}; dot_unescape([$.|Rest], true, Acc) -> dot_unescape(Rest, false, Acc); dot_unescape([$\n|Rest], _, Acc) -> dot_unescape(Rest, true, [$\n|Acc]); dot_unescape([L|Rest], NL, Acc) when list(L) -> {NL2, L2} = dot_unescape(L, NL, []), dot_unescape(Rest, NL2, [L2|Acc]); dot_unescape([C|Rest], _, Acc) -> dot_unescape(Rest, false, [C|Acc]). %% smtp_send_b64(State, Data0) -> Data = State#send.estate++Data0, {Rest,B64} = str2b64(Data), gen_tcp:send(State#send.port, B64), State#send{estate=Rest}. smtp_send_b64_final(State) -> Data = State#send.estate, B64 = str2b64_final(Data), gen_tcp:send(State#send.port, B64). smtp_send(Server, Session, Recipients, Message) -> case catch smtp_send2(Server, Session, Recipients, Message) of ok -> ok; {error, Reason} -> {error, Reason}; _ -> {error, "Failed to send message."} end. smtp_send2(Server, Session, Recipients, Message) -> {ok, Port} = gen_tcp:connect(Server, 25, [{active, false}, {reuseaddr,true}, binary]), smtp_expect(220, Port, "SMTP server does not respond"), smtp_put("MAIL FROM: " ++ Session#session.user++"@"++maildomain(), Port), smtp_expect(250, Port, "Sender not accepted by mail server"), send_recipients(Recipients,Port), smtp_put("DATA", Port), smtp_expect(354, Port, "Message not accepted by mail server."), smtp_put(Message, Port), smtp_put(".", Port), smtp_expect(250, Port, "Message not accepted by mail server."), smtp_put("QUIT", Port), ok. send_recipients([], Port) -> ok; send_recipients([R|Rs], Port) -> smtp_put("RCPT TO: " ++ R, Port), smtp_expect(250, Port, io_lib:format("Recipient ~s not accepted.",[R])), send_recipients(Rs, Port). smtp_put(Message, Port) -> gen_tcp:send(Port, [Message,"\r\n"]). smtp_expect(Code, Port, ErrorMsg) -> smtp_expect(Code, Port, [], ErrorMsg). smtp_expect(Code, Port, Acc, ErrorMsg) -> Res = gen_tcp:recv(Port, 0, sendtimeout()), case Res of {ok, Bin} -> NAcc = Acc++binary_to_list(Bin), case string:chr(NAcc, $\n) of 0 -> smtp_expect(Code, Port, NAcc, ErrorMsg); N -> ResponseCode = to_int(NAcc), if ResponseCode == Code -> ok; true -> throw({error, ErrorMsg}) end end; Err -> throw({error, Err}) end. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% str2b64(String) -> str2b64(String, []). str2b64([], Acc) -> {[], lists:reverse(Acc)}; str2b64(String, Acc) -> case str2b64_line(String, []) of {ok, Line, Rest} -> str2b64(Rest, ["\n",Line|Acc]); {more, _} -> {String, lists:reverse(Acc)} end. % str2b64_final(String) -> str2b64_final(String, []). str2b64_final([], Acc) -> lists:reverse(Acc); str2b64_final(String, Acc) -> case str2b64_line(String, []) of {ok, Line, Rest} -> str2b64_final(Rest, ["\n",Line|Acc]); {more, Cont} -> lists:reverse(["\n",str2b64_end(Cont)|Acc]) end. % str2b64_line(S, []) -> str2b64_line(S, [], 0); str2b64_line(S, {Rest,Acc,N}) -> str2b64_line(Rest ++ S, Acc, N). str2b64_line(S, Out, 76) -> {ok,lists:reverse(Out),S}; str2b64_line([C1,C2,C3|S], Out, N) -> O1 = e(C1 bsr 2), O2 = e(((C1 band 16#03) bsl 4) bor (C2 bsr 4)), O3 = e(((C2 band 16#0f) bsl 2) bor (C3 bsr 6)), O4 = e(C3 band 16#3f), str2b64_line(S, [O4,O3,O2,O1|Out], N+4); str2b64_line(S, Out, N) -> {more,{S,Out,N}}. % str2b64_end({[C1,C2],Out,N}) -> O1 = e(C1 bsr 2), O2 = e(((C1 band 16#03) bsl 4) bor (C2 bsr 4)), O3 = e((C2 band 16#0f) bsl 2), lists:reverse(Out, [O1,O2,O3,$=]); str2b64_end({[C1],Out,N}) -> O1 = e(C1 bsr 2), O2 = e((C1 band 16#03) bsl 4), lists:reverse(Out, [O1,O2,$=,$=]); str2b64_end({[],Out,N}) -> lists:reverse(Out); str2b64_end([]) -> []. % base64_2_str(Str) -> b642str(Str, 0, 0, []). b642str([$=|_], Acc, N, Out) -> case N of 2 -> %% If I have seen two characters before the = %% Them I'm encoding one byte lists:reverse([(Acc bsr 4)|Out]); 3 -> %% If I have seen three characters before the = %% Them I'm encoding two bytes B1 = Acc bsr 10, B2 = (Acc bsr 2) band 16#ff, lists:reverse([B2,B1|Out]); _ -> exit({bad,b64,N}) end; b642str([H|T], Acc, N, Out) -> case d(H) of no -> b642str(T, Acc, N, Out); I -> Acc1 = (Acc bsl 6) bor I, case N of 3 -> B1 = Acc1 bsr 16, B2 = (Acc1 band 16#ffff) bsr 8, B3 = (Acc1 band 16#ff), b642str(T, 0, 0, [B3,B2,B1|Out]); _ -> b642str(T, Acc1, N+1, Out) end end; b642str([], 0, 0, Out) -> lists:reverse(Out). d(X) when X >= $A, X =<$Z -> X - $A; d(X) when X >= $a, X =<$z -> X - $a + 26; d(X) when X >= $0, X =<$9 -> X - $0 + 52; d($+) -> 62; d($/) -> 63; d(_) -> no. e(X) when X >= 0, X < 26 -> X + $A; e(X) when X >= 26, X < 52 -> X + $a - 26; e(X) when X >= 52, X < 62 -> X + $0 - 52; e(62) -> $+; e(63) -> $/; e(X) -> erlang:fault({badchar,X}). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% boundary_date() -> dat2str_boundary(yaws:date_and_time()). dat2str_boundary([Y1,Y2, Mo, D, H, M, S | Diff]) -> lists:flatten( io_lib:format("~s_~2.2.0w_~s_~w_~2.2.0w:~2.2.0w:~2.2.0w_~w", [weekday(Y1,Y2,Mo,D), D, int_to_mt(Mo), y(Y1,Y2),H,M,S,random:uniform(5000)])). date_and_time_to_string(DAT) -> case validate_date_and_time(DAT) of true -> dat2str(DAT); false -> exit({badarg, {?MODULE, date_and_time_to_string, [DAT]}}) end. dat2str([Y1,Y2, Mo, D, H, M, S | Diff]) -> lists:flatten( io_lib:format("~s, ~2.2.0w ~s ~w ~2.2.0w:~2.2.0w:~2.2.0w", [weekday(Y1,Y2,Mo,D), D, int_to_mt(Mo), y(Y1,Y2),H,M,S]) ++ case Diff of [Sign,Hd,Md] -> io_lib:format("~c~2.2.0w~2.2.0w", [Sign,Hd,Md]); _ -> [] end). y(Y1, Y2) -> 256 * Y1 + Y2. weekday(Y1,Y2,Mo,D) -> int_to_wd(calendar:day_of_the_week(Y1*256+Y2,Mo,D)). int_to_wd(1) -> "Mon"; int_to_wd(2) -> "Tue"; int_to_wd(3) -> "Wed"; int_to_wd(4) -> "Thu"; int_to_wd(5) -> "Fri"; int_to_wd(6) -> "Sat"; int_to_wd(7) -> "Sun". int_to_mt(1) -> "Jan"; int_to_mt(2) -> "Feb"; int_to_mt(3) -> "Mar"; int_to_mt(4) -> "Apr"; int_to_mt(5) -> "May"; int_to_mt(6) -> "Jun"; int_to_mt(7) -> "Jul"; int_to_mt(8) -> "Aug"; int_to_mt(9) -> "Sep"; int_to_mt(10) -> "Oct"; int_to_mt(11) -> "Nov"; int_to_mt(12) -> "Dec". validate_date_and_time([Y1,Y2, Mo, D, H, M, S | Diff]) when 0 =< Y1, 0 =< Y2, 0 < Mo, Mo < 13, 0 < D, D < 32, 0 =< H, H < 24, 0 =< M, M < 60, 0 =< S, S < 61 -> case check_diff(Diff) of true -> calendar:valid_date(y(Y1,Y2), Mo, D); false -> false end; validate_date_and_time(_) -> false. check_diff([]) -> true; check_diff([$+, H, M]) when 0 =< H, H < 12, 0 =< M, M < 60 -> true; check_diff([$-, H, M]) when 0 =< H, H < 12, 0 =< M, M < 60 -> true; check_diff(_) -> false. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% to_string(Atom) when atom(Atom) -> atom_to_list(Atom); to_string(Integer) when integer(Integer) -> integer_to_list(Integer); to_string(List) -> List. format_error(Reason) -> [build_toolbar([{"","mail.yaws","Close"}]), {p, [], {font, [{size,4},{color,red}],["Error: ", Reason]}}]. format_message(Session, Message, MailNr, Depth) -> {HeadersList,Msg} = parse_message(Message), H = parse_headers(HeadersList), Headers = [[Head,$\n] || Head <- HeadersList], Formated = format_body(Session, H, Msg, Depth), Quoted = quote_format(Session, H, Msg), To = lists:flatten(decode(H#mail.to)), From = lists:flatten(decode(H#mail.from)), Subject = lists:flatten(decode(H#mail.subject)), CC = lists:flatten(decode(H#mail.cc)), ToolBar = if MailNr == -1 -> [{"tool-newmail.gif", "javascript:setCmd('reply');", "Reply"}]; MailNr == attachment -> [{"../tool-newmail.gif", "javascript:setCmd('reply');", "Reply"}]; true -> [{"tool-newmail.gif","compose.yaws","New"}, {"tool-newmail.gif", "javascript:setCmd('reply');", "Reply"}, {"","javascript:changeActive("++Depth++");", "
Headers
" "" }, {"tool-delete.gif","javascript:setCmd('delete');", "Delete"}, {"","mail.yaws","Close"}] end, Action = if MailNr == attachment -> "../reply.yaws"; true -> "reply.yaws" end, [{form, [{name,compose},{action,Action},{method,post}], [build_toolbar(ToolBar), {table,[{width,645},{height,"100%"},{border,0},{bgcolor,silver}, {cellspacing,0},{callpadding,0}], {tr,[],{td,[{valign,top},{height,"1%"}], [{table, [{border,0},{cellspacing,0},{cellpadding,0},{width,"100%"}, {bgcolor,silver}], [{tr,[], [{td,[{valign,middle},{align,left},{width,"15%"}, {height,25}], {font, [{color,"#000000"},{size,2}], {nobr,[]," From: "}}}, {td, [{valign,middle},{align,left}], {font, [{color,"#000000"},{size,2}], [" ", unquote(From)]}}, {td,[{valign,middle},{align,right},{height,"25"}], {font, [{color,"#000000"},{size,2}], {nobr,[]," Sent: "}}}, {td, [nowrap,{valign,middle},{align,right}, {width,"30%"}], {font, [{color,"#000000"},{size,2}], " "++H#mail.date}}]}, {tr,[], [{td,[{valign,top},{align,left},{width,"15%"}, {height,25}], {font, [{color,"#000000"},{size,2}], {nobr,[]," To: "}}}, {td, [{valign,top},{align,left},{width,"100%"}], {font, [{color,"#000000"},{size,2}], [" ", unquote(To)]}}]}, {tr,[], [{td,[{valign,middle},{align,left},{width,"15%"}, {height,25}], {font, [{color,"#000000"},{size,2}], {nobr,[]," Cc: "}}}, {td, [{valign,middle},{align,left},{width,"100%"}], {font, [{color,"#000000"},{size,2}], [" ",CC]}}]}, {tr,[], [{td,[{valign,middle},{align,left},{width,"15%"}, {height,25}], {font, [{color,"#000000"},{size,2}], {nobr,[]," Subject: "}}}, {td, [{valign,middle},{align,left},{width,"100%"}], {font, [{color,"#000000"},{size,2}], [" ",Subject]}}]} ]}, {table, [{width,"100%"},{border,1},{cellpadding,6}, {class,msgbody}], [{tr,[], {td,[{width,"100%"},{height,300},{valign,top}, {bgcolor,white}], {p,[],{font,[{size,3},{id, contents}], [ {'div', [{id,"msg-body:msg"++Depth}, {class,"msg-body"}, {style,"display: block;"}], Formated }, {'div', [{id,"msg-body:hdr"++Depth}, {class,"msg-body"}, {style, "display: none;"}], {pre, [], Headers} } ] } } } } ] } ] } } }] ++ if MailNr == -1 -> []; true -> [{input,[{type,hidden},{name,nr}, {value,MailNr}],[]}] end++ [{input,[{type,hidden},{name,from}, {check,value,yaws_api:url_encode(From)}],[]}, {input,[{type,hidden},{name,to}, {check,value,yaws_api:url_encode(To)}],[]}, {input,[{type,hidden},{name,cc}, {check,value,yaws_api:url_encode(CC)}],[]}, {input,[{type,hidden},{name,bcc}, {check,value,yaws_api:url_encode(decode(H#mail.bcc))}],[]}, {input,[{type,hidden},{name,subject}, {check,value,yaws_api:url_encode(Subject)}],[]}, {input,[{type,hidden},{name,quote}, {check,value,yaws_api:url_encode(Quoted)}],[]}, {input,[{type,hidden},{name,cmd},{value,""}],[]} ] }]. select_alt_body([], [First|_]) -> First; select_alt_body([Prefered|Rest], Bodies) -> case [Body || Body <- Bodies, has_body_type(Prefered,Body)] of [] -> select_alt_body(Rest, Bodies); [First|_] -> First end. has_body_type(Type, {H,B}) -> case H#mail.content_type of {CT, _Ops} -> CTL = lowercase(CT), CTL == Type; _ -> false end. format_body(Session, H, Msg, Depth) -> ContentType = case H#mail.content_type of {CT,Ops} -> {lowercase(CT), Ops}; Other -> Other end, case {ContentType,H#mail.transfer_encoding} of {{"text/html",_}, Encoding} -> Decoded = decode_message(Encoding, Msg), Decoded; {{"text/plain",_}, Encoding} -> Decoded = decode_message(Encoding, Msg), {pre, [], yaws_api:htmlize(wrap_text(Decoded, 80))}; {{"multipart/mixed",Opts}, Encoding} -> {value, {_,Boundary}} = lists:keysearch("boundary",1,Opts), [{Headers,Body}|Parts] = parse_multipart(Msg, Boundary), PartHeaders = lists:foldl(fun({K,V},MH) -> add_header(K,V,MH) end, #mail{}, Headers), [format_body(Session, PartHeaders, Body, Depth++".1"), format_attachements(Session, Parts, Depth)]; {{"multipart/alternative",Opts}, Encoding} -> {value, {_,Boundary}} = lists:keysearch("boundary",1,Opts), Parts = parse_multipart(Msg, Boundary), HParts = lists:map( fun({Head,Body}) -> NewHead = lists:foldl(fun({K,V},MH) -> add_header(K,V,MH) end, #mail{}, Head), {NewHead, Body} end, Parts), {H1,B1} = select_alt_body(["text/html","text/plain"],HParts), format_body(Session, H1,B1,Depth++".1"); {{"multipart/signed",Opts}, Encoding} -> {value, {_,Boundary}} = lists:keysearch("boundary",1,Opts), [{Headers,Body}|Parts] = parse_multipart(Msg, Boundary), PartHeaders = lists:foldl(fun({K,V},MH) -> add_header(K,V,MH) end, #mail{}, Headers), format_body(Session, PartHeaders, Body, Depth++".1"); {{"message/rfc822",Opts}, Encoding} -> Decoded = decode_message(Encoding, Msg), format_message(Session, Decoded, -1, Depth); {{ContT="application/"++_,Opts},Encoding} -> B1 = decode_message(Encoding, Msg), B = list_to_binary(B1), FileName = decode(extraxt_h_info(H)), Cookie = Session#session.cookie, mail_session_manager ! {session_set_attach_data, self(), Cookie, FileName, ContT, B}, receive {session_manager, Num} -> [{table,[{bgcolor, "lightgrey"}], [ {tr,[], {td, [], {h5,[], "Attachments:"}}}, {tr, [], {td, [], {table, [], [{tr,[], {td,[], {a, [{href,io_lib:format( "attachment/~s?nr=~w", [yaws_api:url_encode(FileName), Num])}], FileName}}}]}}}]}] after 10000 -> [] end; {_,_} -> {pre, [], yaws_api:htmlize(wrap_text(Msg, 80))} end. quote_format(Session, H, Msg) -> Text = quote_format_body(Session, H, Msg), From = lists:flatten(decode(H#mail.from)), include_quote(Text, From). quote_format_body(Session, H,Msg) -> ContentType = case H#mail.content_type of {CT,Ops} -> {lowercase(CT), Ops}; Other -> Other end, case {ContentType,H#mail.transfer_encoding} of {{"text/html",_}, Encoding} -> Decoded = decode_message(Encoding, Msg), wrap_text(mail_html:html_to_text(Decoded), 78); {{"text/plain",_}, Encoding} -> Decoded = decode_message(Encoding, Msg), wrap_text(Decoded, 78); {{"multipart/mixed",Opts}, Encoding} -> {value, {_,Boundary}} = lists:keysearch("boundary",1,Opts), [{Headers,Body}|Parts] = parse_multipart(Msg, Boundary), PartHeaders = lists:foldl(fun({K,V},MH) -> add_header(K,V,MH) end, #mail{}, Headers), quote_format_body(Session, PartHeaders, Body); {{"multipart/alternative",Opts}, Encoding} -> {value, {_,Boundary}} = lists:keysearch("boundary",1,Opts), Parts = parse_multipart(Msg, Boundary), HParts = lists:map( fun({Head,Body}) -> NewHead = lists:foldl(fun({K,V},MH) -> add_header(K,V,MH) end, #mail{}, Head), {NewHead, Body} end, Parts), {H1,B1} = select_alt_body(["text/plain","text/html"], HParts), quote_format_body(Session, H1,B1); {{"multipart/signed",Opts}, Encoding} -> {value, {_,Boundary}} = lists:keysearch("boundary",1,Opts), [{Headers,Body}|Parts] = parse_multipart(Msg, Boundary), PartHeaders = lists:foldl(fun({K,V},MH) -> add_header(K,V,MH) end, #mail{}, Headers), quote_format_body(Session, PartHeaders, Body); {{"message/rfc822",_},_} -> ""; {{ContT="application/"++_,_},_} -> ""; {_,_} -> wrap_text(Msg, 78) end. include_quote(Text, From) -> {Quoted, _} = include_quote(Text, [], ">", nl), From++" wrote: \n"++lists:reverse(Quoted). include_quote([], Acc, Prefix, State) -> {Acc, State}; include_quote([L|Text], Acc, Prefix, State) when list(L) -> {Acc1, State1} = include_quote(L, Acc, Prefix, State), include_quote(Text, Acc1, Prefix, State1); include_quote(Text, Acc, Prefix, nl) -> case lists:prefix(Prefix, Text) of true -> include_quote(Text, Prefix++Acc, Prefix, body); false -> include_quote(Text, [$ |Prefix++Acc], Prefix, body) end; include_quote([$\n|Text], Acc, Prefix, body) -> include_quote(Text, [$\n|Acc], Prefix, nl); include_quote([C|Text], Acc, Prefix, body) -> include_quote(Text, [C|Acc], Prefix, body). format_attachements(S, [], _Depth) -> []; format_attachements(S, Bs, Depth) -> [{table,[{bgcolor, "lightgrey"}], [ {tr,[], {td, [], {h5,[], "Attachments:"}}}, {tr, [], {td, [], {table, [], format_attach(S, Bs, Depth)}}}]}]. format_attach(_S, [], Depth) -> []; format_attach(S, [{Headers,B0}|Bs], Depth) -> H = lists:foldl(fun({K,V},MH) -> add_header(K,V,MH) end, #mail{}, Headers), Cookie = S#session.cookie, FileName = decode(extraxt_h_info(H)), HttpCtype = case H#mail.content_type of undefined -> yaws_api:mime_type(FileName); {ContType,Opts} -> case lowercase(ContType) of "text/"++_ -> yaws_api:mime_type(FileName); "application/octet-stream" -> yaws_api:mime_type(FileName); CT -> CT end; _ -> yaws_api:mime_type(FileName) end, B1 = decode_message(H#mail.transfer_encoding, B0), B = list_to_binary(B1), mail_session_manager ! {session_set_attach_data, self(), Cookie, FileName, HttpCtype, B}, receive {session_manager, Num} -> [{tr,[],{td,[], [{a, [{href,io_lib:format("attachment/~s?nr=~w", [yaws_api:url_encode(FileName), Num])}], FileName}, " (", {a, [{href,io_lib:format("attachment/~s?form=text&" "nr=~w", [yaws_api:url_encode(FileName), Num])}],"text"}, ")"]}} | format_attach(S, Bs, Depth)] after 10000 -> format_attach(S, Bs, Depth) end. extraxt_h_info(H) -> L = case {H#mail.content_type, H#mail.content_disposition} of {undefined, undefined} -> []; {undefined, {_, LL}} -> LL; {{_,LL}, undefined} -> LL; {{_,L1}, {_,L2}} -> L1 ++ L2 end, case lists:keysearch("filename", 1, L) of false -> "attachment.txt"; {value, {_, FN}} -> FN end. decode_message("7bit"++_, Msg) -> Msg; decode_message("8bit"++_, Msg) -> Msg; decode_message("base64"++_, Msg) -> case catch base64_2_str(lists:flatten(Msg)) of {'EXIT', _} -> Msg; Decoded -> Decoded end; decode_message("quoted-printable"++_, Msg) -> case catch quoted_2_str(lists:flatten(Msg)) of {'EXIT', Reason} -> io:format("failed to decode quoted-printable ~p\n", [Reason]), Msg; Decoded -> Decoded end; decode_message(_, Msg) -> Msg. quoted_2_str(Msg) -> quoted_2_str(Msg, []). quoted_2_str([], Acc) -> lists:reverse(Acc); quoted_2_str([$=,$\r,$\n|Rest], Acc) -> quoted_2_str_scan(Rest,Acc); quoted_2_str([$=,H1,H2|Rest], Acc) -> case catch yaws:hex_to_integer([H1,H2]) of {'EXIT', _} -> quoted_2_str(Rest, [H2,H1,$=|Acc]); C -> quoted_2_str(Rest, [C|Acc]) end; quoted_2_str([$\r,$\n|Rest], Acc) -> quoted_2_str_scan(Rest, [$\n|Acc]); quoted_2_str([C|Cs], Acc) -> quoted_2_str(Cs, [C|Acc]). quoted_2_str_scan([$ |Rest], Acc) -> quoted_2_str_scan(Rest, Acc); quoted_2_str_scan([$\t|Rest], Acc) -> quoted_2_str_scan(Rest, Acc); quoted_2_str_scan([$\v|Rest], Acc) -> quoted_2_str_scan(Rest, Acc); quoted_2_str_scan(Rest, Acc) -> quoted_2_str(Rest, Acc). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % parse_multipart(Data, Boundary) -> Res = parse_multipart(Data, Boundary, []), process_parts(Res, [], [], []). parse_multipart([], _State, Res) -> Res; parse_multipart([D|Ds], State, Res) -> case yaws_api:parse_multipart(D, State) of {cont, Cont, NewRes} -> parse_multipart(Ds, Cont, Res++NewRes); {result, NewRes} -> Res++NewRes end. process_parts([], [], [], Res) -> lists:reverse(Res); process_parts([{head,{Headers}}|Ps], [], [], Res) -> process_parts(Ps, Headers, [], Res); process_parts([{body,B}|Ps], [], Body, Res) -> % ignore headless body process_parts(Ps, [], [], Res); process_parts([{body,B}|Ps], Head, Body, Res) -> process_parts(Ps, [], [], [{Head, lists:reverse([B|Body])}|Res]); process_parts([{part_body,B}|Ps], Head, Body, Res) -> process_parts(Ps, Head, [B|Body], Res). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % % The text to wrap may be arbitrarily nested. We deal with this % without flattening the whole thing. % wrap_text(Text, Max) -> wrap_text(Text, [], [], [], 0, Max, []). %% wrap_text(Text, ContText, PendingWord, PendingSpace, CurrentCol, WrapCol, Acc) wrap_text([], [], Unwrapped, Space, Col, Max, Acc) -> if Col < Max -> lists:reverse(Acc,add_space(Space,lists:reverse(Unwrapped))); true -> lists:reverse(Acc, [$\n|lists:reverse(Unwrapped)]) end; wrap_text([], Cont, Unwrapped, Space, Col, Max, Acc) -> wrap_text(Cont, [], Unwrapped, Space, Col, Max, Acc); wrap_text([L|Rest], [], Unwrapped, Space, Col, Max, Acc) when list(L) -> wrap_text(L, Rest, Unwrapped, Space, Col, Max, Acc); wrap_text([L|Rest], Cont, Unwrapped, Space, Col, Max, Acc) when list(L) -> wrap_text(L, [Rest|Cont], Unwrapped, Space, Col, Max, Acc); wrap_text([C|Rest], Cont, Unwrapped, Space, Col, Max, Acc) when Col < Max -> case char_class(C) of space -> wrap_text(Rest, Cont, [], C, Col+1, Max, Unwrapped++add_space(Space,Acc)); tab -> wrap_text(Rest, Cont, [], C, Col+8, Max, Unwrapped++add_space(Space,Acc)); nl -> wrap_text(Rest, Cont, [], [], 0, Max, [C|Unwrapped++add_space(Space,Acc)]); text -> wrap_text(Rest, Cont, [C|Unwrapped], Space, Col+1, Max, Acc) end; wrap_text([C|Rest], Cont, Unwrapped, Space, Col, Max, Acc) when Col >= Max -> case char_class(C) of space -> wrap_text(Rest, Cont, [], C, length(Unwrapped), Max, Unwrapped++[$\n|Acc]); tab -> wrap_text(Rest, Cont, [], C, length(Unwrapped), Max, Unwrapped++[$\n|Acc]); nl -> wrap_text(Rest, Cont, [], [], length(Unwrapped), Max, Unwrapped++[$\n|Acc]); text -> wrap_text(Rest, Cont, [C|Unwrapped], Space, Col+1, Max, Acc) end. add_space([], Text) -> Text; add_space(C, Text) -> [C|Text]. char_class($\n) -> nl; char_class($\r) -> nl; char_class($ ) -> space; char_class($\t) -> tab; char_class(O) -> text. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% sleep(X) -> receive xxxxxxx -> ok after X -> ok end. %%%%%%%%%%%%%%%%%%%%%% read cfg file %%%%%%%%%%%%%%%%%% %% def for root is: /etc/mail/yaws-webmail.conf read_config() -> Paths = case yaws:getuid() of {ok, "0"} -> ["/etc/mail/yaws-webmail.conf"]; _ -> [filename:join([os:getenv("HOME"),"yaws-webmail.conf"]), "./yaws-webmail.conf", "/etc/mail/yaws-webmail.conf"] end, case yaws:first(fun(F) -> yaws:exists(F) end, Paths) of false -> error_logger:info_msg("yaws webmail: Can't find no config file .. " "using defaults",[]), #cfg{}; {ok, _, File} -> read_config(File) end. read_config(File) -> error_logger:info_msg("Yaws webmail: Using config file ~s~n", [File]), case file:open(File, [read]) of {ok, FD} -> read_config(FD, #cfg{}, 1, io:get_line(FD, '')); Err -> error_logger:info_msg("Yaws webmail: Can't open config file ... " "using defaults",[]), #cfg{} end. read_config(FD, Cfg, Lno, eof) -> file:close(FD), Cfg; read_config(FD, Cfg, Lno, Chars) -> Next = io:get_line(FD, ''), case yaws_config:toks(Chars) of [] -> read_config(FD, Cfg, Lno+1, Next); ["ttl", '=', IntList] -> case (catch list_to_integer(IntList)) of {'EXIT', _} -> error_logger:info_msg("Yaws webmail: expect integer at " "line ~p", [Lno]), read_config(FD, Cfg, Lno+1, Next); Int -> read_config(FD, Cfg#cfg{ttl = Int}, Lno+1, Next) end; ["popserver", '=', Server] -> read_config(FD, Cfg#cfg{popserver = Server}, Lno+1, Next); ["smtpserver", '=', Domain] -> read_config(FD, Cfg#cfg{smtpserver = Domain}, Lno+1, Next); ["maildomain", '=', Domain] -> read_config(FD, Cfg#cfg{maildomain = Domain}, Lno+1, Next); ["sendtimeout", '=', IntList] -> case (catch list_to_integer(IntList)) of {'EXIT', _} -> error_logger:info_msg("Yaws webmail: expect integer at " "line ~p", [Lno]), read_config(FD, Cfg, Lno+1, Next); Int -> read_config(FD, Cfg#cfg{sendtimeout = Int}, Lno+1, Next) end; [H|_] -> error_logger:info_msg("Yaws webmail: Unexpected tokens ~p at " "line ~w", [H, Lno]), read_config(FD, Cfg, Lno+1, Next) end. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -record(date, {year, month, day, hours, minutes, seconds}). parse_date([]) -> []; parse_date(Date) -> D = parse_date(Date, #date{}), if integer(D#date.year),integer(D#date.month), integer(D#date.day),integer(D#date.hours), integer(D#date.minutes),integer(D#date.seconds) -> {{D#date.year, D#date.month, D#date.day}, {D#date.hours, D#date.minutes, D#date.seconds}}; true -> error end. parse_date([], D) -> D; parse_date([D|Ds], Date) -> case char_type(D) of space -> parse_date(Ds, Date); alpha when Date#date.month == undefined -> case is_month(lowercase([D|Ds])) of false -> parse_date(Ds, Date); {true, M, Rest} -> parse_date(Rest, Date#date{month=M}) end; alpha -> parse_date(Ds, Date); digit -> case parse_time([D|Ds]) of error -> {Number,Rest} = get_number([D|Ds], 0), if Number < 32, Date#date.day == undefined -> parse_date(Rest, Date#date{day=Number}); Number < 50, Date#date.year == undefined -> parse_date(Rest, Date#date{year=Number+2000}); Number < 100, Date#date.year == undefined -> parse_date(Rest, Date#date{year=Number+1900}); Number > 1900, Date#date.year == undefined -> parse_date(Rest, Date#date{year=Number}); true -> parse_date(Rest, Date) end; {Hours, Minutes, Seconds, Rest} -> parse_date(Rest, Date#date{hours=Hours, minutes=Minutes, seconds=Seconds}) end; _ -> parse_date(Ds, Date) end. is_month("jan"++Rest) -> {true, 1, Rest}; is_month("feb"++Rest) -> {true, 2, Rest}; is_month("mar"++Rest) -> {true, 3, Rest}; is_month("apr"++Rest) -> {true, 4, Rest}; is_month("may"++Rest) -> {true, 5, Rest}; is_month("jun"++Rest) -> {true, 6, Rest}; is_month("jul"++Rest) -> {true, 7, Rest}; is_month("aug"++Rest) -> {true, 8, Rest}; is_month("sep"++Rest) -> {true, 9, Rest}; is_month("oct"++Rest) -> {true, 10, Rest}; is_month("nov"++Rest) -> {true, 11, Rest}; is_month("dec"++Rest) -> {true, 12, Rest}; is_month(_) -> false. enc_month(1) -> "Jan"; enc_month(2) -> "Feb"; enc_month(3) -> "Mar"; enc_month(4) -> "Apr"; enc_month(5) -> "May"; enc_month(6) -> "Jun"; enc_month(7) -> "Jul"; enc_month(8) -> "Aug"; enc_month(9) -> "Sep"; enc_month(10) -> "Oct"; enc_month(11) -> "Nov"; enc_month(12) -> "Dec". char_type(D) when D>=$a, D=<$z -> alpha; char_type(D) when D>=$A, D=<$Z -> alpha; char_type(D) when D>=$0, D=<$9 -> digit; char_type($\ ) -> space; char_type($\n) -> space; char_type($\t) -> space; char_type($\v) -> space; char_type(_) -> unknown. get_number([D|Ds], N) when D>=$0, D=<$9 -> get_number(Ds, N*10+(D-$0)); get_number(Rest, N) -> {N, Rest}. parse_time(Time) -> F = fun() -> {Hour,[$:|R1]} = get_number(Time, 0), {Minutes,[$:|R2]} = get_number(R1, 0), {Seconds,R3} = get_number(R2, 0), {Hour, Minutes, Seconds, R3} end, case catch F() of {Hour, Minutes, Seconds, Rest} when integer(Hour), integer(Minutes), integer(Seconds) -> {Hour, Minutes, Seconds, Rest}; _ -> error end. format_date({{Year,Month,Day},{Hour,Minutes,Seconds}}) -> M = enc_month(Month), io_lib:format("~2..0w ~s ~4..0w ~2..0w:~2..0w:~2..0w", [Day, M, Year, Hour, Minutes, Seconds]); format_date(Seconds) when integer(Seconds) -> Zero = calendar:datetime_to_gregorian_seconds({{1970,1,1},{0,0,0}}), Time = Zero + Seconds, Date = calendar:gregorian_seconds_to_datetime(Time), format_date(Date); format_date([]) -> []; format_date(error) -> []. send_attachment(Session, Number) -> mail_session_manager ! {session_get_attach_data, self(), Session#session.cookie, Number}, receive {session_manager, error} -> none; {session_manager, A} -> case A#satt.ctype of "message/rfc822" -> Message = binary_to_list(A#satt.data), Formated = format_message(Session, [Message], attachment, "1"), (dynamic_headers() ++ [{ehtml, [{script,[{src,"../mail.js"}], []}, {style, [{type,"text/css"}], ".conts { visibility:hidden }\n" "A:link { color: 0;text-decoration: none}\n" "A:visited { color: 0;text-decoration: none}\n" "A:active { color: 0;text-decoration: none}\n" "DIV.msg-body { background: white; }\n" }, {body,[{bgcolor,silver}, {marginheight,0},{topmargin,0},{leftmargin,0}, {rightmargin,0},{marginwidth,0}], [{table, [{border,0},{bgcolor,"c0c000"}, {cellspacing,0}, {width,"100%"}], {tr,[],{td,[{nowrap,true},{align,left}, {valign,middle}], {font, [{size,6},{color,black}], "Attachment"}}}}] ++ Formated } ]}]); _ -> {content, A#satt.ctype, A#satt.data} end after 15000 -> exit(normal) end. % send_attachment_plain(Session, Number) -> mail_session_manager ! {session_get_attach_data, self(), Session#session.cookie, Number}, receive {session_manager, error} -> none; {session_manager, A} -> {content, "text/plain", A#satt.data} after 15000 -> exit(normal) end. % basename(FilePath) -> case string:rchr(FilePath, $\\) of 0 -> %% probably not a DOS name filename:basename(FilePath); N -> %% probably a DOS name, remove everything after last \ basename(string:substr(FilePath, N+1)) end. %% getopt(Key, KeyList) -> getopt(Key, KeyList, undefined). getopt(Key, KeyList, Default) -> case lists:keysearch(Key, 1, KeyList) of false -> Default; {value, Tuple} -> Val = element(2,Tuple), if Val == undefined -> Default; true -> Val end end. %% content_type(FileName) -> case yaws_api:mime_type(FileName) of "text/plain" -> "application/octet-stream"; Type -> Type end. %% %% State = find_dot(Data, State) -> find_dot(State, Data, []). find_dot(State, [], Acc) -> {more, State}; find_dot(0, [$\r|R], Acc) -> find_dot(1, R, [$\r|Acc]); find_dot(0, [C|R], Acc) -> find_dot(1, R, [C|Acc]); find_dot(1, [$\n|R], Acc) -> find_dot(2, R, [$\n|Acc]); find_dot(1, R, Acc) -> find_dot(0, R, Acc); find_dot(2, [$.|R], Acc) -> find_dot(3, R, [$\.|Acc]); find_dot(2, R, Acc) -> find_dot(0, R, Acc); find_dot(3, [$\r|R], Acc) -> find_dot(4, R, [$\r|Acc]); find_dot(3, R, Acc) -> find_dot(0, R, Acc); find_dot(4, [$\n|R], Acc) -> {ok, 0, lists:reverse(Acc), R}; find_dot(4, R, Acc) -> find_dot(0, R, Acc).