天天看點

轉 JavaMail發送和接收郵件API(詳解) 一、JavaMail概述: 二、對相關協定的回顧: 三、JavaMail的關鍵對象: 四、JavaMail的使用:

    javamail是由sun定義的一套收發電子郵件的api,不同的廠商可以提供自己的實作類。但它并沒有包含在jdk中,而是作為javaee的一部分。

    廠商所提供的javamail服務程式可以有選擇地實作某些郵件協定,常見的郵件協定包括:

smtp:簡單郵件傳輸協定,用于發送電子郵件的傳輸協定;

pop3:用于接收電子郵件的标準協定;

imap:網際網路消息協定,是pop3的替代協定。

    這三種協定都有對應ssl加密傳輸的協定,分别是smtps,pop3s和imaps。

    除javamail服務提供程式之外,javamail還需要jaf(javabeans activation framework)來處理不是純文字的郵件内容,這包括mime(多用途網際網路郵件擴充)、url頁面和檔案附件等内容。

    mail.jar:此jar檔案包含javamail api和sun提供的smtp、imap和pop3服務提供程式;

    activation.jar:此jar檔案包含jaf api和sun的實作。

    在研究 javamail api 的細則之前,讓我們回顧用于 api 的協定。基本上,您會逐漸熟悉并喜愛的協定有四個:

    * smtp

    * pop

    * imap

    * mime

    您還将碰到 nntp 和其它協定。了解所有協定的基本知識将有助于您了解如何使用 javamail api。雖然不了解這些協定您照樣可以用這個 api,卻不能夠克服那些基礎協定的局限性。如果我們精選的協定不能支援某種性能,javamail api 決不能魔術般的将這種性能添加上去。(您很快就會看到,在處理 pop 時這将成為一個難題。)

    2、smtp

    簡單郵件傳輸協定(simple mail transfer protocol,smtp)由 rfc 821 定義。它定義了發送電子郵件的機制。在 javamail api 環境中,您基于 javamail 的程式将和您的公司或網際網路服務供應商的(internet service provider's,isp's)smtp 伺服器通信。smtp 伺服器會中轉消息給接收方 smtp 伺服器以便最終讓使用者經由 pop 或 imap 獲得。這不是要求 smtp 伺服器成為開放的中繼,盡管 smtp 伺服器支援身份驗證,不過還是得確定它的配置正确。像配置伺服器來中繼消息或添加删除郵件賬号這類任務的實作,javamail api 中并不支援。

    3、pop

    pop 代表郵局協定(post office protocol)。目前用的是版本 3,也稱 pop3,rfc 1939 定義了這個協定。pop 是一種機制,網際網路上大多數人用它得到郵件。它規定每個使用者一個郵箱的支援。這就是它所能做的,而這也造成了許多混淆。使用pop 時,使用者熟悉的許多性能并不是由 pop 協定支援的,如檢視有幾封新郵件消息這一性能。這些性能内建于如 eudora 或microsoft outlook 之類的程式中,它們能記住一些事,諸如最近一次收到的郵件,還能計算出有多少是新的。是以當使用javamail api 時,如果您想要這類資訊,您就必須自己算。

    4、imap

    imap 是更進階的用于接收消息的協定。在 rfc 2060 中被定義,imap 代表網際網路消息通路協定(internet message access protocol),目前用的是版本 4,也稱 imap4。在用到 imap 時,郵件伺服器必需支援這個協定。不能僅僅把使用 pop 的程式用于 imap,并指望它支援 imap 所有性能。假設郵件伺服器支援 imap,基于 javamail 的程式可以利用這種情況 — 使用者在伺服器上有多個檔案夾(folder),并且這些檔案夾可以被多個使用者共享。

    因為有這一更進階的性能,您也許會認為所有使用者都會使用 imap。事實并不是這樣。要求伺服器接收新消息,在使用者請求時發送到使用者手中,還要在每個使用者的多個檔案夾中維護消息。這樣雖然能将消息集中備份,但随着使用者長期的郵件夾越來越大,到磁盤空間耗盡時,每個使用者都會受到損失。使用 pop,就能解除安裝郵件伺服器上儲存的消息了。

    5、mime

    mime 代表多用途網際網路郵件擴充标準(multipurpose internet mail extensions)。它不是郵件傳輸協定。但對傳輸内容的消息、附件及其它的内容定義了格式。這裡有很多不同的有效文檔:rfc 822、rfc 2045、rfc 2046 和 rfc 2047。作為一個javamail api 的使用者,您通常不必對這些格式操心。無論如何,一定存在這些格式而且程式會用到它。

    6、nntp及其他

    因為 javamail api 将供應商和所有其它的東西分開了,您就能輕松添加額外的協定支援。sun 保留了一張第三方供應商清單,他們利用了 sun 不提供超出(out-of-the-box)支援範圍的協定。您會找到 nntp(網絡新聞傳輸協定)[新聞討論區]、s/mime(安全多用途網際網路郵件擴充)及其它支援。

    javamail對收發郵件進行了進階的抽象,形成了一些關鍵的的接口和類,它們構成了程式的基礎,下面我們分别來了解一下這些最常見的對象。

    由于javamail需要和郵件伺服器進行通信,這就要求程式提供許多諸如伺服器位址、端口、使用者名、密碼等資訊,javamail通過properties對象封裝這些屬性西資訊。如下面的代碼封裝了兩個屬性資訊:

<a href="http://my.oschina.net/itblog/blog/298379#">?</a>

1

2

3

<code>properties props = </code><code>new</code> <code>properties();</code>

<code>props.put(</code><code>"mail.smtp.host"</code><code>, </code><code>"smtp.sina.com.cn"</code><code>);</code>

<code>props.put(</code><code>"mail.smtp.auth"</code><code>, </code><code>"true"</code><code>);</code>

    針對不同的的郵件協定,javamail規定了服務提供者必須支援一系列屬性,下表是針對smtp協定的一些常見屬性(屬性值都以string類型進行設定,屬性類型欄僅表示屬性是如何被解析的):

屬性名

屬性類型

說明

mail.stmp.host

string

smtp伺服器位址,如smtp.sina.com.cn

mail.stmp.port

int

smtp伺服器端口号,預設為25

mail.stmp.auth

boolean

smtp伺服器是否需要使用者認證,預設為false

mail.stmp.user

smtp預設的登陸使用者名

mail.stmp.from

預設的郵件發送源位址

mail.stmp.socketfactory.class

socket工廠類類名,通過設定該屬性可以覆寫提供者預設的實作,必須實作javax.net.socketfactory接口

mail.stmp.socketfactory.port

指定socket工廠類所用的端口号,如果沒有規定,則使用預設的端口号

mail.smtp.socketfactory.fallback

設定為true時,當使用指定的socket類建立socket失敗後,将使用java.net.socket建立socket,預設為true

mail.stmp.timeout

i/o連接配接逾時時間,機關為毫秒,預設為永不逾時

    session是一個很容易被誤解的類,這歸咎于混淆視聽的類名。千萬不要以為這裡的session像httpsession一樣代表真實的互動會話,但建立session對象時,并沒有對應的實體連接配接,它隻不過是一對配置資訊的集合。session的主要作用包括兩個方面:

    1)接收各種配置屬性資訊:通過properties對象設定的屬性資訊;

    2)初始化javamail環境:根據javamail的配置檔案,初始化javamail環境,以便通過session對象建立其他重要類的執行個體。

    是以,如果把session更名為configure也許更容易了解一些。javamail提供者在jar包的meta-inf目錄下,通過以下檔案提供了基本配置資訊,以便session能夠根據這個配置檔案加載提供者的實作類:

    javamail.providers和javamail.default.providers;

    javamail.address.map和javamail.default.address.map。

    下面是sun提供者java.mail.default.providers檔案的配置資訊(位于mail.jar中):

4

5

6

7

8

9

<code># javamail imap provider sun microsystems, inc</code>

<code>protocol=imap; type=store; sun.mail.imap.imapstore; vendor=sun microsystems, inc;</code>

<code>protocol=imaps; type=store; sun.mail.imap.imapsslstore; vendor=sun microsystems, inc;</code>

<code># javamail smtp provider sun microsystems, inc</code>

<code>protocol=smtp; type=transport; sun.mail.smtp.smtptransport; vendor=sun microsystems, inc;</code>

<code>protocol=smtps; type=transport; </code><code>class</code><code>=com.sun.mail.smtp.smtpssltransport; vendor=sun microsystems, inc;</code>

<code># javamail pop3 provider sun microsystems, inc</code>

<code>protocol=pop3; type=store; sun.mail.pop3.pop3store; vendor=sun microsystems, inc;</code>

<code>protocol=pop3s; type=store; sun.mail.pop3.pop3sslstore; vendor=sun microsystems, inc;</code>

    這個配置檔案提供了以下四個方面的資訊:

       protocol:協定名稱;

       type:協定類型;

       class:對應該操作類型的實作類;

       vendor:廠商名稱。

    session在加載配置檔案時會按照以下優先級順序進行:

       1)首先使用&lt;java_home&gt;/lib中的javamail.providers;

       2)如果1)不存在相應的配置檔案,使用類路徑下mail.jar中meta-inf目錄下的javamail.providers;

       3)如果2)不存在相應的配置檔案,使用類路徑下的mail.jar中meta-inf目錄下的javamail.default.providers;

       是以開發者可以在&lt;java_home&gt;/lib目錄下提供配置檔案覆寫mail.jar/meta-inf目錄中廠商的配置。但是,一般情況下,我們無須這樣做。

       session通過javamail配置檔案以及程式中設定的properties對象建構一個郵件處理環境,後續的處理将在session基礎上進行。session擁有多個靜态工廠方法用于建立session執行個體。

static session getdefaultinstance(properties props, authenticator authenticator):當jvm中已經存在預設的session執行個體中,直接傳回這個執行個體,否則建立一個新的session執行個體,并将其作為jvm中預設session執行個體。這個api很詭異,我們将對它進行詳細的講解。由于這個預設session執行個體可以被同一個jvm所有的代碼通路到,而session中本身又可能包括密碼、使用者名等敏感資訊在内的所有屬性資訊,是以後續調用也必須傳入和第一次相同的authenticator執行個體,否則将抛出java.lang.securityexception異常。如果第一次調用時authenticator入參為null,則後續調用通過null的authenticator入參或直接使用getdefaultinstance(properties props)即可傳回這個預設的session執行個體。值得一提的是,雖然後續調用也會傳入properties,但新屬性并不會起作用,如果希望采用新的屬性值,則可以通過getdefaultinstance(properties props)建立一個新的session執行個體達到目的。authenticator在這裡承當了兩個功能:首先,對jvm中預設session執行個體進行認證保護,後續調用執行getdefaultinstance(properties props, authenticator authenticator)方法時必須和第一次一樣;其次,在具體和郵件伺服器互動時,又作為認證的資訊;

static session getdefaultinstance(properties props):傳回jvm中預設的session執行個體,如果第一次建立session未指定authenticator入參,後續調用可以使用該通路擷取session;

static session getinstance(properties props, authenticator authenticator):建立一個新的session執行個體,它不會在jvm中被作為預設執行個體共享;

static session getinstance(properties props):根據相關屬性建立一個新的session執行個體,未使用安全認證資訊;

    session是javamail提供者配置檔案以及設定屬性資訊的“容器”,session本身不會和郵件伺服器進行任何的通信。是以在一般情況下,我們僅需要通過getdefaultinstance()擷取一個共享的session執行個體就可以了,下面的代碼建立了一個session執行個體:

<code>properties props = system.getproperties();</code>

<code>props.setproperty(</code><code>"mail.transport.protocol"</code><code>, </code><code>"smtp"</code><code>);              …</code>

<code>session session = session.getdefaultinstance(props);</code>

    郵件操作隻有發送或接收兩種處理方式,javamail将這兩種不同操作描述為傳輸(javax.mail.transport)和存儲(javax.mail.store),傳輸對應郵件的發送,而存儲對應郵件的接收。

    session提供了幾個用于建立transport和store執行個體的方法,在具體講解這些方法之前,我們事先了解一下session建立transport和store的内部機制。我們知道提供者在javamail.providers配置檔案中為每一種支援的郵件協定定義了實作類,session根據協定類型(stmp、pop3等)和郵件操作方式(傳輸和存儲)這兩個資訊就可以定位到一個執行個體類上。比如,指定stmp協定和transport類型後,session就會使用com.sun.mail.smtp.smtptransport實作類建立一個transport執行個體,而指定pop3協定和store類型時,則會使用com.sun.mail.pop3.pop3store執行個體類建立一個store執行個體。session提供了多個重載的gettransport()和getstore()方法,這些方法将根據session中properties屬性設定情況進行工作,影響這兩套方法工作的屬性包括:

mail.transport.protocol

預設的郵件傳輸協定,例如,smtp

mail.store.protocol

預設的存儲郵件協定,例如:pop3

mail.host

預設的郵件服務位址,例如:192.168.67.1

mail.user

預設的登陸使用者名,例如:zapldy

    下面,我們再回頭來了解session的gettransport()和getstore()的重載方法。

transport gettransport():當session執行個體設定了mail.transport.protocol屬性時,該方法傳回對應的transport執行個體,否則抛出javax.mail.nosuchproviderexception。

transport gettransport(string protocol):如果session沒有設定mail.transport.protocol屬性,可以通過該方法傳回指定類型的transport,如transport = session.gettransport(“smtp”)。

    如果session中未包含authenticator,以上兩方法建立的transport執行個體和郵件伺服器互動時必須顯示提供使用者名/密碼的認證資訊。如果authenticator非空,則可以在和郵件伺服器互動時被作為認證資訊使用。除了以上兩種提供認證資訊的方式外,session還可以使用以下的方法為transport提供認證資訊。

transport gettransport(urlname url):使用者可以通過urlname入參指定郵件協定、郵件伺服器、端口、使用者名和密碼資訊,請看下面的代碼:

<code>urlname urln = </code><code>new</code> <code>urlname(“smtp”, “smtp.sina.com.cn”, </code><code>25</code><code>, </code><code>null</code><code>, </code>

<code>        </code><code>“masterspring2”, “spring”);</code>

<code>transport transport = session.gettransport(urln);</code>

    這裡,指定了郵件協定為smtp,郵件伺服器是smtp.sina.com.cn,端口為25,使用者名/密碼為masterspring2/spring。

    消息發送的最後一部分是使用  transport 類。這個類用協定指定的語言發送消息(通常是 smtp)。它是抽象類,它的工作方式與 session 有些類似。僅調用靜态 send() 方法,就能使用類的 預設 版本:

<code>transport.send(message);</code>

    或者,您也可以從針對您的協定的會話中獲得一個特定的執行個體,傳遞使用者名和密碼(如果不必要就不傳),發送消息,然後關閉連接配接。

<code>message.savechanges(); </code><code>// implicit with send()</code>

<code>transport transport = session.gettransport(</code><code>"smtp"</code><code>);</code>

<code>transport.connect(host, username, password);</code>

<code>transport.sendmessage(message, message.getallrecipients());</code>

<code>transport.close();</code>

    後面這種方法在您要發送多條消息時最好,因為它能保持郵件伺服器在消息間的活動狀态。基本 send() 機制為每個方法的調用設定與伺服器獨立的連接配接。

    注意:要觀察傳到郵件伺服器上的郵件指令,請用 session.setdebug(true) 設定調試标志。

    用 session 擷取消息與發送消息開始很相似。但是,在 session 得到後,很可能使用使用者名和密碼或使用 authenticator 連接配接到一個 store。類似于 transport ,您告知 store 使用什麼協定:

<code>// store store = session.getstore("imap");</code>

<code>store store = session.getstore(</code><code>"pop3"</code><code>);</code>

<code>store.connect(host, username, password);</code>

    連接配接到 store 之後,接下來,您就可以擷取一個 folder,您必需先打開它,然後才能讀裡面的消息。

<code>folder folder = store.getfolder(</code><code>"inbox"</code><code>);</code>

<code>folder.open(folder.read_only);</code>

<code>message message[] = folder.getmessages();</code>

    pop3 唯一可以用的檔案夾是 inbox。如果使用 imap,還可以用其它檔案夾。

    注意:sun 的供應商有意變得聰明。雖然 message message[] = folder.getmessages(); 看上去是個很慢的操作,它從伺服器上讀取每一條消息,但僅在你實際需要消息的一部分時,消息的内容才會被檢索。

    一旦有了要讀的 message,您可以用 getcontent() 來擷取其内容,或者用 writeto() 将内容寫入流。getcontent() 方法隻能得到消息内容,而 writeto() 的輸出卻包含消息頭。

<code>system.out.println(((mimemessage)message).getcontent());</code>

    同樣的,你還可以通過相應的api擷取郵件的發信人、收信人、标題等内容:

<code>string from = internetaddress.tostring(msg.getfrom());</code>

<code>string replyto = internetaddress.tostring(msg.getreplyto());</code>

<code>string to = internetaddress.tostring(msg.getrecipients(message.recipienttype.to));</code>

<code>string subject = msg.getsubject();</code>

<code>date sentdate = msg.getsentdate();</code>

<code>date recedate = msg.getreceiveddate();</code>

<code>enumeration headers = msg.getallheaders();</code>

    注意:此種方式擷取到的from、to、subject以及附件名字如果包含非英文字元,則可能為亂碼,是以需要進行轉碼後才能正确顯示,詳細情況請參見4. 8亂碼處理章節。

    一旦讀完郵件,要關閉與 folder 和 store 的連接配接。

<code>folder.close(aboolean);</code>

<code>store.close();</code>

    傳遞給 folder 的 close() 方法的 boolean 表示是否清除已删除的消息進而更新 folder。

    一旦獲得 session 對象,就可以繼續建立要發送的消息。這由 message 類來完成。因為 message 是個抽象類,您必需用一個子類,多數情況下為 javax.mail.internet.mimemessage。mimemessage 是個能了解 mime 類型和頭的電子郵件消息,正如不同 rfc 中所定義的。雖然在某些頭部域非 ascii 字元也能被譯碼,但 message 頭隻能被限制為用 us-ascii 字元。

    要建立一個 message,請将 session 對象傳遞給 mimemessage 構造器:

<code>mimemessage message = </code><code>new</code> <code>mimemessage(session);</code>

    注意:還存在其它構造器,如用按 rfc822 格式的輸入流來建立消息。

     一旦獲得消息,您就可以設定各個部分,因為 message 實作 part 接口(且 mimemessage 實作 mimepart )。設定内容的基本機制是 setcontent() 方法,同時使用參數,分别代表内容和 mime 類型:

<code>message.setcontent(</code><code>"hello"</code><code>, </code><code>"text/plain"</code><code>);</code>

     但如果,您知道您在使用 mimemessage,而且消息是純文字格式,您就可以用 settext() 方法,它隻需要代表實際内容的參數,( mime 類型預設為 text/plain):

<code>message.settext(</code><code>"hello"</code><code>);</code>

    後一種格式是設定純文字消息内容的首選機制。至于發送其它類型的消息,如 html 檔案格式的消息,我們首選前者。

    用 setsubject() 方法設定 subject(主題):

<code>message.setsubject(</code><code>"first"</code><code>);</code>

    下面的代碼示範了建立一個簡單郵件資訊的過程:

<code>message msg = </code><code>new</code> <code>mimemessage(session);</code>

<code>msg.setsubject(</code><code>"test title"</code><code>);</code>

<code>msg.settext(</code><code>"how are you!"</code><code>);</code>

<code>msg.setsentdate(</code><code>new</code> <code>date());</code>

    一旦您建立了 session 和 message,并将内容填入消息後,就可以用 address 确定信件位址了。和 message 一樣,address 也是個抽象類。您用的是 javax.mail.internet.internetaddress 類。

    若建立的位址隻包含電子郵件位址,隻要傳遞電子郵件位址到構造器就行了。

<code>address address = </code><code>new</code> <code>internetaddress(</code><code>"[email protected]"</code><code>);</code>

    若希望名字緊挨着電子郵件顯示,也可以把它傳遞給構造器:

<code>address address = </code><code>new</code> <code>internetaddress(</code><code>"[email protected]"</code><code>, </code><code>"george bush"</code><code>);</code>

     需要為消息的 from 域和 to 域建立位址對象。除非郵件伺服器阻止,沒什麼能阻止你發送一段看上去是來自任何人的消息。

    一旦建立了 address(位址),将它們與消息連接配接的方法有兩種。如果要識别發件人,您可以用 setfrom() 和 setreplyto() 方法。

<code>message.setfrom(address)</code>

    需要消息顯示多個 from 位址,可以使用 addfrom() 方法:

<code>address address[] = ...;</code>

<code>message.addfrom(address);</code>

    若要識别消息 recipient(收件人),您可以使用 addrecipient() 方法。除 address(位址)外,這一方法還請求一個 message.recipienttype。

<code>message.addrecipient(type, address);</code>

     三種預定義的位址類型是:

<code>message.recipienttype.to</code>

<code>message.recipienttype.cc</code>

<code>message.recipienttype.bcc</code>

    如果消息是發給副總統的,同時發送一個副本(carbon copy)給總統夫人,以下做法比較恰當:

<code>address toaddress = </code><code>new</code> <code>internetaddress(</code><code>"[email protected]"</code><code>);</code>

<code>address ccaddress = </code><code>new</code> <code>internetaddress(</code><code>"[email protected]"</code><code>);</code>

<code>message.addrecipient(message.recipienttype.to, toaddress);</code>

<code>message.addrecipient(message.recipienttype.cc, ccaddress);</code>

     javamail api 沒有提供電子郵件位址有效性核查機制。雖然通過程式設計,自己能夠掃描有效字元(如 rfc 822 中定義的)或驗證郵件交換(mail exchange,mx)記錄,但這些功能不屬于 javamail api。

    與 java.net 類一樣,javamail api 也可以利用 authenticator 通過使用者名和密碼通路受保護的資源。對于javamail api 來說,這些資源就是郵件伺服器。javamail authenticator 在 javax.mail 包中,而且它和 java.net 中同名的類 authenticator 不同。兩者并不共享同一個 authenticator,因為javamail api 用于 java 1.1,它沒有 java.net 類别。

    要使用 authenticator,先建立一個抽象類的子類,并從 getpasswordauthentication() 方法中傳回 passwordauthentication 執行個體。建立完成後,您必需向 session 注冊 authenticator。然後,在需要認證的時候,就會通知 authenticator。您可以彈出視窗,也可以從配置檔案中(雖然沒有加密是不安全的)讀取使用者名和密碼,将它們作為 passwordauthentication 對象傳回給調用程式。

<code>// fill props with any information</code>

<code>authenticator auth = </code><code>new</code> <code>myauthenticator();</code>

<code>session session = session.getdefaultinstance(props, auth);</code>

    發送電子郵件消息這一過程包括擷取一個會話,建立并填充一則消息,然後發送。得到 session 時,經由設定傳遞的 properties 對象中的 mail.smtp.host 屬性,可以指定您的 smtp 伺服器:

10

11

12

13

14

15

16

17

18

19

20

21

22

23

<code>string host = ...;</code>

<code>string from = ...;</code>

<code>string to = ...;</code>

<code> </code> 

<code>// get system properties</code>

<code>// setup mail server</code>

<code>props.put(</code><code>"mail.smtp.host"</code><code>, host);</code>

<code>// get session</code>

<code>session session = session.getdefaultinstance(props, </code><code>null</code><code>);</code>

<code>// define message</code>

<code>message.setfrom(</code><code>new</code> <code>internetaddress(from));</code>

<code>message.addrecipient(message.recipienttype.to,</code>

<code>new</code> <code>internetaddress(to));</code>

<code>message.setsubject(</code><code>"hello javamail"</code><code>);</code>

<code>message.settext(</code><code>"welcome to javamail"</code><code>);</code>

<code>// send message</code>

    您應該将代碼放在一個 try-catch 程式塊中,這樣建立和發送消息時就能夠抛出異常。

    注意:如果郵件的from、to、subject以及附件名字如果包含非英文字元,則可能為亂碼,是以需要進行轉碼後才能正确顯示,詳細情況請參見4. 8亂碼處理章節。

   下文介紹javamail不同場景中的基本使用。

    為讀郵件,您擷取一個會話,擷取并連接配接一個用于郵箱的适宜的存儲(store),打開适宜的檔案夾,然後擷取您的消息。同樣,切記完成後關閉連接配接。

24

25

26

27

28

<code>string username = ...;</code>

<code>string password = ...;</code>

<code>// create empty properties</code>

<code>// get the store</code>

<code>// get folder</code>

<code>// get directory</code>

<code>for</code> <code>(</code><code>int</code> <code>i=</code><code>0</code><code>, n=message.length; i&lt;n; i++) {</code>

<code>   </code><code>system.out.println(i + </code><code>": "</code> <code>+ message[i].getfrom()[</code><code>0</code><code>] + </code><code>"/t"</code> <code>+ message[i].getsubject());</code>

<code>}</code>

<code>// close connection</code>

<code>folder.close(</code><code>false</code><code>);</code>

    對每條消息做些什麼由您決定。上面的代碼塊隻是顯示這些消息的發件人和主題。技術上講,from 位址清單可能為空,而 getfrom()[0] 調用會抛出一個異常。

    要顯示全部資訊,您可以在使用者看完 from 和 subject 域之後給出提示,如使用者有需要,就調用消息的 writeto() 方法來實作。

<code>bufferedreader reader = </code><code>new</code> <code>bufferedreader (</code><code>new</code> <code>inputstreamreader(system.in));</code>

<code>   </code><code>system.out.println(i + </code><code>": "</code> <code>+ message[i].getfrom()[</code><code>0</code><code>]</code>

<code>        </code><code>+ </code><code>"/t"</code> <code>+ message[i].getsubject());</code>

<code>   </code><code>system.out.println(</code><code>"do you want to read message? "</code> <code>+</code>

<code>        </code><code>"[yes to read/quit to end]"</code><code>);</code>

<code>   </code><code>string line = reader.readline();</code>

<code>   </code><code>if</code> <code>(</code><code>"yes"</code><code>.equals(line)) {</code>

<code>      </code><code>message[i].writeto(system.out);</code>

<code>   </code><code>} </code><code>else</code> <code>if</code> <code>(</code><code>"quit"</code><code>.equals(line)) {</code>

<code>      </code><code>break</code><code>;</code>

<code>   </code><code>}</code>

    消息的删除涉及使用與消息相關的 flags(标志)。不同 flag 對應不同的狀态,有些由系統定義而有些則由使用者定義。下面列出在内部類 flags.flag 中預定義的标志:

    * flags.flag.answered

    * flags.flag.deleted

    * flags.flag.draft

    * flags.flag.flagged

    * flags.flag.recent

    * flags.flag.seen

    * flags.flag.user

    僅僅因為存在一個标志,并不意味着所有郵件伺服器或供應商都支援這個标志。例如,除了删除消息标志外,pop 協定不再支援其它任何标志。檢查是否存在新郵件,這不是個 pop 任務,而是内建于郵件客戶機的任務。為找出哪些标志能被支援,可以用 getpermanentflags() 向 folder 提出要求。

    要删除消息,您可以設定消息的 deleted flag:

<code>message.setflag(flags.flag.deleted, </code><code>true</code><code>);</code>

     首先,請以 read_write 模式打開 folder:

<code>folder.open(folder.read_write);</code>

    然後,當所有消息的處理完成後,關閉 folder,并傳遞一個 true 值,進而擦除(expunge)有 delete 标志的消息。

<code>folder.close(</code><code>true</code><code>);</code>

    一個 folder 的 expunge() 方法可以用來删除消息。但 sun 的 pop3 供應商不支援。其它供應商有的或許能夠實作這一功能,而有的則不能。imap 供應商極有可能實作此功能。因為 pop 隻支援單個對郵箱的通路,對 sun 的供應商來說,您必需關閉 folder 以删除消息。

    要取消标志,隻要傳遞 false 給 setflag() 方法就行了。想知道是否設定過标志,可以用 isset() 檢查。

    您已經知道 — 如果需要可以用一個 authenticator 提示使用者輸入使用者名和密碼,而不是将使用者名和密碼作為字元串傳遞。在這裡您會明确了解怎樣更充分的使用認證。

    不用主機、使用者名和密碼與 store 相連接配接,而是設定 properties 來擁有主機,然後告訴 session 自定義的 authenticator 執行個體,如下所示:

<code>// setup properties</code>

<code>props.put(</code><code>"mail.pop3.host"</code><code>, host);</code>

<code>// setup authentication, get session</code>

<code>authenticator auth = </code><code>new</code> <code>popupauthenticator();</code>

<code>store.connect();</code>

    然後,您建立一個 authenticator 子類并從 getpasswordauthentication() 方法中傳回 passwordauthentication 對象。下面就是這樣一種實作,其中使用者名和密碼僅占用一個域。(這不是一個 swing 工程教程;隻要将兩部分輸入同一個域,用逗号分隔就行。)

<code>import</code> <code>javax.mail.*;</code>

<code>import</code> <code>javax.swing.*;</code>

<code>import</code> <code>java.util.*;</code>

<code>public</code> <code>class</code> <code>popupauthenticator </code><code>extends</code> <code>authenticator {</code>

<code>  </code><code>public</code> <code>passwordauthentication getpasswordauthentication() {</code>

<code>    </code><code>string username, password;</code>

<code>    </code><code>string result = joptionpane.showinputdialog(</code>

<code>      </code><code>"enter 'username,password'"</code><code>);</code>

<code>    </code><code>stringtokenizer st = </code><code>new</code> <code>stringtokenizer(result, </code><code>","</code><code>);</code>

<code>    </code><code>username = st.nexttoken();</code>

<code>    </code><code>password = st.nexttoken();</code>

<code>    </code><code>return</code> <code>new</code> <code>passwordauthentication(username, password);</code>

<code>  </code><code>}</code>

    因為 popupauthenticator 涉及到 swing,它會啟動 awt 的事件處理線程。這一點基本上要求您在代碼中添加一個對 system.exit() 的調用來終止程式。

    message 類引入一個 reply() 方法來配置一個新 message,包括正确的 recipient(收件人)和添加“re”(如果沒有就添加)的正确的 subject。這樣做并沒有為消息添加新内容,僅僅将 from 或 reply-to(被回複人) 頭複制給新的收件人。這種方法用一個 boolean 參數指定消息隻回複給發件人(false)或回複給全體(true)。

<code>mimemessage reply = (mimemessage)message.reply(</code><code>false</code><code>);</code>

<code>reply.setfrom(</code><code>new</code> <code>internetaddress(</code><code>"[email protected]"</code><code>));</code>

<code>reply.settext(</code><code>"thanks"</code><code>);</code>

<code>transport.send(reply);</code>

    在發送消息時要配置 reply to(被回複人) 位址,可以用 setreplyto() 方法。

    轉發消息有一點棘手。沒有單獨的方法可以調用,您通過對組成消息各部分的處理來組織要轉發的消息。

    一條郵件消息可以由多個部分組成。在處理 mime 消息時,消息中每部分都是 bodypart,再特殊些,是 mimebodypart。不同的 body part(信體部件或正文部件)結合成一個容器,名為 multipart,再特殊些,就是 mimemultipart。要轉發一條消息,您為自己的消息正文建立一個部件,要轉發的消息作為另一部件。并且将兩個部件結合成一個 multipart(多部件)。然後您将這個 multipart 添加到一則已寫好恰當位址的消息中,并發送。

    本質上就是如此。要将一條消息内容複制到另一條,隻要複制 datahandler (javabeans activation framework 中的類)就行了。

29

30

<code>// create the message to forward</code>

<code>message forward = </code><code>new</code> <code>mimemessage(session);</code>

<code>// fill in header</code>

<code>forward.setsubject(</code><code>"fwd: "</code> <code>+ message.getsubject());</code>

<code>forward.setfrom(</code><code>new</code> <code>internetaddress(from));</code>

<code>forward.addrecipient(message.recipienttype.to,</code>

<code>  </code><code>new</code> <code>internetaddress(to));</code>

<code>// create your new message part</code>

<code>bodypart messagebodypart = </code><code>new</code> <code>mimebodypart();</code>

<code>messagebodypart.settext(</code>

<code>  </code><code>"here you go with the original message:/n/n"</code><code>);</code>

<code>// create a multi-part to combine the parts</code>

<code>multipart multipart = </code><code>new</code> <code>mimemultipart();</code>

<code>multipart.addbodypart(messagebodypart);</code>

<code>// create and fill part for the forwarded content</code>

<code>messagebodypart = </code><code>new</code> <code>mimebodypart();</code>

<code>messagebodypart.setdatahandler(message.getdatahandler());</code>

<code>// add part to multi part</code>

<code>// associate multi-part with message</code>

<code>forward.setcontent(multipart);</code>

<code>transport.send(forward);</code>

    附件是郵件消息的相關資源,如通常不包含在消息正文裡文本檔案、電子表格或圖像等。常見的郵件程式,如 eudora 和 pine 之類,可以用 javamail api 将資源 attach(附加) 到您的消息上,就可以在收到消息時得到。

    發送附件非常像轉發消息。您建立各部分以組成完整消息。完成第一部件,即消息正文後,您添加其它部件,其中每個 datahandler 都代表附件,而不是轉發消息情況下的共享處理程式。如果從檔案中讀附件,附件的資料源是 filedatasource。而如果從 url 中讀時,附件的資料源是 urldatasource。一旦存在 datasource,隻要先把它傳遞給 datahandler 構造器,最後再用 setdatahandler() 把它附加到 bodypart。假定您要保留附件的原始檔案名,最終要做的是用 bodypart 的 setfilename() 方法設定與附件相關的檔案名。如下所示:

<code>message message = </code><code>new</code> <code>mimemessage(session);</code>

<code>message.addrecipient(message.recipienttype.to, </code><code>new</code> <code>internetaddress(to));</code>

<code>message.setsubject(</code><code>"hello javamail attachment"</code><code>);</code>

<code>// create the message part</code>

<code>// fill the message</code>

<code>messagebodypart.settext(</code><code>"pardon ideas"</code><code>);</code>

<code>// part two is attachment</code>

<code>datasource source = </code><code>new</code> <code>filedatasource(filename);</code>

<code>messagebodypart.setdatahandler(</code><code>new</code> <code>datahandler(source));</code>

<code>messagebodypart.setfilename(filename);</code>

<code>// put parts in message</code>

<code>message.setcontent(multipart);</code>

<code>// send the message</code>

    就消息引入附件時,若程式是個 servlet (小服務程式),除告知消息發送到何處外,還必需上載附件。可以将 multipart/form-data 表單編碼類型(form encoding type)用于每個上載檔案的處理。

<code>&lt;</code><code>form</code> <code>enctype</code><code>=</code><code>"multipart/form-data"</code> <code>method</code><code>=</code><code>post</code> <code>action</code><code>=</code><code>"/myservlet"</code><code>&gt;</code>

<code>  </code><code>&lt;</code><code>input</code> <code>type</code><code>=</code><code>"file"</code> <code>name</code><code>=</code><code>"thefile"</code><code>&gt;</code>

<code>  </code><code>&lt;</code><code>input</code> <code>type</code><code>=</code><code>"submit"</code> <code>value</code><code>=</code><code>"upload"</code><code>&gt;</code>

<code>&lt;/</code><code>form</code><code>&gt;</code>

    注意:消息大小由 smtp 伺服器而不是 javamail api 來限制。如果您碰到問題,可以考慮用設定 ms 和 mx 參數的方法增大 java 堆大小。

    從消息中擷取附件比發送它們棘手些,因為 mime 沒有簡單的關于附件的概念。當消息包含附件時,消息的内容是個 multipart 對象。接着,您需要處理每個 part,擷取主要内容和附件。标有從 part.getdisposition() 獲得的 part.attachment 配置(disposition)的部件(part)無疑就是附件。但是,沒有配置(以及一個非文本 mime 類型)和帶 part.inline 配置的部件也可能是附件。當配置要麼是 part.attachment,要麼是 part.inline 時,這個消息部件的内容就能被儲存。隻要用 getfilename() 和 getinputstream() 就能分别得到原始檔案名和輸入流。

<code>multipart mp = (multipart)message.getcontent();</code>

<code>for</code> <code>(</code><code>int</code> <code>i=</code><code>0</code><code>, n=multipart.getcount(); i&lt;n; i++) {</code>

<code>  </code><code>part part = multipart.getbodypart(i));</code>

<code>  </code><code>string disposition = part.getdisposition();</code>

<code>  </code><code>if</code> <code>((disposition != </code><code>null</code><code>) &amp;&amp;</code>

<code>       </code><code>((disposition.equals(part.attachment) || (disposition.equals(part.inline))) {</code>

<code>    </code><code>savefile(part.getfilename(), part.getinputstream());</code>

    savefile() 方法僅依據檔案名建立了一個 file,它從輸入流中将位元組讀出,然後寫入到檔案中。萬一檔案已經存在,就在檔案名後添加一個數字作為新檔案名,如果這個檔案名仍存在,則繼續添,直到找不到這樣的檔案名為止。

<code>// from savefile()</code>

<code>file file = </code><code>new</code> <code>file(filename);</code>

<code>for</code> <code>(</code><code>int</code> <code>i=</code><code>0</code><code>; file.exists(); i++) {</code>

<code>  </code><code>file = </code><code>new</code> <code>file(filename+i);</code>

     上面的代碼涵蓋了最簡單的情況 — 消息中各部件恰當的标記了。要涵蓋所有情況,還要在配置為空時進行處理,并且擷取部件的 mime 類型來進行相應處理。

<code>if</code> <code>(disposition == </code><code>null</code><code>) {</code>

<code>  </code><code>// check if plain</code>

<code>  </code><code>mimebodypart mbp = (mimebodypart)part;</code>

<code>  </code><code>if</code> <code>(mbp.ismimetype(</code><code>"text/plain"</code><code>)) {</code>

<code>     </code><code>// handle plain</code>

<code>  </code><code>} </code><code>else</code> <code>{</code>

<code>     </code><code>// special non-attachment cases here of image/gif, text/html, ...</code>

<code>  </code><code>...</code>

    發送基于 html 檔案格式消息的工作量比發送純文字消息多,雖然不一定非要這些多餘的工作量。如何選擇完全取決于給定的請求。

    若您所要做的全部事情是發送一份 html 檔案的等價物作為消息,但讓郵件閱讀者為不能提取任何内嵌圖像或相關片段而擔心的話,可以使用 message 的 setcontent() 方法,把内容當作一個 string 傳入,并将内容類型設定成 text/html。

<code>string htmltext = </code><code>"&lt;h1&gt;hello&lt;/h1&gt;"</code> <code>+ </code><code>"&lt;img src=/"</code><code>http:</code><code>//www.jguru.com/images/logo.gif/"&gt;";</code>

<code>message.setcontent(htmltext, </code><code>"text/html"</code><code>));</code>

    在接收端,如果您用 javamail api 提取消息,api 中沒有内建的顯示 html 消息的東西。 javamail api 隻把它看成一串位元組流。要顯示 html 檔案格式的消息,您必需使用 swing jeditorpane 或其它第三方 html 格式檢視器元件。

<code>if</code> <code>(message.getcontenttype().equals(</code><code>"text/html"</code><code>)) {</code>

<code>    </code><code>string content = (string)message.getcontent();</code>

<code>    </code><code>jframe frame = </code><code>new</code> <code>jframe();</code>

<code>    </code><code>jeditorpane text = </code><code>new</code> <code>jeditorpane(</code><code>"text/html"</code><code>, content);</code>

<code>    </code><code>text.seteditable(</code><code>false</code><code>);</code>

<code>    </code><code>jscrollpane pane = </code><code>new</code> <code>jscrollpane(text);</code>

<code>    </code><code>frame.getcontentpane().add(pane);</code>

<code>    </code><code>frame.setsize(</code><code>300</code><code>, </code><code>300</code><code>);</code>

<code>    </code><code>frame.setdefaultcloseoperation(jframe.dispose_on_close);</code>

<code>    </code><code>frame.show();</code>

    另一方面,如果您想讓 html 檔案格式内容的消息完整(内嵌的圖像作為消息的一部分),您必需把圖像作為附件,并且用一個給定的 cid url 引用圖像,其中 cid 是圖像附件 content-id 頭的引用。

    嵌入圖像的過程與附加檔案到消息的過程非常相似,唯一的差別在于您必需通過設定 mimemultipart 構造器中的子類型(或者說用 setsubtype())告知 mimemultipart 各個相關部件,并且将這個圖像的 content-id 頭設定成随機字元串,作為圖像的 src 在 img 标記中使用。完整的示範如下。

31

32

33

34

<code>string file = ...;</code>

<code>// create the message</code>

<code>// fill its headers</code>

<code>message.setsubject(</code><code>"embedded image"</code><code>);</code>

<code>string htmltext = </code><code>"&lt;h1&gt;hello&lt;/h1&gt;"</code> <code>+</code>

<code>  </code><code>"&lt;img src=/"</code><code>cid:memememe/</code><code>"&gt;"</code><code>;</code>

<code>messagebodypart.setcontent(htmltext, </code><code>"text/html"</code><code>);</code>

<code>// create a related multi-part to combine the parts</code>

<code>mimemultipart multipart = </code><code>new</code> <code>mimemultipart(</code><code>"related"</code><code>);</code>

<code>// create part for the image</code>

<code>// fetch the image and associate to part</code>

<code>datasource fds = </code><code>new</code> <code>filedatasource(file);</code>

<code>messagebodypart.setdatahandler(</code><code>new</code> <code>datahandler(fds));</code>

<code>messagebodypart.setheader(</code><code>"content-id"</code><code>,</code><code>"memememe"</code><code>);</code>

<code>// add part to multi-part</code>

    值得一提的是,mime要解決的一個問題是将smtp協定不支援的位元組流轉換成為smtp協定支援的位元組流。比如我們要通過郵件傳輸一個附件文檔,該附件文檔就是一個8bit 位元組流,如果簡單的直接通過smtp 發送,其最高位資訊将被丢失。mime規定可以用兩種編碼方式将8bit 的位元組流編碼成為低于8bit 的位元組流,它們分别是base64 編碼(base64 将8bit 位元組流編碼成6bit 位元組流)和qp 編碼。這兩種編碼方式同樣應用在對中文的編碼上。例如如果郵件中文題目叫做“cvs 介紹”,那麼其編碼後的形式可能為:

<code>subject: =?gb2312?b?q1ztls3qpmx0lnbwda==?=</code>

    标題字元串以”=?”開始,以”?=”結束。”gb2312”表示字元串的字元集,而以”?”分隔的”b”就表示此字元串的編碼方式為base64。我們處理此标題時就要先将base64編碼的6bit 位元組流轉換為原來的8bit 位元組流,再根據字元集”gb2312”轉換為java 中的string 類型。這裡可以簡單的使用javamail 提供的mimeutility.decodetext()靜态方法将編碼後的字元串解碼。這裡我寫了一個簡單的解碼工具方法:

<code>public</code> <code>static</code> <code>string mimedecodestring(string res) {</code>

<code>    </code><code>if</code><code>(res != </code><code>null</code><code>) {</code>

<code>        </code><code>res = res.trim();</code>

<code>        </code><code>try</code> <code>{</code>

<code>            </code><code>if</code> <code>(from.startswith(</code><code>"=?gb"</code><code>) || from.startswith(</code><code>"=?gb"</code><code>)</code>

<code>                    </code><code>|| from.startswith(</code><code>"=?utf"</code><code>) || from.startswith(</code><code>"=?utf"</code><code>)) {</code>

<code>                </code><code>res = mimeutility.decodetext(from);</code>

<code>            </code><code>}</code>

<code>        </code><code>} </code><code>catch</code> <code>(exception e) {</code>

<code>            </code><code>logger.error(</code><code>"decode string error. origin string is: "</code> <code>+ res, e);</code>

<code>        </code><code>}</code>

<code>        </code><code>return</code> <code>res;</code>

<code>    </code><code>}</code>

<code>    </code><code>return</code> <code>null</code><code>;</code>

    如果接收郵件擷取到的結果是全英文的,則不會出現亂碼,在此方法中亦不會進行解碼。同樣,在發送郵件時,如果附件或者主題中帶有中文,則需要使用mimeutility.encodetext()靜态方法進行編碼,此處不再贅述。