Copybara import of the project: - beab9265aa39a4c3c4a2e220b1e4c198060803a5 Initial Contribution by Jayasheelan Kumar <jayasheelan.kumar@oracle.com> - 6adf99452b0aef38e6b31f885f7e4bbcc2e59b55 https://github.com/eclipse-ee4j/javamail/issues/336 relea... by Scott Marlow <smarlow@redhat.com> - af160fc3ef126bda42001b04c4ab5157ed084b97 Changed groupId and artifactId. Fixes #338 (#340) by Guillermo González de Agüero <ggam@users.noreply.github.com> - cb505e119af47844efd5a3c1860d7df9e8db60de Update to newest parent pom. (#341) by Bill Shannon <bill.shannon@oracle.com> - 22daa79f6b516a23840636ee28c401ae35b31771 Switch to jakarta coordinates. by Bill Shannon <bill.shannon@oracle.com> - f8f6ee8b62198ba758b2e2289591b3407f6eae22 Update parent pom version and update JAF version. by Bill Shannon <bill.shannon@oracle.com> - ecc40b842820f62347938635cb0da764c4f6ccec Update documentation for the move to Eclipse - fix #348 by Bill Shannon <bill.shannon@oracle.com> - 0977a71f1dd44b00a05cdbe8fd2a15e6b7b7591b JavaMail 1.6.3 Final Release. by Bill Shannon <bill.shannon@oracle.com> - fc43f4d6679ec43cbabb75c31b3af863fd0bff9e Update dependency javax.mail -> jakarta.mail. by Bill Shannon <bill.shannon@oracle.com> - d08a9e505bd1382ef32fbe69cfc18d02756d877a Change Java platform module system name to jakarta.mail. by Bill Shannon <bill.shannon@oracle.com> - 7be4f44afd920e35ede59aa06221cedc7b9bd12c Don't include a version in the OSGi javax.activation depe... by Bill Shannon <bill.shannon@oracle.com> GitOrigin-RevId: 7be4f44afd920e35ede59aa06221cedc7b9bd12c Change-Id: I310600fedd79de9ce07c9dd07d3de3bd2503ff34
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cb3fd73 --- /dev/null +++ b/.gitignore
@@ -0,0 +1,6 @@ +target/ +*/target/ +nbproject/private/ +.m2/ +webrev +src/main/java/javax/mail/Version\.java
diff --git a/.hgignore b/.hgignore new file mode 100644 index 0000000..e774dec --- /dev/null +++ b/.hgignore
@@ -0,0 +1,6 @@ +^target/ +/target/ +^nbproject/private/ +^\.m2/ +^webrev +^src/main/java/javax/mail/Version\.java$
diff --git a/.hgtags b/.hgtags new file mode 100644 index 0000000..4fcda51 --- /dev/null +++ b/.hgtags
@@ -0,0 +1,33 @@ +da19cbb2014590348273983815b35ac8a48717c5 JAVAMAIL-1_4_1 +dfb709919353eec53e9dca61b379163186f4d154 1.4.2-beta +24c89d103755d49d62c67368cea5c13b39c2b399 JAVAMAIL-1_4_2 +dfaa56e47386544f4002ade511f2cb0207107734 JAVAMAIL-1_4_3-RC1 +200b5a729cff286d1288b64287dc339233cd5040 JAVAMAIL-1_4_3 +200b5a729cff286d1288b64287dc339233cd5040 JAVAMAIL-1_4_3 +1f3f699ed17aa2c740eb09def5d83dd80671748a JAVAMAIL-1_4_3 +0a99728ee1d1d3d2b27ab76e0db05d9652ee1063 JAVAMAIL-1_4_4-RC1 +9cff25c6d73c8f86ff27f4cb39f7c3c092c34dd7 JAVAMAIL-1_4_4 +ce00d5900757397cbc7943b686035e8b27aa9449 JAVAMAIL-1_4_5-RC1 +e0ece38db9a0a6b46fe03575bffba6605603761d JAVAMAIL-1_4_5 +da6e25608ec175cf0556c6b7b7d1fee40cbe09c5 JAVAMAIL-1_4_6-RC1 +b9c00b0cf3cf560077cd3c393c597e4ed9f1bf14 JAVAMAIL-1_4_6 +b9c00b0cf3cf560077cd3c393c597e4ed9f1bf14 JAVAMAIL-1_4_6 +3dba8c78634fcb1ffd9a32e840bcfab91a83a551 JAVAMAIL-1_4_6 +ba4e734fdfa9916c0e8867b8ea8b4218e276be0e JAVAMAIL-1_4_7 +fa1edb81787d0b26828cee438a6d54df619fa6da JAVAMAIL-1_5_0 +f6c4d3181f06de080bb3b107382464fad875dfdb JAVAMAIL-1_5_1 +1cbe387fd6841ae0f1c5c6da1e7f872c49255f1d JAVAMAIL-1_5_2 +a63663b1bb07ae812f9cf13b03fbcf3e8e129521 JAVAMAIL-1_5_3 +15b5ba9b58712d064178b2530a2b7f89e1d2ebc1 JAVAMAIL-1_5_4 +6f854b7031906581f1c91bf70a13734cbec18e0a JAVAMAIL-1_5_5 +ddcb8608cc654e273da1cd11969a41d60c7c07db JAVAMAIL-1_5_6 +d90b0230ffde36e71d06cc8efc35c1bc662d3731 JAVAMAIL-1_6_0-RC1 +946643021f7a4facdbf1d98238b2cfee28e656ef JAVAMAIL-1_6_0-RC2 +e4cb3b3d3aeefd1afab7e965317604bcd6644700 JAVAMAIL-1_6_0 +dfb709919353eec53e9dca61b379163186f4d154 1.4.2-beta +0000000000000000000000000000000000000000 1.4.2-beta +48f8142d2dc471a93866f1da6c13436371d9024c JAVAMAIL-1_6_1 +d8410291f8ed108e94ce0664e1f1a01d9a52e14a JAVAMAIL_1_6_2 +d8410291f8ed108e94ce0664e1f1a01d9a52e14a JAVAMAIL_1_6_2 +0000000000000000000000000000000000000000 JAVAMAIL_1_6_2 +3e658a0fb7b8d36912df0b29f9868946fbffc19c JAVAMAIL-1_6_2
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..db1f146 --- /dev/null +++ b/CONTRIBUTING.md
@@ -0,0 +1,45 @@ +# Contributing to Eclipse Project for JavaMail + +Thanks for your interest in this project. + +## Project description + +The JavaMail API provides a platform-independent and protocol-independent +framework to build mail and messaging applications. The JavaMail API is +available as an optional package for use with the Java SE platform and is also +included in the Java EE platform. + +* https://projects.eclipse.org/projects/ee4j.javamail + +## Developer resources + +Information regarding source code management, builds, coding standards, and +more. + +* https://projects.eclipse.org/projects/ee4j.javamail/developer + +The project maintains the following source code repositories + +* https://github.com/eclipse-ee4j/javamail + +## Eclipse Contributor Agreement + +Before your contribution can be accepted by the project team contributors must +electronically sign the Eclipse Contributor Agreement (ECA). + +* http://www.eclipse.org/legal/ECA.php + +Commits that are provided by non-committers must have a Signed-off-by field in +the footer indicating that the author is aware of the terms by which the +contribution has been provided to the project. The non-committer must +additionally have an Eclipse Foundation account and must have a signed Eclipse +Contributor Agreement (ECA) on file. + +For more information, please see the Eclipse Committer Handbook: +https://www.eclipse.org/projects/handbook/#resources-commit + +## Contact + +Contact the project developers via the project's "dev" list. + +* https://accounts.eclipse.org/mailing-list/javamail-dev
diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..5de3d1b --- /dev/null +++ b/LICENSE.md
@@ -0,0 +1,637 @@ +# Eclipse Public License - v 2.0 + + THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE + PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION + OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + + 1. DEFINITIONS + + "Contribution" means: + + a) in the case of the initial Contributor, the initial content + Distributed under this Agreement, and + + b) in the case of each subsequent Contributor: + i) changes to the Program, and + ii) additions to the Program; + where such changes and/or additions to the Program originate from + and are Distributed by that particular Contributor. A Contribution + "originates" from a Contributor if it was added to the Program by + such Contributor itself or anyone acting on such Contributor's behalf. + Contributions do not include changes or additions to the Program that + are not Modified Works. + + "Contributor" means any person or entity that Distributes the Program. + + "Licensed Patents" mean patent claims licensable by a Contributor which + are necessarily infringed by the use or sale of its Contribution alone + or when combined with the Program. + + "Program" means the Contributions Distributed in accordance with this + Agreement. + + "Recipient" means anyone who receives the Program under this Agreement + or any Secondary License (as applicable), including Contributors. + + "Derivative Works" shall mean any work, whether in Source Code or other + form, that is based on (or derived from) the Program and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. + + "Modified Works" shall mean any work in Source Code or other form that + results from an addition to, deletion from, or modification of the + contents of the Program, including, for purposes of clarity any new file + in Source Code form that contains any contents of the Program. Modified + Works shall not include works that contain only declarations, + interfaces, types, classes, structures, or files of the Program solely + in each case in order to link to, bind by name, or subclass the Program + or Modified Works thereof. + + "Distribute" means the acts of a) distributing or b) making available + in any manner that enables the transfer of a copy. + + "Source Code" means the form of a Program preferred for making + modifications, including but not limited to software source code, + documentation source, and configuration files. + + "Secondary License" means either the GNU General Public License, + Version 2.0, or any later versions of that license, including any + exceptions or additional permissions as identified by the initial + Contributor. + + 2. GRANT OF RIGHTS + + a) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free copyright + license to reproduce, prepare Derivative Works of, publicly display, + publicly perform, Distribute and sublicense the Contribution of such + Contributor, if any, and such Derivative Works. + + b) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free patent + license under Licensed Patents to make, use, sell, offer to sell, + import and otherwise transfer the Contribution of such Contributor, + if any, in Source Code or other form. This patent license shall + apply to the combination of the Contribution and the Program if, at + the time the Contribution is added by the Contributor, such addition + of the Contribution causes such combination to be covered by the + Licensed Patents. The patent license shall not apply to any other + combinations which include the Contribution. No hardware per se is + licensed hereunder. + + c) Recipient understands that although each Contributor grants the + licenses to its Contributions set forth herein, no assurances are + provided by any Contributor that the Program does not infringe the + patent or other intellectual property rights of any other entity. + Each Contributor disclaims any liability to Recipient for claims + brought by any other entity based on infringement of intellectual + property rights or otherwise. As a condition to exercising the + rights and licenses granted hereunder, each Recipient hereby + assumes sole responsibility to secure any other intellectual + property rights needed, if any. For example, if a third party + patent license is required to allow Recipient to Distribute the + Program, it is Recipient's responsibility to acquire that license + before distributing the Program. + + d) Each Contributor represents that to its knowledge it has + sufficient copyright rights in its Contribution, if any, to grant + the copyright license set forth in this Agreement. + + e) Notwithstanding the terms of any Secondary License, no + Contributor makes additional grants to any Recipient (other than + those set forth in this Agreement) as a result of such Recipient's + receipt of the Program under the terms of a Secondary License + (if permitted under the terms of Section 3). + + 3. REQUIREMENTS + + 3.1 If a Contributor Distributes the Program in any form, then: + + a) the Program must also be made available as Source Code, in + accordance with section 3.2, and the Contributor must accompany + the Program with a statement that the Source Code for the Program + is available under this Agreement, and informs Recipients how to + obtain it in a reasonable manner on or through a medium customarily + used for software exchange; and + + b) the Contributor may Distribute the Program under a license + different than this Agreement, provided that such license: + i) effectively disclaims on behalf of all other Contributors all + warranties and conditions, express and implied, including + warranties or conditions of title and non-infringement, and + implied warranties or conditions of merchantability and fitness + for a particular purpose; + + ii) effectively excludes on behalf of all other Contributors all + liability for damages, including direct, indirect, special, + incidental and consequential damages, such as lost profits; + + iii) does not attempt to limit or alter the recipients' rights + in the Source Code under section 3.2; and + + iv) requires any subsequent distribution of the Program by any + party to be under a license that satisfies the requirements + of this section 3. + + 3.2 When the Program is Distributed as Source Code: + + a) it must be made available under this Agreement, or if the + Program (i) is combined with other material in a separate file or + files made available under a Secondary License, and (ii) the initial + Contributor attached to the Source Code the notice described in + Exhibit A of this Agreement, then the Program may be made available + under the terms of such Secondary Licenses, and + + b) a copy of this Agreement must be included with each copy of + the Program. + + 3.3 Contributors may not remove or alter any copyright, patent, + trademark, attribution notices, disclaimers of warranty, or limitations + of liability ("notices") contained within the Program from any copy of + the Program which they Distribute, provided that Contributors may add + their own appropriate notices. + + 4. COMMERCIAL DISTRIBUTION + + Commercial distributors of software may accept certain responsibilities + with respect to end users, business partners and the like. While this + license is intended to facilitate the commercial use of the Program, + the Contributor who includes the Program in a commercial product + offering should do so in a manner which does not create potential + liability for other Contributors. Therefore, if a Contributor includes + the Program in a commercial product offering, such Contributor + ("Commercial Contributor") hereby agrees to defend and indemnify every + other Contributor ("Indemnified Contributor") against any losses, + damages and costs (collectively "Losses") arising from claims, lawsuits + and other legal actions brought by a third party against the Indemnified + Contributor to the extent caused by the acts or omissions of such + Commercial Contributor in connection with its distribution of the Program + in a commercial product offering. The obligations in this section do not + apply to any claims or Losses relating to any actual or alleged + intellectual property infringement. In order to qualify, an Indemnified + Contributor must: a) promptly notify the Commercial Contributor in + writing of such claim, and b) allow the Commercial Contributor to control, + and cooperate with the Commercial Contributor in, the defense and any + related settlement negotiations. The Indemnified Contributor may + participate in any such claim at its own expense. + + For example, a Contributor might include the Program in a commercial + product offering, Product X. That Contributor is then a Commercial + Contributor. If that Commercial Contributor then makes performance + claims, or offers warranties related to Product X, those performance + claims and warranties are such Commercial Contributor's responsibility + alone. Under this section, the Commercial Contributor would have to + defend claims against the other Contributors related to those performance + claims and warranties, and if a court requires any other Contributor to + pay any damages as a result, the Commercial Contributor must pay + those damages. + + 5. NO WARRANTY + + EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT + PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" + BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR + IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF + TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR + PURPOSE. Each Recipient is solely responsible for determining the + appropriateness of using and distributing the Program and assumes all + risks associated with its exercise of rights under this Agreement, + including but not limited to the risks and costs of program errors, + compliance with applicable laws, damage to or loss of data, programs + or equipment, and unavailability or interruption of operations. + + 6. DISCLAIMER OF LIABILITY + + EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT + PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS + SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST + PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE + EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGES. + + 7. GENERAL + + If any provision of this Agreement is invalid or unenforceable under + applicable law, it shall not affect the validity or enforceability of + the remainder of the terms of this Agreement, and without further + action by the parties hereto, such provision shall be reformed to the + minimum extent necessary to make such provision valid and enforceable. + + If Recipient institutes patent litigation against any entity + (including a cross-claim or counterclaim in a lawsuit) alleging that the + Program itself (excluding combinations of the Program with other software + or hardware) infringes such Recipient's patent(s), then such Recipient's + rights granted under Section 2(b) shall terminate as of the date such + litigation is filed. + + All Recipient's rights under this Agreement shall terminate if it + fails to comply with any of the material terms or conditions of this + Agreement and does not cure such failure in a reasonable period of + time after becoming aware of such noncompliance. If all Recipient's + rights under this Agreement terminate, Recipient agrees to cease use + and distribution of the Program as soon as reasonably practicable. + However, Recipient's obligations under this Agreement and any licenses + granted by Recipient relating to the Program shall continue and survive. + + Everyone is permitted to copy and distribute copies of this Agreement, + but in order to avoid inconsistency the Agreement is copyrighted and + may only be modified in the following manner. The Agreement Steward + reserves the right to publish new versions (including revisions) of + this Agreement from time to time. No one other than the Agreement + Steward has the right to modify this Agreement. The Eclipse Foundation + is the initial Agreement Steward. The Eclipse Foundation may assign the + responsibility to serve as the Agreement Steward to a suitable separate + entity. Each new version of the Agreement will be given a distinguishing + version number. The Program (including Contributions) may always be + Distributed subject to the version of the Agreement under which it was + received. In addition, after a new version of the Agreement is published, + Contributor may elect to Distribute the Program (including its + Contributions) under the new version. + + Except as expressly stated in Sections 2(a) and 2(b) above, Recipient + receives no rights or licenses to the intellectual property of any + Contributor under this Agreement, whether expressly, by implication, + estoppel or otherwise. All rights in the Program not expressly granted + under this Agreement are reserved. Nothing in this Agreement is intended + to be enforceable by any entity that is not a Contributor or Recipient. + No third-party beneficiary rights are created under this Agreement. + + Exhibit A - Form of Secondary Licenses Notice + + "This Source Code may also be made available under the following + Secondary Licenses when the conditions for such availability set forth + in the Eclipse Public License, v. 2.0 are satisfied: {name license(s), + version(s), and exceptions or additional permissions here}." + + Simply including a copy of this Agreement, including this Exhibit A + is not sufficient to license the Source Code under Secondary Licenses. + + If it is not possible or desirable to put the notice in a particular + file, then You may include the notice in a location (such as a LICENSE + file in a relevant directory) where a recipient would be likely to + look for such a notice. + + You may add additional accurate notices of copyright ownership. + +--- + +## The GNU General Public License (GPL) Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor + Boston, MA 02110-1335 + USA + + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your freedom to + share and change it. By contrast, the GNU General Public License is + intended to guarantee your freedom to share and change free software--to + make sure the software is free for all its users. This General Public + License applies to most of the Free Software Foundation's software and + to any other program whose authors commit to using it. (Some other Free + Software Foundation software is covered by the GNU Library General + Public License instead.) You can apply it to your programs, too. + + When we speak of free software, we are referring to freedom, not price. + Our General Public Licenses are designed to make sure that you have the + freedom to distribute copies of free software (and charge for this + service if you wish), that you receive source code or can get it if you + want it, that you can change the software or use pieces of it in new + free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid anyone + to deny you these rights or to ask you to surrender the rights. These + restrictions translate to certain responsibilities for you if you + distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether gratis + or for a fee, you must give the recipients all the rights that you have. + You must make sure that they, too, receive or can get the source code. + And you must show them these terms so they know their rights. + + We protect your rights with two steps: (1) copyright the software, and + (2) offer you this license which gives you legal permission to copy, + distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain + that everyone understands that there is no warranty for this free + software. If the software is modified by someone else and passed on, we + want its recipients to know that what they have is not the original, so + that any problems introduced by others will not reflect on the original + authors' reputations. + + Finally, any free program is threatened constantly by software patents. + We wish to avoid the danger that redistributors of a free program will + individually obtain patent licenses, in effect making the program + proprietary. To prevent this, we have made it clear that any patent must + be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and + modification follow. + + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains a + notice placed by the copyright holder saying it may be distributed under + the terms of this General Public License. The "Program", below, refers + to any such program or work, and a "work based on the Program" means + either the Program or any derivative work under copyright law: that is + to say, a work containing the Program or a portion of it, either + verbatim or with modifications and/or translated into another language. + (Hereinafter, translation is included without limitation in the term + "modification".) Each licensee is addressed as "you". + + Activities other than copying, distribution and modification are not + covered by this License; they are outside its scope. The act of running + the Program is not restricted, and the output from the Program is + covered only if its contents constitute a work based on the Program + (independent of having been made by running the Program). Whether that + is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's source + code as you receive it, in any medium, provided that you conspicuously + and appropriately publish on each copy an appropriate copyright notice + and disclaimer of warranty; keep intact all the notices that refer to + this License and to the absence of any warranty; and give any other + recipients of the Program a copy of this License along with the Program. + + You may charge a fee for the physical act of transferring a copy, and + you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion of + it, thus forming a work based on the Program, and copy and distribute + such modifications or work under the terms of Section 1 above, provided + that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any part + thereof, to be licensed as a whole at no charge to all third parties + under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a notice + that there is no warranty (or else, saying that you provide a + warranty) and that users may redistribute the program under these + conditions, and telling the user how to view a copy of this License. + (Exception: if the Program itself is interactive but does not + normally print such an announcement, your work based on the Program + is not required to print an announcement.) + + These requirements apply to the modified work as a whole. If + identifiable sections of that work are not derived from the Program, and + can be reasonably considered independent and separate works in + themselves, then this License, and its terms, do not apply to those + sections when you distribute them as separate works. But when you + distribute the same sections as part of a whole which is a work based on + the Program, the distribution of the whole must be on the terms of this + License, whose permissions for other licensees extend to the entire + whole, and thus to each and every part regardless of who wrote it. + + Thus, it is not the intent of this section to claim rights or contest + your rights to work written entirely by you; rather, the intent is to + exercise the right to control the distribution of derivative or + collective works based on the Program. + + In addition, mere aggregation of another work not based on the Program + with the Program (or with a work based on the Program) on a volume of a + storage or distribution medium does not bring the other work under the + scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, + under Section 2) in object code or executable form under the terms of + Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections 1 + and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your cost + of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer to + distribute corresponding source code. (This alternative is allowed + only for noncommercial distribution and only if you received the + program in object code or executable form with such an offer, in + accord with Subsection b above.) + + The source code for a work means the preferred form of the work for + making modifications to it. For an executable work, complete source code + means all the source code for all modules it contains, plus any + associated interface definition files, plus the scripts used to control + compilation and installation of the executable. However, as a special + exception, the source code distributed need not include anything that is + normally distributed (in either source or binary form) with the major + components (compiler, kernel, and so on) of the operating system on + which the executable runs, unless that component itself accompanies the + executable. + + If distribution of executable or object code is made by offering access + to copy from a designated place, then offering equivalent access to copy + the source code from the same place counts as distribution of the source + code, even though third parties are not compelled to copy the source + along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program + except as expressly provided under this License. Any attempt otherwise + to copy, modify, sublicense or distribute the Program is void, and will + automatically terminate your rights under this License. However, parties + who have received copies, or rights, from you under this License will + not have their licenses terminated so long as such parties remain in + full compliance. + + 5. You are not required to accept this License, since you have not + signed it. However, nothing else grants you permission to modify or + distribute the Program or its derivative works. These actions are + prohibited by law if you do not accept this License. Therefore, by + modifying or distributing the Program (or any work based on the + Program), you indicate your acceptance of this License to do so, and all + its terms and conditions for copying, distributing or modifying the + Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the + Program), the recipient automatically receives a license from the + original licensor to copy, distribute or modify the Program subject to + these terms and conditions. You may not impose any further restrictions + on the recipients' exercise of the rights granted herein. You are not + responsible for enforcing compliance by third parties to this License. + + 7. If, as a consequence of a court judgment or allegation of patent + infringement or for any other reason (not limited to patent issues), + conditions are imposed on you (whether by court order, agreement or + otherwise) that contradict the conditions of this License, they do not + excuse you from the conditions of this License. If you cannot distribute + so as to satisfy simultaneously your obligations under this License and + any other pertinent obligations, then as a consequence you may not + distribute the Program at all. For example, if a patent license would + not permit royalty-free redistribution of the Program by all those who + receive copies directly or indirectly through you, then the only way you + could satisfy both it and this License would be to refrain entirely from + distribution of the Program. + + If any portion of this section is held invalid or unenforceable under + any particular circumstance, the balance of the section is intended to + apply and the section as a whole is intended to apply in other + circumstances. + + It is not the purpose of this section to induce you to infringe any + patents or other property right claims or to contest validity of any + such claims; this section has the sole purpose of protecting the + integrity of the free software distribution system, which is implemented + by public license practices. Many people have made generous + contributions to the wide range of software distributed through that + system in reliance on consistent application of that system; it is up to + the author/donor to decide if he or she is willing to distribute + software through any other system and a licensee cannot impose that choice. + + This section is intended to make thoroughly clear what is believed to be + a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in + certain countries either by patents or by copyrighted interfaces, the + original copyright holder who places the Program under this License may + add an explicit geographical distribution limitation excluding those + countries, so that distribution is permitted only in or among countries + not thus excluded. In such case, this License incorporates the + limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new + versions of the General Public License from time to time. Such new + versions will be similar in spirit to the present version, but may + differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the Program + specifies a version number of this License which applies to it and "any + later version", you have the option of following the terms and + conditions either of that version or of any later version published by + the Free Software Foundation. If the Program does not specify a version + number of this License, you may choose any version ever published by the + Free Software Foundation. + + 10. If you wish to incorporate parts of the Program into other free + programs whose distribution conditions are different, write to the + author to ask for permission. For software which is copyrighted by the + Free Software Foundation, write to the Free Software Foundation; we + sometimes make exceptions for this. Our decision will be guided by the + two goals of preserving the free status of all derivatives of our free + software and of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO + WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. + EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR + OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, + EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE + ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH + YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL + NECESSARY SERVICING, REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN + WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY + AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR + DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL + DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM + (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED + INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF + THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR + OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest + possible use to the public, the best way to achieve this is to make it + free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest to + attach them to the start of each source file to most effectively convey + the exclusion of warranty; and each file should have at least the + "copyright" line and a pointer to where the full notice is found. + + One line to give the program's name and a brief idea of what it does. + Copyright (C) <year> <name of author> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335 USA + + Also add information on how to contact you by electronic and paper mail. + + If the program is interactive, make it output a short notice like this + when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type + `show w'. This is free software, and you are welcome to redistribute + it under certain conditions; type `show c' for details. + + The hypothetical commands `show w' and `show c' should show the + appropriate parts of the General Public License. Of course, the commands + you use may be called something other than `show w' and `show c'; they + could even be mouse-clicks or menu items--whatever suits your program. + + You should also get your employer (if you work as a programmer) or your + school, if any, to sign a "copyright disclaimer" for the program, if + necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + program `Gnomovision' (which makes passes at compilers) written by + James Hacker. + + signature of Ty Coon, 1 April 1989 + Ty Coon, President of Vice + + This General Public License does not permit incorporating your program + into proprietary programs. If your program is a subroutine library, you + may consider it more useful to permit linking proprietary applications + with the library. If this is what you want to do, use the GNU Library + General Public License instead of this License. + +--- + +## CLASSPATH EXCEPTION + + Linking this library statically or dynamically with other modules is + making a combined work based on this library. Thus, the terms and + conditions of the GNU General Public License version 2 cover the whole + combination. + + As a special exception, the copyright holders of this library give you + permission to link this library with independent modules to produce an + executable, regardless of the license terms of these independent + modules, and to copy and distribute the resulting executable under + terms of your choice, provided that you also meet, for each linked + independent module, the terms and conditions of the license of that + module. An independent module is a module which is not derived from or + based on this library. If you modify this library, you may extend this + exception to your version of the library, but you are not obligated to + do so. If you do not wish to do so, delete this exception statement + from your version.
diff --git a/NOTICE.md b/NOTICE.md new file mode 100644 index 0000000..9a5159e --- /dev/null +++ b/NOTICE.md
@@ -0,0 +1,50 @@ +# Notices for Eclipse Project for JavaMail + +This content is produced and maintained by the Eclipse Project for JavaMail +project. + +* Project home: https://projects.eclipse.org/projects/ee4j.javamail + +## Trademarks + +Eclipse Project for JavaMail is a trademark of the Eclipse Foundation. + +## Copyright + +All content is the property of the respective authors or their employers. For +more information regarding authorship of content, please consult the listed +source code repository logs. + +## Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License v. 2.0 which is available at +http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made +available under the following Secondary Licenses when the conditions for such +availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU +General Public License, version 2 with the GNU Classpath Exception which is +available at https://www.gnu.org/software/classpath/license.html. + +SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +## Source Code + +The project maintains the following source code repositories: + +* https://github.com/eclipse-ee4j/javamail + +## Third-party Content + +This project leverages the following third party content. + +None + +## Cryptography + +Content may contain encryption software. The country in which you are currently +may have restrictions on the import, possession, and use, and/or re-export to +another country, of encryption software. BEFORE using any encryption software, +please check the country's laws, regulations and policies concerning the import, +possession, or use, and re-export of encryption software, to see if this is +permitted. +
diff --git a/README.md b/README.md new file mode 100644 index 0000000..01c8392 --- /dev/null +++ b/README.md
@@ -0,0 +1 @@ +See the [JavaMail web site](https://javaee.github.io/javamail).
diff --git a/android/activation/exclude.xml b/android/activation/exclude.xml new file mode 100644 index 0000000..3ca7394 --- /dev/null +++ b/android/activation/exclude.xml
@@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Distribution License v. 1.0, which is available at + http://www.eclipse.org/org/documents/edl-v10.php. + + SPDX-License-Identifier: BSD-3-Clause + +--> + +<!-- FindBugs exclude list for JAF --> + +<FindBugsFilter> + <!-- + We're purposely using the platform default encoding when reading + these files. + --> + <Match> + <Or> + <Class name="com.sun.activation.registries.MailcapFile"/> + <Class name="com.sun.activation.registries.MimeTypeFile"/> + </Or> + <Bug pattern="DM_DEFAULT_ENCODING"/> + </Match> + + <!-- + We're purposely using the platform default encoding when writing + a String to an OutputStream. + --> + <Match> + <Class name="javax.activation.ObjectDataContentHandler"/> + <Method name="writeTo"/> + <Bug pattern="DM_DEFAULT_ENCODING"/> + </Match> +</FindBugsFilter>
diff --git a/android/activation/pom.xml b/android/activation/pom.xml new file mode 100644 index 0000000..492e698 --- /dev/null +++ b/android/activation/pom.xml
@@ -0,0 +1,203 @@ +<?xml version="1.0" encoding="iso-8859-1"?> +<!-- + + Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Distribution License v. 1.0, which is available at + http://www.eclipse.org/org/documents/edl-v10.php. + + SPDX-License-Identifier: BSD-3-Clause + +--> + +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 + http://maven.apache.org/maven-v4_0_0.xsd"> + <parent> + <groupId>com.sun.mail</groupId> + <artifactId>android</artifactId> + <version>1.6.3</version> + </parent> + <modelVersion>4.0.0</modelVersion> + <groupId>com.sun.mail</groupId> + <artifactId>android-activation</artifactId> + <packaging>jar</packaging> + <name>JavaBeans Activation Framework fork for Android</name> + + <!-- + This is a fork of the JavaBeans Activation Framework (JAF) that + is sufficient for use by JavaMail when running on Android. + Android is not Java Compatible, and is missing the java.awt.datatransfer + classes. This fork of JAF breaks the dependency on the + java.awt.datatransfer classes and thus isn't compatible with + the JAF specification. It should not be used by anything other + than this Android-specific version of JavaMail. + --> + + <properties> + <activation.spec.version>1.2</activation.spec.version> + <activation.extensionName> + jakarta.activation.android + </activation.extensionName> + <activation.specificationTitle> + JavaBeans(TM) Activation Framework Specification + </activation.specificationTitle> + <activation.implementationTitle> + javax.activation + </activation.implementationTitle> + <activation.bundle.symbolicName> + ${project.groupId}.${project.artifactId} + </activation.bundle.symbolicName> + <activation.packages.export> + javax.activation.*; version=${activation.spec.version}, + com.sun.activation.*; version=${activation.osgiversion} + </activation.packages.export> + <findbugs.skip> + false + </findbugs.skip> + <findbugs.exclude> + ${project.basedir}/exclude.xml + </findbugs.exclude> + </properties> + + <build> + <resources> + <resource> + <directory>src/main/resources</directory> + <filtering>true</filtering> + </resource> + </resources> + <plugins> + <!-- + Configure compiler plugin to print lint warnings. + --> + <plugin> + <artifactId>maven-compiler-plugin</artifactId> + <executions> + <execution> + <id>default-compile</id> + <configuration> + <source>1.7</source> + <target>1.7</target> + <fork>true</fork> + <!-- + ignore the errors that I don't + want to fix now + --> + <compilerArgs> + <arg>-Xlint</arg> + <arg>-Xlint:-options</arg> + <arg>-Xlint:-path</arg> + <arg>-Xlint:-rawtypes</arg> + <arg>-Xlint:-unchecked</arg> + <arg>-Xlint:-finally</arg> + <arg>-Xlint:-serial</arg> + <arg>-Xlint:-cast</arg> + <arg>-Xlint:-deprecation</arg> + <arg>-Xlint:-dep-ann</arg> + <arg>-Werror</arg> + </compilerArgs> + <showWarnings>true</showWarnings> + </configuration> + </execution> + <execution> + <id>default-testCompile</id> + <configuration> + <source>1.7</source> + <target>1.7</target> + </configuration> + </execution> + </executions> + </plugin> + + <!-- + Configure test plugin to find *TestSuite classes. + --> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-surefire-plugin</artifactId> + <configuration> + <includes> + <include>**/*Test.java</include> + <include>**/*TestSuite.java</include> + </includes> + </configuration> + </plugin> + + <plugin> + <groupId>org.apache.felix</groupId> + <artifactId>maven-bundle-plugin</artifactId> + <configuration> + <instructions> + <Bundle-SymbolicName> + ${activation.bundle.symbolicName} + </Bundle-SymbolicName> + <Export-Package> + ${activation.packages.export} + </Export-Package> + <Import-Package> + ${activation.packages.import} + </Import-Package> + <Private-Package> + ${activation.packages.private} + </Private-Package> + <DynamicImport-Package> + * + </DynamicImport-Package> + </instructions> + </configuration> + <!-- + Since we don't change the packaging type to bundle, we + need to configure the plugin to execute the manifest goal + during the process-classes phase of the build life cycle. + --> + <executions> + <execution> + <id>osgi-manifest</id> + <phase>process-classes</phase> + <goals> + <goal>manifest</goal> + </goals> + </execution> + </executions> + </plugin> + + <!-- + Since we don't want a qualifier like b05 or SNAPSHOT to + appear in the OSGi package version attribute, we use + the following plugin to populate a project property + with an OSGi version that is equivalent to the maven + version without the qualifier. + --> + <plugin> + <groupId>org.glassfish.hk2</groupId> + <artifactId>osgiversion-maven-plugin</artifactId> + <version>${hk2.plugin.version}</version> + <configuration> + <dropVersionComponent>qualifier</dropVersionComponent> + <versionPropertyName>activation.osgiversion</versionPropertyName> + </configuration> + <executions> + <execution> + <id>compute-osgi-version</id> + <goals> + <goal>compute-osgi-version</goal> + </goals> + </execution> + </executions> + </plugin> + </plugins> + </build> + + <dependencies> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <version>4.12</version> + <scope>test</scope> + <optional>true</optional> + </dependency> + </dependencies> +</project>
diff --git a/android/activation/src/main/java/com/sun/activation/registries/LogSupport.java b/android/activation/src/main/java/com/sun/activation/registries/LogSupport.java new file mode 100644 index 0000000..3228f3c --- /dev/null +++ b/android/activation/src/main/java/com/sun/activation/registries/LogSupport.java
@@ -0,0 +1,55 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package com.sun.activation.registries; + +import java.io.*; +import java.util.logging.*; + +/** + * Logging related methods. + */ +public class LogSupport { + private static boolean debug = false; + private static Logger logger; + private static final Level level = Level.FINE; + + static { + try { + debug = Boolean.getBoolean("javax.activation.debug"); + } catch (Throwable t) { + // ignore any errors + } + logger = Logger.getLogger("javax.activation"); + } + + /** + * Constructor. + */ + private LogSupport() { + // private constructor, can't create instances + } + + public static void log(String msg) { + if (debug) + System.out.println(msg); + logger.log(level, msg); + } + + public static void log(String msg, Throwable t) { + if (debug) + System.out.println(msg + "; Exception: " + t); + logger.log(level, msg, t); + } + + public static boolean isLoggable() { + return debug || logger.isLoggable(level); + } +}
diff --git a/android/activation/src/main/java/com/sun/activation/registries/MailcapFile.java b/android/activation/src/main/java/com/sun/activation/registries/MailcapFile.java new file mode 100644 index 0000000..afcb5d6 --- /dev/null +++ b/android/activation/src/main/java/com/sun/activation/registries/MailcapFile.java
@@ -0,0 +1,548 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package com.sun.activation.registries; + +import java.io.*; +import java.util.*; + +public class MailcapFile { + + /** + * A Map indexed by MIME type (string) that references + * a Map of commands for each type. The comand Map + * is indexed by the command name and references a List of + * class names (strings) for each command. + */ + private Map type_hash = new HashMap(); + + /** + * Another Map like above, but for fallback entries. + */ + private Map fallback_hash = new HashMap(); + + /** + * A Map indexed by MIME type (string) that references + * a List of native commands (string) corresponding to the type. + */ + private Map native_commands = new HashMap(); + + private static boolean addReverse = false; + + static { + try { + addReverse = Boolean.getBoolean("javax.activation.addreverse"); + } catch (Throwable t) { + // ignore any errors + } + } + + /** + * The constructor that takes a filename as an argument. + * + * @param new_fname The file name of the mailcap file. + */ + public MailcapFile(String new_fname) throws IOException { + if (LogSupport.isLoggable()) + LogSupport.log("new MailcapFile: file " + new_fname); + FileReader reader = null; + try { + reader = new FileReader(new_fname); + parse(new BufferedReader(reader)); + } finally { + if (reader != null) { + try { + reader.close(); + } catch (IOException ex) { } + } + } + } + + /** + * The constructor that takes an input stream as an argument. + * + * @param is the input stream + */ + public MailcapFile(InputStream is) throws IOException { + if (LogSupport.isLoggable()) + LogSupport.log("new MailcapFile: InputStream"); + parse(new BufferedReader(new InputStreamReader(is, "iso-8859-1"))); + } + + /** + * Mailcap file default constructor. + */ + public MailcapFile() { + if (LogSupport.isLoggable()) + LogSupport.log("new MailcapFile: default"); + } + + /** + * Get the Map of MailcapEntries based on the MIME type. + * + * <p> + * <strong>Semantics:</strong> First check for the literal mime type, + * if that fails looks for wildcard <i>type</i>/* and return that. Return the + * list of all that hit. + */ + public Map getMailcapList(String mime_type) { + Map search_result = null; + Map wildcard_result = null; + + // first try the literal + search_result = (Map)type_hash.get(mime_type); + + // ok, now try the wildcard + int separator = mime_type.indexOf('/'); + String subtype = mime_type.substring(separator + 1); + if (!subtype.equals("*")) { + String type = mime_type.substring(0, separator + 1) + "*"; + wildcard_result = (Map)type_hash.get(type); + + if (wildcard_result != null) { // damn, we have to merge!!! + if (search_result != null) + search_result = + mergeResults(search_result, wildcard_result); + else + search_result = wildcard_result; + } + } + return search_result; + } + + /** + * Get the Map of fallback MailcapEntries based on the MIME type. + * + * <p> + * <strong>Semantics:</strong> First check for the literal mime type, + * if that fails looks for wildcard <i>type</i>/* and return that. Return the + * list of all that hit. + */ + public Map getMailcapFallbackList(String mime_type) { + Map search_result = null; + Map wildcard_result = null; + + // first try the literal + search_result = (Map)fallback_hash.get(mime_type); + + // ok, now try the wildcard + int separator = mime_type.indexOf('/'); + String subtype = mime_type.substring(separator + 1); + if (!subtype.equals("*")) { + String type = mime_type.substring(0, separator + 1) + "*"; + wildcard_result = (Map)fallback_hash.get(type); + + if (wildcard_result != null) { // damn, we have to merge!!! + if (search_result != null) + search_result = + mergeResults(search_result, wildcard_result); + else + search_result = wildcard_result; + } + } + return search_result; + } + + /** + * Return all the MIME types known to this mailcap file. + */ + public String[] getMimeTypes() { + Set types = new HashSet(type_hash.keySet()); + types.addAll(fallback_hash.keySet()); + types.addAll(native_commands.keySet()); + String[] mts = new String[types.size()]; + mts = (String[])types.toArray(mts); + return mts; + } + + /** + * Return all the native comands for the given MIME type. + */ + public String[] getNativeCommands(String mime_type) { + String[] cmds = null; + List v = + (List)native_commands.get(mime_type.toLowerCase(Locale.ENGLISH)); + if (v != null) { + cmds = new String[v.size()]; + cmds = (String[])v.toArray(cmds); + } + return cmds; + } + + /** + * Merge the first hash into the second. + * This merge will only effect the hashtable that is + * returned, we don't want to touch the one passed in since + * its integrity must be maintained. + */ + private Map mergeResults(Map first, Map second) { + Iterator verb_enum = second.keySet().iterator(); + Map clonedHash = new HashMap(first); + + // iterate through the verbs in the second map + while (verb_enum.hasNext()) { + String verb = (String)verb_enum.next(); + List cmdVector = (List)clonedHash.get(verb); + if (cmdVector == null) { + clonedHash.put(verb, second.get(verb)); + } else { + // merge the two + List oldV = (List)second.get(verb); + cmdVector = new ArrayList(cmdVector); + cmdVector.addAll(oldV); + clonedHash.put(verb, cmdVector); + } + } + return clonedHash; + } + + /** + * appendToMailcap: Append to this Mailcap DB, use the mailcap + * format: + * Comment == "# <i>comment string</i> + * Entry == "mimetype; javabeanclass<br> + * + * Example: + * # this is a comment + * image/gif jaf.viewers.ImageViewer + */ + public void appendToMailcap(String mail_cap) { + if (LogSupport.isLoggable()) + LogSupport.log("appendToMailcap: " + mail_cap); + try { + parse(new StringReader(mail_cap)); + } catch (IOException ex) { + // can't happen + } + } + + /** + * parse file into a hash table of MC Type Entry Obj + */ + private void parse(Reader reader) throws IOException { + BufferedReader buf_reader = new BufferedReader(reader); + String line = null; + String continued = null; + + while ((line = buf_reader.readLine()) != null) { + // LogSupport.log("parsing line: " + line); + + line = line.trim(); + + try { + if (line.charAt(0) == '#') + continue; + if (line.charAt(line.length() - 1) == '\\') { + if (continued != null) + continued += line.substring(0, line.length() - 1); + else + continued = line.substring(0, line.length() - 1); + } else if (continued != null) { + // handle the two strings + continued = continued + line; + // LogSupport.log("parse: " + continued); + try { + parseLine(continued); + } catch (MailcapParseException e) { + //e.printStackTrace(); + } + continued = null; + } + else { + // LogSupport.log("parse: " + line); + try { + parseLine(line); + // LogSupport.log("hash.size = " + type_hash.size()); + } catch (MailcapParseException e) { + //e.printStackTrace(); + } + } + } catch (StringIndexOutOfBoundsException e) {} + } + } + + /** + * A routine to parse individual entries in a Mailcap file. + * + * Note that this routine does not handle line continuations. + * They should have been handled prior to calling this routine. + */ + protected void parseLine(String mailcapEntry) + throws MailcapParseException, IOException { + MailcapTokenizer tokenizer = new MailcapTokenizer(mailcapEntry); + tokenizer.setIsAutoquoting(false); + + if (LogSupport.isLoggable()) + LogSupport.log("parse: " + mailcapEntry); + // parse the primary type + int currentToken = tokenizer.nextToken(); + if (currentToken != MailcapTokenizer.STRING_TOKEN) { + reportParseError(MailcapTokenizer.STRING_TOKEN, currentToken, + tokenizer.getCurrentTokenValue()); + } + String primaryType = + tokenizer.getCurrentTokenValue().toLowerCase(Locale.ENGLISH); + String subType = "*"; + + // parse the '/' between primary and sub + // if it's not present that's ok, we just don't have a subtype + currentToken = tokenizer.nextToken(); + if ((currentToken != MailcapTokenizer.SLASH_TOKEN) && + (currentToken != MailcapTokenizer.SEMICOLON_TOKEN)) { + reportParseError(MailcapTokenizer.SLASH_TOKEN, + MailcapTokenizer.SEMICOLON_TOKEN, currentToken, + tokenizer.getCurrentTokenValue()); + } + + // only need to look for a sub type if we got a '/' + if (currentToken == MailcapTokenizer.SLASH_TOKEN) { + // parse the sub type + currentToken = tokenizer.nextToken(); + if (currentToken != MailcapTokenizer.STRING_TOKEN) { + reportParseError(MailcapTokenizer.STRING_TOKEN, + currentToken, tokenizer.getCurrentTokenValue()); + } + subType = + tokenizer.getCurrentTokenValue().toLowerCase(Locale.ENGLISH); + + // get the next token to simplify the next step + currentToken = tokenizer.nextToken(); + } + + String mimeType = primaryType + "/" + subType; + + if (LogSupport.isLoggable()) + LogSupport.log(" Type: " + mimeType); + + // now setup the commands hashtable + Map commands = new LinkedHashMap(); // keep commands in order found + + // parse the ';' that separates the type from the parameters + if (currentToken != MailcapTokenizer.SEMICOLON_TOKEN) { + reportParseError(MailcapTokenizer.SEMICOLON_TOKEN, + currentToken, tokenizer.getCurrentTokenValue()); + } + // eat it + + // parse the required view command + tokenizer.setIsAutoquoting(true); + currentToken = tokenizer.nextToken(); + tokenizer.setIsAutoquoting(false); + if ((currentToken != MailcapTokenizer.STRING_TOKEN) && + (currentToken != MailcapTokenizer.SEMICOLON_TOKEN)) { + reportParseError(MailcapTokenizer.STRING_TOKEN, + MailcapTokenizer.SEMICOLON_TOKEN, currentToken, + tokenizer.getCurrentTokenValue()); + } + + if (currentToken == MailcapTokenizer.STRING_TOKEN) { + // have a native comand, save the entire mailcap entry + //String nativeCommand = tokenizer.getCurrentTokenValue(); + List v = (List)native_commands.get(mimeType); + if (v == null) { + v = new ArrayList(); + v.add(mailcapEntry); + native_commands.put(mimeType, v); + } else { + // XXX - check for duplicates? + v.add(mailcapEntry); + } + } + + // only have to get the next token if the current one isn't a ';' + if (currentToken != MailcapTokenizer.SEMICOLON_TOKEN) { + currentToken = tokenizer.nextToken(); + } + + // look for a ';' which will indicate whether + // a parameter list is present or not + if (currentToken == MailcapTokenizer.SEMICOLON_TOKEN) { + boolean isFallback = false; + do { + // eat the ';' + + // parse the parameter name + currentToken = tokenizer.nextToken(); + if (currentToken != MailcapTokenizer.STRING_TOKEN) { + reportParseError(MailcapTokenizer.STRING_TOKEN, + currentToken, tokenizer.getCurrentTokenValue()); + } + String paramName = tokenizer.getCurrentTokenValue(). + toLowerCase(Locale.ENGLISH); + + // parse the '=' which separates the name from the value + currentToken = tokenizer.nextToken(); + if ((currentToken != MailcapTokenizer.EQUALS_TOKEN) && + (currentToken != MailcapTokenizer.SEMICOLON_TOKEN) && + (currentToken != MailcapTokenizer.EOI_TOKEN)) { + reportParseError(MailcapTokenizer.EQUALS_TOKEN, + MailcapTokenizer.SEMICOLON_TOKEN, + MailcapTokenizer.EOI_TOKEN, + currentToken, tokenizer.getCurrentTokenValue()); + } + + // we only have a useful command if it is named + if (currentToken == MailcapTokenizer.EQUALS_TOKEN) { + // eat it + + // parse the parameter value (which is autoquoted) + tokenizer.setIsAutoquoting(true); + currentToken = tokenizer.nextToken(); + tokenizer.setIsAutoquoting(false); + if (currentToken != MailcapTokenizer.STRING_TOKEN) { + reportParseError(MailcapTokenizer.STRING_TOKEN, + currentToken, tokenizer.getCurrentTokenValue()); + } + String paramValue = + tokenizer.getCurrentTokenValue(); + + // add the class to the list iff it is one we care about + if (paramName.startsWith("x-java-")) { + String commandName = paramName.substring(7); + // 7 == "x-java-".length + + if (commandName.equals("fallback-entry") && + paramValue.equalsIgnoreCase("true")) { + isFallback = true; + } else { + + // setup the class entry list + if (LogSupport.isLoggable()) + LogSupport.log(" Command: " + commandName + + ", Class: " + paramValue); + List classes = (List)commands.get(commandName); + if (classes == null) { + classes = new ArrayList(); + commands.put(commandName, classes); + } + if (addReverse) + classes.add(0, paramValue); + else + classes.add(paramValue); + } + } + + // set up the next iteration + currentToken = tokenizer.nextToken(); + } + } while (currentToken == MailcapTokenizer.SEMICOLON_TOKEN); + + Map masterHash = isFallback ? fallback_hash : type_hash; + Map curcommands = + (Map)masterHash.get(mimeType); + if (curcommands == null) { + masterHash.put(mimeType, commands); + } else { + if (LogSupport.isLoggable()) + LogSupport.log("Merging commands for type " + mimeType); + // have to merge current and new commands + // first, merge list of classes for commands already known + Iterator cn = curcommands.keySet().iterator(); + while (cn.hasNext()) { + String cmdName = (String)cn.next(); + List ccv = (List)curcommands.get(cmdName); + List cv = (List)commands.get(cmdName); + if (cv == null) + continue; + // add everything in cv to ccv, if it's not already there + Iterator cvn = cv.iterator(); + while (cvn.hasNext()) { + String clazz = (String)cvn.next(); + if (!ccv.contains(clazz)) + if (addReverse) + ccv.add(0, clazz); + else + ccv.add(clazz); + } + } + // now, add commands not previously known + cn = commands.keySet().iterator(); + while (cn.hasNext()) { + String cmdName = (String)cn.next(); + if (curcommands.containsKey(cmdName)) + continue; + List cv = (List)commands.get(cmdName); + curcommands.put(cmdName, cv); + } + } + } else if (currentToken != MailcapTokenizer.EOI_TOKEN) { + reportParseError(MailcapTokenizer.EOI_TOKEN, + MailcapTokenizer.SEMICOLON_TOKEN, + currentToken, tokenizer.getCurrentTokenValue()); + } + } + + protected static void reportParseError(int expectedToken, int actualToken, + String actualTokenValue) throws MailcapParseException { + throw new MailcapParseException("Encountered a " + + MailcapTokenizer.nameForToken(actualToken) + " token (" + + actualTokenValue + ") while expecting a " + + MailcapTokenizer.nameForToken(expectedToken) + " token."); + } + + protected static void reportParseError(int expectedToken, + int otherExpectedToken, int actualToken, String actualTokenValue) + throws MailcapParseException { + throw new MailcapParseException("Encountered a " + + MailcapTokenizer.nameForToken(actualToken) + " token (" + + actualTokenValue + ") while expecting a " + + MailcapTokenizer.nameForToken(expectedToken) + " or a " + + MailcapTokenizer.nameForToken(otherExpectedToken) + " token."); + } + + protected static void reportParseError(int expectedToken, + int otherExpectedToken, int anotherExpectedToken, int actualToken, + String actualTokenValue) throws MailcapParseException { + if (LogSupport.isLoggable()) + LogSupport.log("PARSE ERROR: " + "Encountered a " + + MailcapTokenizer.nameForToken(actualToken) + " token (" + + actualTokenValue + ") while expecting a " + + MailcapTokenizer.nameForToken(expectedToken) + ", a " + + MailcapTokenizer.nameForToken(otherExpectedToken) + ", or a " + + MailcapTokenizer.nameForToken(anotherExpectedToken) + " token."); + throw new MailcapParseException("Encountered a " + + MailcapTokenizer.nameForToken(actualToken) + " token (" + + actualTokenValue + ") while expecting a " + + MailcapTokenizer.nameForToken(expectedToken) + ", a " + + MailcapTokenizer.nameForToken(otherExpectedToken) + ", or a " + + MailcapTokenizer.nameForToken(anotherExpectedToken) + " token."); + } + + /** for debugging + public static void main(String[] args) throws Exception { + Map masterHash = new HashMap(); + for (int i = 0; i < args.length; ++i) { + System.out.println("Entry " + i + ": " + args[i]); + parseLine(args[i], masterHash); + } + + Enumeration types = masterHash.keys(); + while (types.hasMoreElements()) { + String key = (String)types.nextElement(); + System.out.println("MIME Type: " + key); + + Map commandHash = (Map)masterHash.get(key); + Enumeration commands = commandHash.keys(); + while (commands.hasMoreElements()) { + String command = (String)commands.nextElement(); + System.out.println(" Command: " + command); + + Vector classes = (Vector)commandHash.get(command); + for (int i = 0; i < classes.size(); ++i) { + System.out.println(" Class: " + + (String)classes.elementAt(i)); + } + } + + System.out.println(""); + } + } + */ +}
diff --git a/android/activation/src/main/java/com/sun/activation/registries/MailcapParseException.java b/android/activation/src/main/java/com/sun/activation/registries/MailcapParseException.java new file mode 100644 index 0000000..754c405 --- /dev/null +++ b/android/activation/src/main/java/com/sun/activation/registries/MailcapParseException.java
@@ -0,0 +1,25 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package com.sun.activation.registries; + +/** + * A class to encapsulate Mailcap parsing related exceptions + */ +public class MailcapParseException extends Exception { + + public MailcapParseException() { + super(); + } + + public MailcapParseException(String inInfo) { + super(inInfo); + } +}
diff --git a/android/activation/src/main/java/com/sun/activation/registries/MailcapTokenizer.java b/android/activation/src/main/java/com/sun/activation/registries/MailcapTokenizer.java new file mode 100644 index 0000000..6437930 --- /dev/null +++ b/android/activation/src/main/java/com/sun/activation/registries/MailcapTokenizer.java
@@ -0,0 +1,307 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package com.sun.activation.registries; + +/** + * A tokenizer for strings in the form of "foo/bar; prop1=val1; ... ". + * Useful for parsing MIME content types. + */ +public class MailcapTokenizer { + + public static final int UNKNOWN_TOKEN = 0; + public static final int START_TOKEN = 1; + public static final int STRING_TOKEN = 2; + public static final int EOI_TOKEN = 5; + public static final int SLASH_TOKEN = '/'; + public static final int SEMICOLON_TOKEN = ';'; + public static final int EQUALS_TOKEN = '='; + + /** + * Constructor + * + * @param inputString the string to tokenize + */ + public MailcapTokenizer(String inputString) { + data = inputString; + dataIndex = 0; + dataLength = inputString.length(); + + currentToken = START_TOKEN; + currentTokenValue = ""; + + isAutoquoting = false; + autoquoteChar = ';'; + } + + /** + * Set whether auto-quoting is on or off. + * + * Auto-quoting means that all characters after the first + * non-whitespace, non-control character up to the auto-quote + * terminator character or EOI (minus any whitespace immediatley + * preceeding it) is considered a token. + * + * This is required for handling command strings in a mailcap entry. + */ + public void setIsAutoquoting(boolean value) { + isAutoquoting = value; + } + + /** + * Retrieve current token. + * + * @return The current token value + */ + public int getCurrentToken() { + return currentToken; + } + + /* + * Get a String that describes the given token. + */ + public static String nameForToken(int token) { + String name = "really unknown"; + + switch(token) { + case UNKNOWN_TOKEN: + name = "unknown"; + break; + case START_TOKEN: + name = "start"; + break; + case STRING_TOKEN: + name = "string"; + break; + case EOI_TOKEN: + name = "EOI"; + break; + case SLASH_TOKEN: + name = "'/'"; + break; + case SEMICOLON_TOKEN: + name = "';'"; + break; + case EQUALS_TOKEN: + name = "'='"; + break; + } + + return name; + } + + /* + * Retrieve current token value. + * + * @return A String containing the current token value + */ + public String getCurrentTokenValue() { + return currentTokenValue; + } + + /* + * Process the next token. + * + * @return the next token + */ + public int nextToken() { + if (dataIndex < dataLength) { + // skip white space + while ((dataIndex < dataLength) && + (isWhiteSpaceChar(data.charAt(dataIndex)))) { + ++dataIndex; + } + + if (dataIndex < dataLength) { + // examine the current character and see what kind of token we have + char c = data.charAt(dataIndex); + if (isAutoquoting) { + if (c == ';' || c == '=') { + currentToken = c; + currentTokenValue = new Character(c).toString(); + ++dataIndex; + } else { + processAutoquoteToken(); + } + } else { + if (isStringTokenChar(c)) { + processStringToken(); + } else if ((c == '/') || (c == ';') || (c == '=')) { + currentToken = c; + currentTokenValue = new Character(c).toString(); + ++dataIndex; + } else { + currentToken = UNKNOWN_TOKEN; + currentTokenValue = new Character(c).toString(); + ++dataIndex; + } + } + } else { + currentToken = EOI_TOKEN; + currentTokenValue = null; + } + } else { + currentToken = EOI_TOKEN; + currentTokenValue = null; + } + + return currentToken; + } + + private void processStringToken() { + // capture the initial index + int initialIndex = dataIndex; + + // skip to 1st non string token character + while ((dataIndex < dataLength) && + isStringTokenChar(data.charAt(dataIndex))) { + ++dataIndex; + } + + currentToken = STRING_TOKEN; + currentTokenValue = data.substring(initialIndex, dataIndex); + } + + private void processAutoquoteToken() { + // capture the initial index + int initialIndex = dataIndex; + + // now skip to the 1st non-escaped autoquote termination character + // XXX - doesn't actually consider escaping + boolean foundTerminator = false; + while ((dataIndex < dataLength) && !foundTerminator) { + char c = data.charAt(dataIndex); + if (c != autoquoteChar) { + ++dataIndex; + } else { + foundTerminator = true; + } + } + + currentToken = STRING_TOKEN; + currentTokenValue = + fixEscapeSequences(data.substring(initialIndex, dataIndex)); + } + + private static boolean isSpecialChar(char c) { + boolean lAnswer = false; + + switch(c) { + case '(': + case ')': + case '<': + case '>': + case '@': + case ',': + case ';': + case ':': + case '\\': + case '"': + case '/': + case '[': + case ']': + case '?': + case '=': + lAnswer = true; + break; + } + + return lAnswer; + } + + private static boolean isControlChar(char c) { + return Character.isISOControl(c); + } + + private static boolean isWhiteSpaceChar(char c) { + return Character.isWhitespace(c); + } + + private static boolean isStringTokenChar(char c) { + return !isSpecialChar(c) && !isControlChar(c) && !isWhiteSpaceChar(c); + } + + private static String fixEscapeSequences(String inputString) { + int inputLength = inputString.length(); + StringBuffer buffer = new StringBuffer(); + buffer.ensureCapacity(inputLength); + + for (int i = 0; i < inputLength; ++i) { + char currentChar = inputString.charAt(i); + if (currentChar != '\\') { + buffer.append(currentChar); + } else { + if (i < inputLength - 1) { + char nextChar = inputString.charAt(i + 1); + buffer.append(nextChar); + + // force a skip over the next character too + ++i; + } else { + buffer.append(currentChar); + } + } + } + + return buffer.toString(); + } + + private String data; + private int dataIndex; + private int dataLength; + private int currentToken; + private String currentTokenValue; + private boolean isAutoquoting; + private char autoquoteChar; + + /* + public static void main(String[] args) { + for (int i = 0; i < args.length; ++i) { + MailcapTokenizer tokenizer = new MailcapTokenizer(args[i]); + + System.out.println("Original: |" + args[i] + "|"); + + int currentToken = tokenizer.nextToken(); + while (currentToken != EOI_TOKEN) { + switch(currentToken) { + case UNKNOWN_TOKEN: + System.out.println(" Unknown Token: |" + tokenizer.getCurrentTokenValue() + "|"); + break; + case START_TOKEN: + System.out.println(" Start Token: |" + tokenizer.getCurrentTokenValue() + "|"); + break; + case STRING_TOKEN: + System.out.println(" String Token: |" + tokenizer.getCurrentTokenValue() + "|"); + break; + case EOI_TOKEN: + System.out.println(" EOI Token: |" + tokenizer.getCurrentTokenValue() + "|"); + break; + case SLASH_TOKEN: + System.out.println(" Slash Token: |" + tokenizer.getCurrentTokenValue() + "|"); + break; + case SEMICOLON_TOKEN: + System.out.println(" Semicolon Token: |" + tokenizer.getCurrentTokenValue() + "|"); + break; + case EQUALS_TOKEN: + System.out.println(" Equals Token: |" + tokenizer.getCurrentTokenValue() + "|"); + break; + default: + System.out.println(" Really Unknown Token: |" + tokenizer.getCurrentTokenValue() + "|"); + break; + } + + currentToken = tokenizer.nextToken(); + } + + System.out.println(""); + } + } + */ +}
diff --git a/android/activation/src/main/java/com/sun/activation/registries/MimeTypeEntry.java b/android/activation/src/main/java/com/sun/activation/registries/MimeTypeEntry.java new file mode 100644 index 0000000..3582f77 --- /dev/null +++ b/android/activation/src/main/java/com/sun/activation/registries/MimeTypeEntry.java
@@ -0,0 +1,35 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package com.sun.activation.registries; + +import java.lang.*; + +public class MimeTypeEntry { + private String type; + private String extension; + + public MimeTypeEntry(String mime_type, String file_ext) { + type = mime_type; + extension = file_ext; + } + + public String getMIMEType() { + return type; + } + + public String getFileExtension() { + return extension; + } + + public String toString() { + return "MIMETypeEntry: " + type + ", " + extension; + } +}
diff --git a/android/activation/src/main/java/com/sun/activation/registries/MimeTypeFile.java b/android/activation/src/main/java/com/sun/activation/registries/MimeTypeFile.java new file mode 100644 index 0000000..4692791 --- /dev/null +++ b/android/activation/src/main/java/com/sun/activation/registries/MimeTypeFile.java
@@ -0,0 +1,302 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package com.sun.activation.registries; + +import java.io.*; +import java.util.*; + +public class MimeTypeFile { + private String fname = null; + private Hashtable type_hash = new Hashtable(); + + /** + * The construtor that takes a filename as an argument. + * + * @param new_fname The file name of the mime types file. + */ + public MimeTypeFile(String new_fname) throws IOException { + File mime_file = null; + FileReader fr = null; + + fname = new_fname; // remember the file name + + mime_file = new File(fname); // get a file object + + fr = new FileReader(mime_file); + + try { + parse(new BufferedReader(fr)); + } finally { + try { + fr.close(); // close it + } catch (IOException e) { + // ignore it + } + } + } + + public MimeTypeFile(InputStream is) throws IOException { + parse(new BufferedReader(new InputStreamReader(is, "iso-8859-1"))); + } + + /** + * Creates an empty DB. + */ + public MimeTypeFile() { + } + + /** + * get the MimeTypeEntry based on the file extension + */ + public MimeTypeEntry getMimeTypeEntry(String file_ext) { + return (MimeTypeEntry)type_hash.get((Object)file_ext); + } + + /** + * Get the MIME type string corresponding to the file extension. + */ + public String getMIMETypeString(String file_ext) { + MimeTypeEntry entry = this.getMimeTypeEntry(file_ext); + + if (entry != null) + return entry.getMIMEType(); + else + return null; + } + + /** + * Appends string of entries to the types registry, must be valid + * .mime.types format. + * A mime.types entry is one of two forms: + * + * type/subtype ext1 ext2 ... + * or + * type=type/subtype desc="description of type" exts=ext1,ext2,... + * + * Example: + * # this is a test + * audio/basic au + * text/plain txt text + * type=application/postscript exts=ps,eps + */ + public void appendToRegistry(String mime_types) { + try { + parse(new BufferedReader(new StringReader(mime_types))); + } catch (IOException ex) { + // can't happen + } + } + + /** + * Parse a stream of mime.types entries. + */ + private void parse(BufferedReader buf_reader) throws IOException { + String line = null, prev = null; + + while ((line = buf_reader.readLine()) != null) { + if (prev == null) + prev = line; + else + prev += line; + int end = prev.length(); + if (prev.length() > 0 && prev.charAt(end - 1) == '\\') { + prev = prev.substring(0, end - 1); + continue; + } + this.parseEntry(prev); + prev = null; + } + if (prev != null) + this.parseEntry(prev); + } + + /** + * Parse single mime.types entry. + */ + private void parseEntry(String line) { + String mime_type = null; + String file_ext = null; + line = line.trim(); + + if (line.length() == 0) // empty line... + return; // BAIL! + + // check to see if this is a comment line? + if (line.charAt(0) == '#') + return; // then we are done! + + // is it a new format line or old format? + if (line.indexOf('=') > 0) { + // new format + LineTokenizer lt = new LineTokenizer(line); + while (lt.hasMoreTokens()) { + String name = lt.nextToken(); + String value = null; + if (lt.hasMoreTokens() && lt.nextToken().equals("=") && + lt.hasMoreTokens()) + value = lt.nextToken(); + if (value == null) { + if (LogSupport.isLoggable()) + LogSupport.log("Bad .mime.types entry: " + line); + return; + } + if (name.equals("type")) + mime_type = value; + else if (name.equals("exts")) { + StringTokenizer st = new StringTokenizer(value, ","); + while (st.hasMoreTokens()) { + file_ext = st.nextToken(); + MimeTypeEntry entry = + new MimeTypeEntry(mime_type, file_ext); + type_hash.put(file_ext, entry); + if (LogSupport.isLoggable()) + LogSupport.log("Added: " + entry.toString()); + } + } + } + } else { + // old format + // count the tokens + StringTokenizer strtok = new StringTokenizer(line); + int num_tok = strtok.countTokens(); + + if (num_tok == 0) // empty line + return; + + mime_type = strtok.nextToken(); // get the MIME type + + while (strtok.hasMoreTokens()) { + MimeTypeEntry entry = null; + + file_ext = strtok.nextToken(); + entry = new MimeTypeEntry(mime_type, file_ext); + type_hash.put(file_ext, entry); + if (LogSupport.isLoggable()) + LogSupport.log("Added: " + entry.toString()); + } + } + } + + // for debugging + /* + public static void main(String[] argv) throws Exception { + MimeTypeFile mf = new MimeTypeFile(argv[0]); + System.out.println("ext " + argv[1] + " type " + + mf.getMIMETypeString(argv[1])); + System.exit(0); + } + */ +} + +class LineTokenizer { + private int currentPosition; + private int maxPosition; + private String str; + private Vector stack = new Vector(); + private static final String singles = "="; // single character tokens + + /** + * Constructs a tokenizer for the specified string. + * <p> + * + * @param str a string to be parsed. + */ + public LineTokenizer(String str) { + currentPosition = 0; + this.str = str; + maxPosition = str.length(); + } + + /** + * Skips white space. + */ + private void skipWhiteSpace() { + while ((currentPosition < maxPosition) && + Character.isWhitespace(str.charAt(currentPosition))) { + currentPosition++; + } + } + + /** + * Tests if there are more tokens available from this tokenizer's string. + * + * @return <code>true</code> if there are more tokens available from this + * tokenizer's string; <code>false</code> otherwise. + */ + public boolean hasMoreTokens() { + if (stack.size() > 0) + return true; + skipWhiteSpace(); + return (currentPosition < maxPosition); + } + + /** + * Returns the next token from this tokenizer. + * + * @return the next token from this tokenizer. + * @exception NoSuchElementException if there are no more tokens in this + * tokenizer's string. + */ + public String nextToken() { + int size = stack.size(); + if (size > 0) { + String t = (String)stack.elementAt(size - 1); + stack.removeElementAt(size - 1); + return t; + } + skipWhiteSpace(); + + if (currentPosition >= maxPosition) { + throw new NoSuchElementException(); + } + + int start = currentPosition; + char c = str.charAt(start); + if (c == '"') { + currentPosition++; + boolean filter = false; + while (currentPosition < maxPosition) { + c = str.charAt(currentPosition++); + if (c == '\\') { + currentPosition++; + filter = true; + } else if (c == '"') { + String s; + + if (filter) { + StringBuffer sb = new StringBuffer(); + for (int i = start + 1; i < currentPosition - 1; i++) { + c = str.charAt(i); + if (c != '\\') + sb.append(c); + } + s = sb.toString(); + } else + s = str.substring(start + 1, currentPosition - 1); + return s; + } + } + } else if (singles.indexOf(c) >= 0) { + currentPosition++; + } else { + while ((currentPosition < maxPosition) && + singles.indexOf(str.charAt(currentPosition)) < 0 && + !Character.isWhitespace(str.charAt(currentPosition))) { + currentPosition++; + } + } + return str.substring(start, currentPosition); + } + + public void pushToken(String token) { + stack.addElement(token); + } +}
diff --git a/android/activation/src/main/java/javax/activation/ActivationDataFlavor.java b/android/activation/src/main/java/javax/activation/ActivationDataFlavor.java new file mode 100644 index 0000000..64b73f0 --- /dev/null +++ b/android/activation/src/main/java/javax/activation/ActivationDataFlavor.java
@@ -0,0 +1,234 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package javax.activation; + +//import java.awt.datatransfer.DataFlavor; +import java.io.IOException; +import javax.activation.MimeType; + +/** + * The ActivationDataFlavor class is a special subclass of + * <code>java.awt.datatransfer.DataFlavor</code>. It allows the JAF to + * set all three values stored by the DataFlavor class via a new + * constructor. It also contains improved MIME parsing in the <code>equals + * </code> method. Except for the improved parsing, its semantics are + * identical to that of the JDK's DataFlavor class. + */ + +public class ActivationDataFlavor /*extends DataFlavor*/ { + + /* + * Raison d'etre: + * + * The DataFlavor class included in JDK 1.1 has several limitations + * including piss poor MIME type parsing, and the limitation of + * only supporting serialized objects and InputStreams as + * representation objects. This class 'fixes' that. + */ + + // I think for now I'll keep copies of all the variables and + // then later I may choose try to better coexist with the base + // class *sigh* + private String mimeType = null; + private MimeType mimeObject = null; + private String humanPresentableName = null; + private Class representationClass = null; + + /** + * Construct a DataFlavor that represents an arbitrary + * Java object. This constructor is an extension of the + * JDK's DataFlavor in that it allows the explicit setting + * of all three DataFlavor attributes. + * <p> + * The returned DataFlavor will have the following characteristics: + * <p> + * representationClass = representationClass<br> + * mimeType = mimeType<br> + * humanName = humanName + * <p> + * + * @param representationClass the class used in this DataFlavor + * @param mimeType the MIME type of the data represented by this class + * @param humanPresentableName the human presentable name of the flavor + */ + public ActivationDataFlavor(Class representationClass, + String mimeType, String humanPresentableName) { + //super(mimeType, humanPresentableName); // need to call super + + // init private variables: + this.mimeType = mimeType; + this.humanPresentableName = humanPresentableName; + this.representationClass = representationClass; + } + + /** + * Construct a DataFlavor that represents a MimeType. + * <p> + * The returned DataFlavor will have the following characteristics: + * <p> + * If the mimeType is "application/x-java-serialized-object; + * class=", the result is the same as calling new + * DataFlavor(Class.forName()) as above. + * <p> + * otherwise: + * <p> + * representationClass = InputStream<p> + * mimeType = mimeType<p> + * + * @param representationClass the class used in this DataFlavor + * @param humanPresentableName the human presentable name of the flavor + */ + public ActivationDataFlavor(Class representationClass, + String humanPresentableName) { + //super(representationClass, humanPresentableName); + //this.mimeType = super.getMimeType(); + this.mimeType = "application/x-java-serialized-object"; + this.representationClass = representationClass; + this.humanPresentableName = humanPresentableName; + } + + /** + * Construct a DataFlavor that represents a MimeType. + * <p> + * The returned DataFlavor will have the following characteristics: + * <p> + * If the mimeType is "application/x-java-serialized-object; class=", + * the result is the same as calling new DataFlavor(Class.forName()) as + * above, otherwise: + * <p> + * representationClass = InputStream<p> + * mimeType = mimeType + * + * @param mimeType the MIME type of the data represented by this class + * @param humanPresentableName the human presentable name of the flavor + */ + public ActivationDataFlavor(String mimeType, String humanPresentableName) { + //super(mimeType, humanPresentableName); + this.mimeType = mimeType; + try { + this.representationClass = Class.forName("java.io.InputStream"); + } catch (ClassNotFoundException ex) { + // XXX - should never happen, ignore it + } + this.humanPresentableName = humanPresentableName; + } + + /** + * Return the MIME type for this DataFlavor. + * + * @return the MIME type + */ + public String getMimeType() { + return mimeType; + } + + /** + * Return the representation class. + * + * @return the representation class + */ + public Class getRepresentationClass() { + return representationClass; + } + + /** + * Return the Human Presentable name. + * + * @return the human presentable name + */ + public String getHumanPresentableName() { + return humanPresentableName; + } + + /** + * Set the human presentable name. + * + * @param humanPresentableName the name to set + */ + public void setHumanPresentableName(String humanPresentableName) { + this.humanPresentableName = humanPresentableName; + } + + /** + * Compares the DataFlavor passed in with this DataFlavor; calls + * the <code>isMimeTypeEqual</code> method. + * + * @param dataFlavor the DataFlavor to compare with + * @return true if the MIME type and representation class + * are the same + */ + public boolean equals(ActivationDataFlavor dataFlavor) { + return (isMimeTypeEqual(dataFlavor.mimeType) && + dataFlavor.getRepresentationClass() == representationClass); + } + + /** + * Is the string representation of the MIME type passed in equivalent + * to the MIME type of this DataFlavor. <p> + * + * ActivationDataFlavor delegates the comparison of MIME types to + * the MimeType class included as part of the JavaBeans Activation + * Framework. This provides a more robust comparison than is normally + * available in the DataFlavor class. + * + * @param mimeType the MIME type + * @return true if the same MIME type + */ + public boolean isMimeTypeEqual(String mimeType) { + MimeType mt = null; + try { + if (mimeObject == null) + mimeObject = new MimeType(this.mimeType); + mt = new MimeType(mimeType); + } catch (MimeTypeParseException e) { + // something didn't parse, do a crude comparison + return this.mimeType.equalsIgnoreCase(mimeType); + } + + return mimeObject.match(mt); + } + + /** + * Called on DataFlavor for every MIME Type parameter to allow DataFlavor + * subclasses to handle special parameters like the text/plain charset + * parameters, whose values are case insensitive. (MIME type parameter + * values are supposed to be case sensitive). + * <p> + * This method is called for each parameter name/value pair and should + * return the normalized representation of the parameterValue. + * This method is never invoked by this implementation. + * + * @param parameterName the parameter name + * @param parameterValue the parameter value + * @return the normalized parameter value + * @deprecated + */ + protected String normalizeMimeTypeParameter(String parameterName, + String parameterValue) { + return parameterValue; + } + + /** + * Called for each MIME type string to give DataFlavor subtypes the + * opportunity to change how the normalization of MIME types is + * accomplished. + * One possible use would be to add default parameter/value pairs in cases + * where none are present in the MIME type string passed in. + * This method is never invoked by this implementation. + * + * @param mimeType the MIME type + * @return the normalized MIME type + * @deprecated + */ + protected String normalizeMimeType(String mimeType) { + return mimeType; + } +}
diff --git a/android/activation/src/main/java/javax/activation/CommandInfo.java b/android/activation/src/main/java/javax/activation/CommandInfo.java new file mode 100644 index 0000000..792c582 --- /dev/null +++ b/android/activation/src/main/java/javax/activation/CommandInfo.java
@@ -0,0 +1,215 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package javax.activation; + +import java.io.*; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.security.AccessController; +import java.security.PrivilegedAction; + +/** + * The CommandInfo class is used by CommandMap implementations to + * describe the results of command requests. It provides the requestor + * with both the verb requested, as well as an instance of the + * bean. There is also a method that will return the name of the + * class that implements the command but <i>it is not guaranteed to + * return a valid value</i>. The reason for this is to allow CommandMap + * implmentations that subclass CommandInfo to provide special + * behavior. For example a CommandMap could dynamically generate + * JavaBeans. In this case, it might not be possible to create an + * object with all the correct state information solely from the class + * name. + */ + +public class CommandInfo { + private String verb; + private String className; + + /** + * The Constructor for CommandInfo. + * @param verb The command verb this CommandInfo decribes. + * @param className The command's fully qualified class name. + */ + public CommandInfo(String verb, String className) { + this.verb = verb; + this.className = className; + } + + /** + * Return the command verb. + * + * @return the command verb. + */ + public String getCommandName() { + return verb; + } + + /** + * Return the command's class name. <i>This method MAY return null in + * cases where a CommandMap subclassed CommandInfo for its + * own purposes.</i> In other words, it might not be possible to + * create the correct state in the command by merely knowing + * its class name. <b>DO NOT DEPEND ON THIS METHOD RETURNING + * A VALID VALUE!</b> + * + * @return The class name of the command, or <i>null</i> + */ + public String getCommandClass() { + return className; + } + + /** + * Return the instantiated JavaBean component. + * <p> + * If the current runtime environment supports + * {@link java.beans.Beans#instantiate Beans.instantiate}, + * use it to instantiate the JavaBeans component. Otherwise, use + * {@link java.lang.Class#forName Class.forName}. + * <p> + * The component class needs to be public. + * On Java SE 9 and newer, if the component class is in a named module, + * it needs to be in an exported package. + * <p> + * If the bean implements the <code>javax.activation.CommandObject</code> + * interface, call its <code>setCommandContext</code> method. + * <p> + * If the DataHandler parameter is null, then the bean is + * instantiated with no data. NOTE: this may be useful + * if for some reason the DataHandler that is passed in + * throws IOExceptions when this method attempts to + * access its InputStream. It will allow the caller to + * retrieve a reference to the bean if it can be + * instantiated. + * <p> + * If the bean does NOT implement the CommandObject interface, + * this method will check if it implements the + * java.io.Externalizable interface. If it does, the bean's + * readExternal method will be called if an InputStream + * can be acquired from the DataHandler.<p> + * + * @param dh The DataHandler that describes the data to be + * passed to the command. + * @param loader The ClassLoader to be used to instantiate the bean. + * @return The bean + * @exception IOException for failures reading data + * @exception ClassNotFoundException if command object class can't + * be found + * @see java.beans.Beans#instantiate + * @see javax.activation.CommandObject + */ + public Object getCommandObject(DataHandler dh, ClassLoader loader) + throws IOException, ClassNotFoundException { + Object new_bean = null; + + // try to instantiate the bean + new_bean = Beans.instantiate(loader, className); + + // if we got one and it is a CommandObject + if (new_bean != null) { + if (new_bean instanceof CommandObject) { + ((CommandObject)new_bean).setCommandContext(verb, dh); + } else if (new_bean instanceof Externalizable) { + if (dh != null) { + InputStream is = dh.getInputStream(); + if (is != null) { + ((Externalizable)new_bean).readExternal( + new ObjectInputStream(is)); + } + } + } + } + + return new_bean; + } + + /** + * Helper class to invoke Beans.instantiate reflectively or the equivalent + * with core reflection when module java.desktop is not readable. + */ + private static final class Beans { + static final Method instantiateMethod; + + static { + Method m; + try { + Class<?> c = Class.forName("java.beans.Beans"); + m = c.getDeclaredMethod("instantiate", ClassLoader.class, String.class); + } catch (ClassNotFoundException e) { + m = null; + } catch (NoSuchMethodException e) { + m = null; + } + instantiateMethod = m; + } + + /** + * Equivalent to invoking java.beans.Beans.instantiate(loader, cn) + */ + static Object instantiate(ClassLoader loader, String cn) + throws IOException, ClassNotFoundException { + + Exception exception; + + if (instantiateMethod != null) { + + // invoke Beans.instantiate + try { + return instantiateMethod.invoke(null, loader, cn); + } catch (InvocationTargetException e) { + exception = e; + } catch (IllegalAccessException e) { + exception = e; + } + + } else { + + SecurityManager security = System.getSecurityManager(); + if (security != null) { + // if it's ok with the SecurityManager, it's ok with me. + String cname = cn.replace('/', '.'); + if (cname.startsWith("[")) { + int b = cname.lastIndexOf('[') + 2; + if (b > 1 && b < cname.length()) { + cname = cname.substring(b); + } + } + int i = cname.lastIndexOf('.'); + if (i != -1) { + security.checkPackageAccess(cname.substring(0, i)); + } + } + + // Beans.instantiate specified to use SCL when loader is null + if (loader == null) { + loader = (ClassLoader) + AccessController.doPrivileged(new PrivilegedAction() { + public Object run() { + ClassLoader cl = null; + try { + cl = ClassLoader.getSystemClassLoader(); + } catch (SecurityException ex) { } + return cl; + } + }); + } + Class<?> beanClass = Class.forName(cn, true, loader); + try { + return beanClass.newInstance(); + } catch (Exception ex) { + throw new ClassNotFoundException(beanClass + ": " + ex, ex); + } + + } + return null; + } + } +}
diff --git a/android/activation/src/main/java/javax/activation/CommandMap.java b/android/activation/src/main/java/javax/activation/CommandMap.java new file mode 100644 index 0000000..69f4910 --- /dev/null +++ b/android/activation/src/main/java/javax/activation/CommandMap.java
@@ -0,0 +1,219 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package javax.activation; + +import java.util.Map; +import java.util.WeakHashMap; + + +/** + * The CommandMap class provides an interface to a registry of + * command objects available in the system. + * Developers are expected to either use the CommandMap + * implementation included with this package (MailcapCommandMap) or + * develop their own. Note that some of the methods in this class are + * abstract. + */ +public abstract class CommandMap { + private static CommandMap defaultCommandMap = null; + private static Map<ClassLoader,CommandMap> map = + new WeakHashMap<ClassLoader,CommandMap>(); + + + /** + * Get the default CommandMap. + * + * <ul> + * <li> In cases where a CommandMap instance has been previously set + * to some value (via <i>setDefaultCommandMap</i>) + * return the CommandMap. + * <li> + * In cases where no CommandMap has been set, the CommandMap + * creates an instance of <code>MailcapCommandMap</code> and + * set that to the default, returning its value. + * + * </ul> + * + * @return the CommandMap + */ + public static synchronized CommandMap getDefaultCommandMap() { + if (defaultCommandMap != null) + return defaultCommandMap; + + // fetch per-thread-context-class-loader default + ClassLoader tccl = SecuritySupport.getContextClassLoader(); + CommandMap def = map.get(tccl); + if (def == null) { + def = new MailcapCommandMap(); + map.put(tccl, def); + } + return def; + } + + /** + * Set the default CommandMap. Reset the CommandMap to the default by + * calling this method with <code>null</code>. + * + * @param commandMap The new default CommandMap. + * @exception SecurityException if the caller doesn't have permission + * to change the default + */ + public static synchronized void setDefaultCommandMap(CommandMap commandMap) { + SecurityManager security = System.getSecurityManager(); + if (security != null) { + try { + // if it's ok with the SecurityManager, it's ok with me... + security.checkSetFactory(); + } catch (SecurityException ex) { + // otherwise, we also allow it if this code and the + // factory come from the same (non-system) class loader (e.g., + // the JAF classes were loaded with the applet classes). + ClassLoader cl = CommandMap.class.getClassLoader(); + if (cl == null || cl.getParent() == null || + cl != commandMap.getClass().getClassLoader()) { + throw ex; + } + } + } + // remove any per-thread-context-class-loader CommandMap + map.remove(SecuritySupport.getContextClassLoader()); + defaultCommandMap = commandMap; + } + + /** + * Get the preferred command list from a MIME Type. The actual semantics + * are determined by the implementation of the CommandMap. + * + * @param mimeType the MIME type + * @return the CommandInfo classes that represent the command Beans. + */ + abstract public CommandInfo[] getPreferredCommands(String mimeType); + + /** + * Get the preferred command list from a MIME Type. The actual semantics + * are determined by the implementation of the CommandMap. <p> + * + * The <code>DataSource</code> provides extra information, such as + * the file name, that a CommandMap implementation may use to further + * refine the list of commands that are returned. The implementation + * in this class simply calls the <code>getPreferredCommands</code> + * method that ignores this argument. + * + * @param mimeType the MIME type + * @param ds a DataSource for the data + * @return the CommandInfo classes that represent the command Beans. + * @since JAF 1.1 + */ + public CommandInfo[] getPreferredCommands(String mimeType, DataSource ds) { + return getPreferredCommands(mimeType); + } + + /** + * Get all the available commands for this type. This method + * should return all the possible commands for this MIME type. + * + * @param mimeType the MIME type + * @return the CommandInfo objects representing all the commands. + */ + abstract public CommandInfo[] getAllCommands(String mimeType); + + /** + * Get all the available commands for this type. This method + * should return all the possible commands for this MIME type. <p> + * + * The <code>DataSource</code> provides extra information, such as + * the file name, that a CommandMap implementation may use to further + * refine the list of commands that are returned. The implementation + * in this class simply calls the <code>getAllCommands</code> + * method that ignores this argument. + * + * @param mimeType the MIME type + * @param ds a DataSource for the data + * @return the CommandInfo objects representing all the commands. + * @since JAF 1.1 + */ + public CommandInfo[] getAllCommands(String mimeType, DataSource ds) { + return getAllCommands(mimeType); + } + + /** + * Get the default command corresponding to the MIME type. + * + * @param mimeType the MIME type + * @param cmdName the command name + * @return the CommandInfo corresponding to the command. + */ + abstract public CommandInfo getCommand(String mimeType, String cmdName); + + /** + * Get the default command corresponding to the MIME type. <p> + * + * The <code>DataSource</code> provides extra information, such as + * the file name, that a CommandMap implementation may use to further + * refine the command that is chosen. The implementation + * in this class simply calls the <code>getCommand</code> + * method that ignores this argument. + * + * @param mimeType the MIME type + * @param cmdName the command name + * @param ds a DataSource for the data + * @return the CommandInfo corresponding to the command. + * @since JAF 1.1 + */ + public CommandInfo getCommand(String mimeType, String cmdName, + DataSource ds) { + return getCommand(mimeType, cmdName); + } + + /** + * Locate a DataContentHandler that corresponds to the MIME type. + * The mechanism and semantics for determining this are determined + * by the implementation of the particular CommandMap. + * + * @param mimeType the MIME type + * @return the DataContentHandler for the MIME type + */ + abstract public DataContentHandler createDataContentHandler(String + mimeType); + + /** + * Locate a DataContentHandler that corresponds to the MIME type. + * The mechanism and semantics for determining this are determined + * by the implementation of the particular CommandMap. <p> + * + * The <code>DataSource</code> provides extra information, such as + * the file name, that a CommandMap implementation may use to further + * refine the choice of DataContentHandler. The implementation + * in this class simply calls the <code>createDataContentHandler</code> + * method that ignores this argument. + * + * @param mimeType the MIME type + * @param ds a DataSource for the data + * @return the DataContentHandler for the MIME type + * @since JAF 1.1 + */ + public DataContentHandler createDataContentHandler(String mimeType, + DataSource ds) { + return createDataContentHandler(mimeType); + } + + /** + * Get all the MIME types known to this command map. + * If the command map doesn't support this operation, + * null is returned. + * + * @return array of MIME types as strings, or null if not supported + * @since JAF 1.1 + */ + public String[] getMimeTypes() { + return null; + } +}
diff --git a/android/activation/src/main/java/javax/activation/CommandObject.java b/android/activation/src/main/java/javax/activation/CommandObject.java new file mode 100644 index 0000000..548c731 --- /dev/null +++ b/android/activation/src/main/java/javax/activation/CommandObject.java
@@ -0,0 +1,38 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package javax.activation; + +import java.io.IOException; + +/** + * JavaBeans components that are Activation Framework aware implement + * this interface to find out which command verb they're being asked + * to perform, and to obtain the DataHandler representing the + * data they should operate on. JavaBeans that don't implement + * this interface may be used as well. Such commands may obtain + * the data using the Externalizable interface, or using an + * application-specific method. + */ +public interface CommandObject { + + /** + * Initialize the Command with the verb it is requested to handle + * and the DataHandler that describes the data it will + * operate on. <b>NOTE:</b> it is acceptable for the caller + * to pass <i>null</i> as the value for <code>DataHandler</code>. + * + * @param verb The Command Verb this object refers to. + * @param dh The DataHandler. + * @exception IOException for failures accessing data + */ + public void setCommandContext(String verb, DataHandler dh) + throws IOException; +}
diff --git a/android/activation/src/main/java/javax/activation/DataContentHandler.java b/android/activation/src/main/java/javax/activation/DataContentHandler.java new file mode 100644 index 0000000..ae78653 --- /dev/null +++ b/android/activation/src/main/java/javax/activation/DataContentHandler.java
@@ -0,0 +1,82 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package javax.activation; + +//import java.awt.datatransfer.DataFlavor; +//import java.awt.datatransfer.UnsupportedFlavorException; +import java.io.InputStream; +import java.io.IOException; +import java.io.OutputStream; +import javax.activation.DataSource; + +/** + * The DataContentHandler interface is implemented by objects that can + * be used to extend the capabilities of the DataHandler's implementation + * of the Transferable interface. Through <code>DataContentHandlers</code> + * the framework can be extended to convert streams in to objects, and + * to write objects to streams. <p> + * + * Applications don't generally call the methods in DataContentHandlers + * directly. Instead, an application calls the equivalent methods in + * DataHandler. The DataHandler will attempt to find an appropriate + * DataContentHandler that corresponds to its MIME type using the + * current DataContentHandlerFactory. The DataHandler then calls + * through to the methods in the DataContentHandler. + */ + +public interface DataContentHandler { + /** + * Returns an array of DataFlavor objects indicating the flavors the + * data can be provided in. The array should be ordered according to + * preference for providing the data (from most richly descriptive to + * least descriptive). + * + * @return The DataFlavors. + */ + public ActivationDataFlavor[] getTransferDataFlavors(); + + /** + * Returns an object which represents the data to be transferred. + * The class of the object returned is defined by the representation class + * of the flavor. + * + * @param df The DataFlavor representing the requested type. + * @param ds The DataSource representing the data to be converted. + * @return The constructed Object. + * @exception IOException if the data can't be accessed + */ + public Object getTransferData(ActivationDataFlavor df, DataSource ds) + throws /*UnsupportedFlavorException,*/ IOException; + + /** + * Return an object representing the data in its most preferred form. + * Generally this will be the form described by the first DataFlavor + * returned by the <code>getTransferDataFlavors</code> method. + * + * @param ds The DataSource representing the data to be converted. + * @return The constructed Object. + * @exception IOException if the data can't be accessed + */ + public Object getContent(DataSource ds) throws IOException; + + /** + * Convert the object to a byte stream of the specified MIME type + * and write it to the output stream. + * + * @param obj The object to be converted. + * @param mimeType The requested MIME type of the resulting byte stream. + * @param os The output stream into which to write the converted + * byte stream. + * @exception IOException errors writing to the stream + */ + public void writeTo(Object obj, String mimeType, OutputStream os) + throws IOException; +}
diff --git a/android/activation/src/main/java/javax/activation/DataContentHandlerFactory.java b/android/activation/src/main/java/javax/activation/DataContentHandlerFactory.java new file mode 100644 index 0000000..0cd0f5c --- /dev/null +++ b/android/activation/src/main/java/javax/activation/DataContentHandlerFactory.java
@@ -0,0 +1,31 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package javax.activation; + +/** + * This interface defines a factory for <code>DataContentHandlers</code>. An + * implementation of this interface should map a MIME type into an + * instance of DataContentHandler. The design pattern for classes implementing + * this interface is the same as for the ContentHandler mechanism used in + * <code>java.net.URL</code>. + */ + +public interface DataContentHandlerFactory { + + /** + * Creates a new DataContentHandler object for the MIME type. + * + * @param mimeType the MIME type to create the DataContentHandler for. + * @return The new <code>DataContentHandler</code>, or <i>null</i> + * if none are found. + */ + public DataContentHandler createDataContentHandler(String mimeType); +}
diff --git a/android/activation/src/main/java/javax/activation/DataHandler.java b/android/activation/src/main/java/javax/activation/DataHandler.java new file mode 100644 index 0000000..40a842d --- /dev/null +++ b/android/activation/src/main/java/javax/activation/DataHandler.java
@@ -0,0 +1,881 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package javax.activation; + +import java.io.InputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.io.OutputStreamWriter; +import java.net.URL; +//import java.awt.datatransfer.Transferable; +//import java.awt.datatransfer.DataFlavor; +//import java.awt.datatransfer.UnsupportedFlavorException; + +/** + * The DataHandler class provides a consistent interface to data + * available in many different sources and formats. + * It manages simple stream to string conversions and related operations + * using DataContentHandlers. + * It provides access to commands that can operate on the data. + * The commands are found using a CommandMap. <p> + * + * <b>DataHandler and the Transferable Interface</b><p> + * DataHandler implements the Transferable interface so that data can + * be used in AWT data transfer operations, such as cut and paste and + * drag and drop. The implementation of the Transferable interface + * relies on the availability of an installed DataContentHandler + * object corresponding to the MIME type of the data represented in + * the specific instance of the DataHandler.<p> + * + * <b>DataHandler and CommandMaps</b><p> + * The DataHandler keeps track of the current CommandMap that it uses to + * service requests for commands (<code>getCommand</code>, + * <code>getAllCommands</code>, <code>getPreferredCommands</code>). + * Each instance of a DataHandler may have a CommandMap associated with + * it using the <code>setCommandMap</code> method. If a CommandMap was + * not set, DataHandler calls the <code>getDefaultCommandMap</code> + * method in CommandMap and uses the value it returns. See + * <i>CommandMap</i> for more information. <p> + * + * <b>DataHandler and URLs</b><p> + * The current DataHandler implementation creates a private + * instance of URLDataSource when it is constructed with a URL. + * + * @see javax.activation.CommandMap + * @see javax.activation.DataContentHandler + * @see javax.activation.DataSource + * @see javax.activation.URLDataSource + */ + +public class DataHandler /*implements Transferable*/ { + + // Use the datasource to indicate whether we were started via the + // DataSource constructor or the object constructor. + private DataSource dataSource = null; + private DataSource objDataSource = null; + + // The Object and mimetype from the constructor (if passed in). + // object remains null if it was instantiated with a + // DataSource. + private Object object = null; + private String objectMimeType = null; + + // Keep track of the CommandMap + private CommandMap currentCommandMap = null; + + // our transfer flavors + private static final ActivationDataFlavor emptyFlavors[] = new ActivationDataFlavor[0]; + private ActivationDataFlavor transferFlavors[] = emptyFlavors; + + // our DataContentHandler + private DataContentHandler dataContentHandler = null; + private DataContentHandler factoryDCH = null; + + // our DataContentHandlerFactory + private static DataContentHandlerFactory factory = null; + private DataContentHandlerFactory oldFactory = null; + // the short representation of the ContentType (sans params) + private String shortType = null; + + /** + * Create a <code>DataHandler</code> instance referencing the + * specified DataSource. The data exists in a byte stream form. + * The DataSource will provide an InputStream to access the data. + * + * @param ds the DataSource + */ + public DataHandler(DataSource ds) { + // save a reference to the incoming DS + dataSource = ds; + oldFactory = factory; // keep track of the factory + } + + /** + * Create a <code>DataHandler</code> instance representing an object + * of this MIME type. This constructor is + * used when the application already has an in-memory representation + * of the data in the form of a Java Object. + * + * @param obj the Java Object + * @param mimeType the MIME type of the object + */ + public DataHandler(Object obj, String mimeType) { + object = obj; + objectMimeType = mimeType; + oldFactory = factory; // keep track of the factory + } + + /** + * Create a <code>DataHandler</code> instance referencing a URL. + * The DataHandler internally creates a <code>URLDataSource</code> + * instance to represent the URL. + * + * @param url a URL object + */ + public DataHandler(URL url) { + dataSource = new URLDataSource(url); + oldFactory = factory; // keep track of the factory + } + + /** + * Return the CommandMap for this instance of DataHandler. + */ + private synchronized CommandMap getCommandMap() { + if (currentCommandMap != null) + return currentCommandMap; + else + return CommandMap.getDefaultCommandMap(); + } + + /** + * Return the DataSource associated with this instance + * of DataHandler. + * <p> + * For DataHandlers that have been instantiated with a DataSource, + * this method returns the DataSource that was used to create the + * DataHandler object. In other cases the DataHandler + * constructs a DataSource from the data used to construct + * the DataHandler. DataSources created for DataHandlers <b>not</b> + * instantiated with a DataSource are cached for performance + * reasons. + * + * @return a valid DataSource object for this DataHandler + */ + public DataSource getDataSource() { + if (dataSource == null) { + // create one on the fly + if (objDataSource == null) + objDataSource = new DataHandlerDataSource(this); + return objDataSource; + } + return dataSource; + } + + /** + * Return the name of the data object. If this DataHandler + * was created with a DataSource, this method calls through + * to the <code>DataSource.getName</code> method, otherwise it + * returns <i>null</i>. + * + * @return the name of the object + */ + public String getName() { + if (dataSource != null) + return dataSource.getName(); + else + return null; + } + + /** + * Return the MIME type of this object as retrieved from + * the source object. Note that this is the <i>full</i> + * type with parameters. + * + * @return the MIME type + */ + public String getContentType() { + if (dataSource != null) // data source case + return dataSource.getContentType(); + else + return objectMimeType; // obj/type case + } + + /** + * Get the InputStream for this object. <p> + * + * For DataHandlers instantiated with a DataSource, the DataHandler + * calls the <code>DataSource.getInputStream</code> method and + * returns the result to the caller. + * <p> + * For DataHandlers instantiated with an Object, the DataHandler + * first attempts to find a DataContentHandler for the Object. If + * the DataHandler can not find a DataContentHandler for this MIME + * type, it throws an UnsupportedDataTypeException. If it is + * successful, it creates a pipe and a thread. The thread uses the + * DataContentHandler's <code>writeTo</code> method to write the + * stream data into one end of the pipe. The other end of the pipe + * is returned to the caller. Because a thread is created to copy + * the data, IOExceptions that may occur during the copy can not be + * propagated back to the caller. The result is an empty stream.<p> + * + * @return the InputStream representing this data + * @exception IOException if an I/O error occurs + * + * @see javax.activation.DataContentHandler#writeTo + * @see javax.activation.UnsupportedDataTypeException + */ + public InputStream getInputStream() throws IOException { + InputStream ins = null; + + if (dataSource != null) { + ins = dataSource.getInputStream(); + } else { + DataContentHandler dch = getDataContentHandler(); + // we won't even try if we can't get a dch + if (dch == null) + throw new UnsupportedDataTypeException( + "no DCH for MIME type " + getBaseType()); + + if (dch instanceof ObjectDataContentHandler) { + if (((ObjectDataContentHandler)dch).getDCH() == null) + throw new UnsupportedDataTypeException( + "no object DCH for MIME type " + getBaseType()); + } + // there is none but the default^^^^^^^^^^^^^^^^ + final DataContentHandler fdch = dch; + + // from bill s. + // ce n'est pas une pipe! + // + // NOTE: This block of code needs to throw exceptions, but + // can't because it is in another thread!!! ARG! + // + final PipedOutputStream pos = new PipedOutputStream(); + PipedInputStream pin = new PipedInputStream(pos); + new Thread( + new Runnable() { + public void run() { + try { + fdch.writeTo(object, objectMimeType, pos); + } catch (IOException e) { + + } finally { + try { + pos.close(); + } catch (IOException ie) { } + } + } + }, + "DataHandler.getInputStream").start(); + ins = pin; + } + + return ins; + } + + /** + * Write the data to an <code>OutputStream</code>.<p> + * + * If the DataHandler was created with a DataSource, writeTo + * retrieves the InputStream and copies the bytes from the + * InputStream to the OutputStream passed in. + * <p> + * If the DataHandler was created with an object, writeTo + * retrieves the DataContentHandler for the object's type. + * If the DataContentHandler was found, it calls the + * <code>writeTo</code> method on the <code>DataContentHandler</code>. + * + * @param os the OutputStream to write to + * @exception IOException if an I/O error occurs + */ + public void writeTo(OutputStream os) throws IOException { + // for the DataSource case + if (dataSource != null) { + InputStream is = null; + byte data[] = new byte[8*1024]; + int bytes_read; + + is = dataSource.getInputStream(); + + try { + while ((bytes_read = is.read(data)) > 0) { + os.write(data, 0, bytes_read); + } + } finally { + is.close(); + is = null; + } + } else { // for the Object case + DataContentHandler dch = getDataContentHandler(); + dch.writeTo(object, objectMimeType, os); + } + } + + /** + * Get an OutputStream for this DataHandler to allow overwriting + * the underlying data. + * If the DataHandler was created with a DataSource, the + * DataSource's <code>getOutputStream</code> method is called. + * Otherwise, <code>null</code> is returned. + * + * @return the OutputStream + * + * @see javax.activation.DataSource#getOutputStream + * @see javax.activation.URLDataSource + */ + public OutputStream getOutputStream() throws IOException { + if (dataSource != null) + return dataSource.getOutputStream(); + else + return null; + } + + /** + * Return the DataFlavors in which this data is available. <p> + * + * Returns an array of DataFlavor objects indicating the flavors + * the data can be provided in. The array is usually ordered + * according to preference for providing the data, from most + * richly descriptive to least richly descriptive.<p> + * + * The DataHandler attempts to find a DataContentHandler that + * corresponds to the MIME type of the data. If one is located, + * the DataHandler calls the DataContentHandler's + * <code>getTransferDataFlavors</code> method. <p> + * + * If a DataContentHandler can <i>not</i> be located, and if the + * DataHandler was created with a DataSource (or URL), one + * DataFlavor is returned that represents this object's MIME type + * and the <code>java.io.InputStream</code> class. If the + * DataHandler was created with an object and a MIME type, + * getTransferDataFlavors returns one DataFlavor that represents + * this object's MIME type and the object's class. + * + * @return an array of data flavors in which this data can be transferred + * @see javax.activation.DataContentHandler#getTransferDataFlavors + */ + public synchronized ActivationDataFlavor[] getTransferDataFlavors() { + if (factory != oldFactory) // if the factory has changed, clear cache + transferFlavors = emptyFlavors; + + // if it's not set, set it... + if (transferFlavors == emptyFlavors) + transferFlavors = getDataContentHandler().getTransferDataFlavors(); + if (transferFlavors == emptyFlavors) + return transferFlavors; // no need to clone an empty array + else + return transferFlavors.clone(); + } + + /** + * Returns whether the specified data flavor is supported + * for this object.<p> + * + * This method iterates through the DataFlavors returned from + * <code>getTransferDataFlavors</code>, comparing each with + * the specified flavor. + * + * @param flavor the requested flavor for the data + * @return true if the data flavor is supported + * @see javax.activation.DataHandler#getTransferDataFlavors + */ + public boolean isDataFlavorSupported(ActivationDataFlavor flavor) { + ActivationDataFlavor[] lFlavors = getTransferDataFlavors(); + + for (int i = 0; i < lFlavors.length; i++) { + if (lFlavors[i].equals(flavor)) + return true; + } + return false; + } + + /** + * Returns an object that represents the data to be + * transferred. The class of the object returned is defined by the + * representation class of the data flavor.<p> + * + * <b>For DataHandler's created with DataSources or URLs:</b><p> + * + * The DataHandler attempts to locate a DataContentHandler + * for this MIME type. If one is found, the passed in DataFlavor + * and the type of the data are passed to its <code>getTransferData</code> + * method. If the DataHandler fails to locate a DataContentHandler + * and the flavor specifies this object's MIME type and the + * <code>java.io.InputStream</code> class, this object's InputStream + * is returned. + * Otherwise it throws an UnsupportedFlavorException. <p> + * + * <b>For DataHandler's created with Objects:</b><p> + * + * The DataHandler attempts to locate a DataContentHandler + * for this MIME type. If one is found, the passed in DataFlavor + * and the type of the data are passed to its getTransferData + * method. If the DataHandler fails to locate a DataContentHandler + * and the flavor specifies this object's MIME type and its class, + * this DataHandler's referenced object is returned. + * Otherwise it throws an UnsupportedFlavorException. + * + * @param flavor the requested flavor for the data + * @return the object + * @exception IOException if an I/O error occurs + * @see javax.activation.ActivationDataFlavor + */ + public Object getTransferData(ActivationDataFlavor flavor) + throws /*UnsupportedFlavorException,*/ IOException { + return getDataContentHandler().getTransferData(flavor, dataSource); + } + + /** + * Set the CommandMap for use by this DataHandler. + * Setting it to <code>null</code> causes the CommandMap to revert + * to the CommandMap returned by the + * <code>CommandMap.getDefaultCommandMap</code> method. + * Changing the CommandMap, or setting it to <code>null</code>, + * clears out any data cached from the previous CommandMap. + * + * @param commandMap the CommandMap to use in this DataHandler + * + * @see javax.activation.CommandMap#setDefaultCommandMap + */ + public synchronized void setCommandMap(CommandMap commandMap) { + if (commandMap != currentCommandMap || commandMap == null) { + // clear cached values... + transferFlavors = emptyFlavors; + dataContentHandler = null; + + currentCommandMap = commandMap; + } + } + + /** + * Return the <i>preferred</i> commands for this type of data. + * This method calls the <code>getPreferredCommands</code> method + * in the CommandMap associated with this instance of DataHandler. + * This method returns an array that represents a subset of + * available commands. In cases where multiple commands for the + * MIME type represented by this DataHandler are present, the + * installed CommandMap chooses the appropriate commands. + * + * @return the CommandInfo objects representing the preferred commands + * + * @see javax.activation.CommandMap#getPreferredCommands + */ + public CommandInfo[] getPreferredCommands() { + if (dataSource != null) + return getCommandMap().getPreferredCommands(getBaseType(), + dataSource); + else + return getCommandMap().getPreferredCommands(getBaseType()); + } + + /** + * Return all the commands for this type of data. + * This method returns an array containing all commands + * for the type of data represented by this DataHandler. The + * MIME type for the underlying data represented by this DataHandler + * is used to call through to the <code>getAllCommands</code> method + * of the CommandMap associated with this DataHandler. + * + * @return the CommandInfo objects representing all the commands + * + * @see javax.activation.CommandMap#getAllCommands + */ + public CommandInfo[] getAllCommands() { + if (dataSource != null) + return getCommandMap().getAllCommands(getBaseType(), dataSource); + else + return getCommandMap().getAllCommands(getBaseType()); + } + + /** + * Get the command <i>cmdName</i>. Use the search semantics as + * defined by the CommandMap installed in this DataHandler. The + * MIME type for the underlying data represented by this DataHandler + * is used to call through to the <code>getCommand</code> method + * of the CommandMap associated with this DataHandler. + * + * @param cmdName the command name + * @return the CommandInfo corresponding to the command + * + * @see javax.activation.CommandMap#getCommand + */ + public CommandInfo getCommand(String cmdName) { + if (dataSource != null) + return getCommandMap().getCommand(getBaseType(), cmdName, + dataSource); + else + return getCommandMap().getCommand(getBaseType(), cmdName); + } + + /** + * Return the data in its preferred Object form. <p> + * + * If the DataHandler was instantiated with an object, return + * the object. <p> + * + * If the DataHandler was instantiated with a DataSource, + * this method uses a DataContentHandler to return the content + * object for the data represented by this DataHandler. If no + * <code>DataContentHandler</code> can be found for the + * the type of this data, the DataHandler returns an + * InputStream for the data. + * + * @return the content. + * @exception IOException if an IOException occurs during + * this operation. + */ + public Object getContent() throws IOException { + if (object != null) + return object; + else + return getDataContentHandler().getContent(getDataSource()); + } + + /** + * A convenience method that takes a CommandInfo object + * and instantiates the corresponding command, usually + * a JavaBean component. + * <p> + * This method calls the CommandInfo's <code>getCommandObject</code> + * method with the <code>ClassLoader</code> used to load + * the <code>javax.activation.DataHandler</code> class itself. + * + * @param cmdinfo the CommandInfo corresponding to a command + * @return the instantiated command object + */ + public Object getBean(CommandInfo cmdinfo) { + Object bean = null; + + try { + // make the bean + ClassLoader cld = null; + // First try the "application's" class loader. + cld = SecuritySupport.getContextClassLoader(); + if (cld == null) + cld = this.getClass().getClassLoader(); + bean = cmdinfo.getCommandObject(this, cld); + } catch (IOException e) { + } catch (ClassNotFoundException e) { } + + return bean; + } + + /** + * Get the DataContentHandler for this DataHandler: <p> + * + * If a DataContentHandlerFactory is set, use it. + * Otherwise look for an object to serve DCH in the + * following order: <p> + * + * 1) if a factory is set, use it <p> + * 2) if a CommandMap is set, use it <p> + * 3) use the default CommandMap <p> + * + * In any case, wrap the real DataContentHandler with one of our own + * to handle any missing cases, fill in defaults, and to ensure that + * we always have a non-null DataContentHandler. + * + * @return the requested DataContentHandler + */ + private synchronized DataContentHandler getDataContentHandler() { + + // make sure the factory didn't change + if (factory != oldFactory) { + oldFactory = factory; + factoryDCH = null; + dataContentHandler = null; + transferFlavors = emptyFlavors; + } + + if (dataContentHandler != null) + return dataContentHandler; + + String simpleMT = getBaseType(); + + if (factoryDCH == null && factory != null) + factoryDCH = factory.createDataContentHandler(simpleMT); + + if (factoryDCH != null) + dataContentHandler = factoryDCH; + + if (dataContentHandler == null) { + if (dataSource != null) + dataContentHandler = getCommandMap(). + createDataContentHandler(simpleMT, dataSource); + else + dataContentHandler = getCommandMap(). + createDataContentHandler(simpleMT); + } + + // getDataContentHandler always uses these 'wrapper' handlers + // to make sure it returns SOMETHING meaningful... + if (dataSource != null) + dataContentHandler = new DataSourceDataContentHandler( + dataContentHandler, + dataSource); + else + dataContentHandler = new ObjectDataContentHandler( + dataContentHandler, + object, + objectMimeType); + return dataContentHandler; + } + + /** + * Use the MimeType class to extract the MIME type/subtype, + * ignoring the parameters. The type is cached. + */ + private synchronized String getBaseType() { + if (shortType == null) { + String ct = getContentType(); + try { + MimeType mt = new MimeType(ct); + shortType = mt.getBaseType(); + } catch (MimeTypeParseException e) { + shortType = ct; + } + } + return shortType; + } + + /** + * Sets the DataContentHandlerFactory. The DataContentHandlerFactory + * is called first to find DataContentHandlers. + * The DataContentHandlerFactory can only be set once. + * <p> + * If the DataContentHandlerFactory has already been set, + * this method throws an Error. + * + * @param newFactory the DataContentHandlerFactory + * @exception Error if the factory has already been defined. + * + * @see javax.activation.DataContentHandlerFactory + */ + public static synchronized void setDataContentHandlerFactory( + DataContentHandlerFactory newFactory) { + if (factory != null) + throw new Error("DataContentHandlerFactory already defined"); + + SecurityManager security = System.getSecurityManager(); + if (security != null) { + try { + // if it's ok with the SecurityManager, it's ok with me... + security.checkSetFactory(); + } catch (SecurityException ex) { + // otherwise, we also allow it if this code and the + // factory come from the same class loader (e.g., + // the JAF classes were loaded with the applet classes). + if (DataHandler.class.getClassLoader() != + newFactory.getClass().getClassLoader()) + throw ex; + } + } + factory = newFactory; + } +} + +/** + * The DataHanderDataSource class implements the + * DataSource interface when the DataHandler is constructed + * with an Object and a mimeType string. + */ +class DataHandlerDataSource implements DataSource { + DataHandler dataHandler = null; + + /** + * The constructor. + */ + public DataHandlerDataSource(DataHandler dh) { + this.dataHandler = dh; + } + + /** + * Returns an <code>InputStream</code> representing this object. + * @return the <code>InputStream</code> + */ + public InputStream getInputStream() throws IOException { + return dataHandler.getInputStream(); + } + + /** + * Returns the <code>OutputStream</code> for this object. + * @return the <code>OutputStream</code> + */ + public OutputStream getOutputStream() throws IOException { + return dataHandler.getOutputStream(); + } + + /** + * Returns the MIME type of the data represented by this object. + * @return the MIME type + */ + public String getContentType() { + return dataHandler.getContentType(); + } + + /** + * Returns the name of this object. + * @return the name of this object + */ + public String getName() { + return dataHandler.getName(); // what else would it be? + } +} + +/* + * DataSourceDataContentHandler + * + * This is a <i>private</i> DataContentHandler that wraps the real + * DataContentHandler in the case where the DataHandler was instantiated + * with a DataSource. + */ +class DataSourceDataContentHandler implements DataContentHandler { + private DataSource ds = null; + private ActivationDataFlavor transferFlavors[] = null; + private DataContentHandler dch = null; + + /** + * The constructor. + */ + public DataSourceDataContentHandler(DataContentHandler dch, DataSource ds) { + this.ds = ds; + this.dch = dch; + } + + /** + * Return the DataFlavors for this <code>DataContentHandler</code>. + * @return the DataFlavors + */ + public ActivationDataFlavor[] getTransferDataFlavors() { + + if (transferFlavors == null) { + if (dch != null) { // is there a dch? + transferFlavors = dch.getTransferDataFlavors(); + } else { + transferFlavors = new ActivationDataFlavor[1]; + transferFlavors[0] = + new ActivationDataFlavor(ds.getContentType(), + ds.getContentType()); + } + } + return transferFlavors; + } + + /** + * Return the Transfer Data of type DataFlavor from InputStream. + * @param df the DataFlavor + * @param ds the DataSource + * @return the constructed Object + */ + public Object getTransferData(ActivationDataFlavor df, DataSource ds) throws + /*UnsupportedFlavorException,*/ IOException { + + if (dch != null) + return dch.getTransferData(df, ds); + else if (df.equals(getTransferDataFlavors()[0])) // only have one now + return ds.getInputStream(); + else + //throw new UnsupportedFlavorException(df); + throw new IOException("Unsupported DataFlavor: " + df); + } + + public Object getContent(DataSource ds) throws IOException { + + if (dch != null) + return dch.getContent(ds); + else + return ds.getInputStream(); + } + + /** + * Write the object to the output stream. + */ + public void writeTo(Object obj, String mimeType, OutputStream os) + throws IOException { + if (dch != null) + dch.writeTo(obj, mimeType, os); + else + throw new UnsupportedDataTypeException( + "no DCH for content type " + ds.getContentType()); + } +} + +/* + * ObjectDataContentHandler + * + * This is a <i>private</i> DataContentHandler that wraps the real + * DataContentHandler in the case where the DataHandler was instantiated + * with an object. + */ +class ObjectDataContentHandler implements DataContentHandler { + private ActivationDataFlavor transferFlavors[] = null; + private Object obj; + private String mimeType; + private DataContentHandler dch = null; + + /** + * The constructor. + */ + public ObjectDataContentHandler(DataContentHandler dch, + Object obj, String mimeType) { + this.obj = obj; + this.mimeType = mimeType; + this.dch = dch; + } + + /** + * Return the DataContentHandler for this object. + * Used only by the DataHandler class. + */ + public DataContentHandler getDCH() { + return dch; + } + + /** + * Return the DataFlavors for this <code>DataContentHandler</code>. + * @return the DataFlavors + */ + public synchronized ActivationDataFlavor[] getTransferDataFlavors() { + if (transferFlavors == null) { + if (dch != null) { + transferFlavors = dch.getTransferDataFlavors(); + } else { + transferFlavors = new ActivationDataFlavor[1]; + transferFlavors[0] = new ActivationDataFlavor(obj.getClass(), + mimeType, mimeType); + } + } + return transferFlavors; + } + + /** + * Return the Transfer Data of type DataFlavor from InputStream. + * @param df the DataFlavor + * @param ds the DataSource + * @return the constructed Object + */ + public Object getTransferData(ActivationDataFlavor df, DataSource ds) + throws /*UnsupportedFlavorException,*/ IOException { + + if (dch != null) + return dch.getTransferData(df, ds); + else if (df.equals(getTransferDataFlavors()[0])) // only have one now + return obj; + else + //throw new UnsupportedFlavorException(df); + throw new IOException("Unsupported DataFlavor: " + df); + + } + + public Object getContent(DataSource ds) { + return obj; + } + + /** + * Write the object to the output stream. + */ + public void writeTo(Object obj, String mimeType, OutputStream os) + throws IOException { + if (dch != null) + dch.writeTo(obj, mimeType, os); + else if (obj instanceof byte[]) + os.write((byte[])obj); + else if (obj instanceof String) { + OutputStreamWriter osw = new OutputStreamWriter(os); + osw.write((String)obj); + osw.flush(); + } else + throw new UnsupportedDataTypeException( + "no object DCH for MIME type " + this.mimeType); + } +}
diff --git a/android/activation/src/main/java/javax/activation/DataSource.java b/android/activation/src/main/java/javax/activation/DataSource.java new file mode 100644 index 0000000..6f9bdb5 --- /dev/null +++ b/android/activation/src/main/java/javax/activation/DataSource.java
@@ -0,0 +1,71 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package javax.activation; + +import java.io.InputStream; +import java.io.OutputStream; +import java.io.IOException; + +/** + * The DataSource interface provides the JavaBeans Activation Framework + * with an abstraction of an arbitrary collection of data. It + * provides a type for that data as well as access + * to it in the form of <code>InputStreams</code> and + * <code>OutputStreams</code> where appropriate. + */ + +public interface DataSource { + + /** + * This method returns an <code>InputStream</code> representing + * the data and throws the appropriate exception if it can + * not do so. Note that a new <code>InputStream</code> object must be + * returned each time this method is called, and the stream must be + * positioned at the beginning of the data. + * + * @return an InputStream + * @exception IOException for failures creating the InputStream + */ + public InputStream getInputStream() throws IOException; + + /** + * This method returns an <code>OutputStream</code> where the + * data can be written and throws the appropriate exception if it can + * not do so. Note that a new <code>OutputStream</code> object must + * be returned each time this method is called, and the stream must + * be positioned at the location the data is to be written. + * + * @return an OutputStream + * @exception IOException for failures creating the OutputStream + */ + public OutputStream getOutputStream() throws IOException; + + /** + * This method returns the MIME type of the data in the form of a + * string. It should always return a valid type. It is suggested + * that getContentType return "application/octet-stream" if the + * DataSource implementation can not determine the data type. + * + * @return the MIME Type + */ + public String getContentType(); + + /** + * Return the <i>name</i> of this object where the name of the object + * is dependant on the nature of the underlying objects. DataSources + * encapsulating files may choose to return the filename of the object. + * (Typically this would be the last component of the filename, not an + * entire pathname.) + * + * @return the name of the object. + */ + public String getName(); +}
diff --git a/android/activation/src/main/java/javax/activation/FileDataSource.java b/android/activation/src/main/java/javax/activation/FileDataSource.java new file mode 100644 index 0000000..3048918 --- /dev/null +++ b/android/activation/src/main/java/javax/activation/FileDataSource.java
@@ -0,0 +1,141 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package javax.activation; + +import java.io.InputStream; +import java.io.OutputStream; +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileNotFoundException; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import com.sun.activation.registries.MimeTypeFile; + +/** + * The FileDataSource class implements a simple DataSource object + * that encapsulates a file. It provides data typing services via + * a FileTypeMap object. <p> + * + * <b>FileDataSource Typing Semantics</b><p> + * + * The FileDataSource class delegates data typing of files + * to an object subclassed from the FileTypeMap class. + * The <code>setFileTypeMap</code> method can be used to explicitly + * set the FileTypeMap for an instance of FileDataSource. If no + * FileTypeMap is set, the FileDataSource will call the FileTypeMap's + * getDefaultFileTypeMap method to get the System's default FileTypeMap. + * + * @see javax.activation.DataSource + * @see javax.activation.FileTypeMap + * @see javax.activation.MimetypesFileTypeMap + */ +public class FileDataSource implements DataSource { + + // keep track of original 'ref' passed in, non-null + // one indicated which was passed in: + private File _file = null; + private FileTypeMap typeMap = null; + + /** + * Creates a FileDataSource from a File object. <i>Note: + * The file will not actually be opened until a method is + * called that requires the file to be opened.</i> + * + * @param file the file + */ + public FileDataSource(File file) { + _file = file; // save the file Object... + } + + /** + * Creates a FileDataSource from + * the specified path name. <i>Note: + * The file will not actually be opened until a method is + * called that requires the file to be opened.</i> + * + * @param name the system-dependent file name. + */ + public FileDataSource(String name) { + this(new File(name)); // use the file constructor + } + + /** + * This method will return an InputStream representing the + * the data and will throw an IOException if it can + * not do so. This method will return a new + * instance of InputStream with each invocation. + * + * @return an InputStream + */ + public InputStream getInputStream() throws IOException { + return new FileInputStream(_file); + } + + /** + * This method will return an OutputStream representing the + * the data and will throw an IOException if it can + * not do so. This method will return a new instance of + * OutputStream with each invocation. + * + * @return an OutputStream + */ + public OutputStream getOutputStream() throws IOException { + return new FileOutputStream(_file); + } + + /** + * This method returns the MIME type of the data in the form of a + * string. This method uses the currently installed FileTypeMap. If + * there is no FileTypeMap explictly set, the FileDataSource will + * call the <code>getDefaultFileTypeMap</code> method on + * FileTypeMap to acquire a default FileTypeMap. <i>Note: By + * default, the FileTypeMap used will be a MimetypesFileTypeMap.</i> + * + * @return the MIME Type + * @see javax.activation.FileTypeMap#getDefaultFileTypeMap + */ + public String getContentType() { + // check to see if the type map is null? + if (typeMap == null) + return FileTypeMap.getDefaultFileTypeMap().getContentType(_file); + else + return typeMap.getContentType(_file); + } + + /** + * Return the <i>name</i> of this object. The FileDataSource + * will return the file name of the object. + * + * @return the name of the object. + * @see javax.activation.DataSource + */ + public String getName() { + return _file.getName(); + } + + /** + * Return the File object that corresponds to this FileDataSource. + * @return the File object for the file represented by this object. + */ + public File getFile() { + return _file; + } + + /** + * Set the FileTypeMap to use with this FileDataSource + * + * @param map The FileTypeMap for this object. + */ + public void setFileTypeMap(FileTypeMap map) { + typeMap = map; + } +}
diff --git a/android/activation/src/main/java/javax/activation/FileTypeMap.java b/android/activation/src/main/java/javax/activation/FileTypeMap.java new file mode 100644 index 0000000..683f998 --- /dev/null +++ b/android/activation/src/main/java/javax/activation/FileTypeMap.java
@@ -0,0 +1,115 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package javax.activation; + +import java.io.File; +import java.util.Map; +import java.util.WeakHashMap; + +/** + * The FileTypeMap is an abstract class that provides a data typing + * interface for files. Implementations of this class will + * implement the getContentType methods which will derive a content + * type from a file name or a File object. FileTypeMaps could use any + * scheme to determine the data type, from examining the file extension + * of a file (like the MimetypesFileTypeMap) to opening the file and + * trying to derive its type from the contents of the file. The + * FileDataSource class uses the default FileTypeMap (a MimetypesFileTypeMap + * unless changed) to determine the content type of files. + * + * @see javax.activation.FileTypeMap + * @see javax.activation.FileDataSource + * @see javax.activation.MimetypesFileTypeMap + */ + +public abstract class FileTypeMap { + + private static FileTypeMap defaultMap = null; + private static Map<ClassLoader,FileTypeMap> map = + new WeakHashMap<ClassLoader,FileTypeMap>(); + + /** + * The default constructor. + */ + public FileTypeMap() { + super(); + } + + /** + * Return the type of the file object. This method should + * always return a valid MIME type. + * + * @param file A file to be typed. + * @return The content type. + */ + abstract public String getContentType(File file); + + /** + * Return the type of the file passed in. This method should + * always return a valid MIME type. + * + * @param filename the pathname of the file. + * @return The content type. + */ + abstract public String getContentType(String filename); + + /** + * Sets the default FileTypeMap for the system. This instance + * will be returned to callers of getDefaultFileTypeMap. + * + * @param fileTypeMap The FileTypeMap. + * @exception SecurityException if the caller doesn't have permission + * to change the default + */ + public static synchronized void setDefaultFileTypeMap(FileTypeMap fileTypeMap) { + SecurityManager security = System.getSecurityManager(); + if (security != null) { + try { + // if it's ok with the SecurityManager, it's ok with me... + security.checkSetFactory(); + } catch (SecurityException ex) { + // otherwise, we also allow it if this code and the + // factory come from the same (non-system) class loader (e.g., + // the JAF classes were loaded with the applet classes). + ClassLoader cl = FileTypeMap.class.getClassLoader(); + if (cl == null || cl.getParent() == null || + cl != fileTypeMap.getClass().getClassLoader()) + throw ex; + } + } + // remove any per-thread-context-class-loader FileTypeMap + map.remove(SecuritySupport.getContextClassLoader()); + defaultMap = fileTypeMap; + } + + /** + * Return the default FileTypeMap for the system. + * If setDefaultFileTypeMap was called, return + * that instance, otherwise return an instance of + * <code>MimetypesFileTypeMap</code>. + * + * @return The default FileTypeMap + * @see javax.activation.FileTypeMap#setDefaultFileTypeMap + */ + public static synchronized FileTypeMap getDefaultFileTypeMap() { + if (defaultMap != null) + return defaultMap; + + // fetch per-thread-context-class-loader default + ClassLoader tccl = SecuritySupport.getContextClassLoader(); + FileTypeMap def = map.get(tccl); + if (def == null) { + def = new MimetypesFileTypeMap(); + map.put(tccl, def); + } + return def; + } +}
diff --git a/android/activation/src/main/java/javax/activation/MailcapCommandMap.java b/android/activation/src/main/java/javax/activation/MailcapCommandMap.java new file mode 100644 index 0000000..f5e0672 --- /dev/null +++ b/android/activation/src/main/java/javax/activation/MailcapCommandMap.java
@@ -0,0 +1,707 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package javax.activation; + +import java.util.*; +import java.io.*; +import java.net.*; +import java.security.AccessController; +import java.security.PrivilegedAction; +import com.sun.activation.registries.MailcapFile; +import com.sun.activation.registries.LogSupport; + +/** + * MailcapCommandMap extends the CommandMap + * abstract class. It implements a CommandMap whose configuration + * is based on mailcap files + * (<A HREF="http://www.ietf.org/rfc/rfc1524.txt">RFC 1524</A>). + * The MailcapCommandMap can be configured both programmatically + * and via configuration files. + * <p> + * <b>Mailcap file search order:</b><p> + * The MailcapCommandMap looks in various places in the user's + * system for mailcap file entries. When requests are made + * to search for commands in the MailcapCommandMap, it searches + * mailcap files in the following order: + * <ol> + * <li> Programatically added entries to the MailcapCommandMap instance. + * <li> The file <code>.mailcap</code> in the user's home directory. + * <li> The file <code>mailcap</code> in the Java runtime. + * <li> The file or resources named <code>META-INF/mailcap</code>. + * <li> The file or resource named <code>META-INF/mailcap.default</code> + * (usually found only in the <code>activation.jar</code> file). + * </ol> + * <p> + * (The current implementation looks for the <code>mailcap</code> file + * in the Java runtime in the directory <code><i>java.home</i>/conf</code> + * if it exists, and otherwise in the directory + * <code><i>java.home</i>/lib</code>, where <i>java.home</i> is the value + * of the "java.home" System property. Note that the "conf" directory was + * introduced in JDK 9.) + * <p> + * <b>Mailcap file format:</b><p> + * + * Mailcap files must conform to the mailcap + * file specification (RFC 1524, <i>A User Agent Configuration Mechanism + * For Multimedia Mail Format Information</i>). + * The file format consists of entries corresponding to + * particular MIME types. In general, the specification + * specifies <i>applications</i> for clients to use when they + * themselves cannot operate on the specified MIME type. The + * MailcapCommandMap extends this specification by using a parameter mechanism + * in mailcap files that allows JavaBeans(tm) components to be specified as + * corresponding to particular commands for a MIME type.<p> + * + * When a mailcap file is + * parsed, the MailcapCommandMap recognizes certain parameter signatures, + * specifically those parameter names that begin with <code>x-java-</code>. + * The MailcapCommandMap uses this signature to find + * command entries for inclusion into its registries. + * Parameter names with the form <code>x-java-<name></code> + * are read by the MailcapCommandMap as identifying a command + * with the name <i>name</i>. When the <i>name</i> is <code> + * content-handler</code> the MailcapCommandMap recognizes the class + * signified by this parameter as a <i>DataContentHandler</i>. + * All other commands are handled generically regardless of command + * name. The command implementation is specified by a fully qualified + * class name of a JavaBean(tm) component. For example; a command for viewing + * some data can be specified as: <code>x-java-view=com.foo.ViewBean</code>.<p> + * + * When the command name is <code>fallback-entry</code>, the value of + * the command may be <code>true</code> or <code>false</code>. An + * entry for a MIME type that includes a parameter of + * <code>x-java-fallback-entry=true</code> defines fallback commands + * for that MIME type that will only be used if no non-fallback entry + * can be found. For example, an entry of the form <code>text/*; ; + * x-java-fallback-entry=true; x-java-view=com.sun.TextViewer</code> + * specifies a view command to be used for any text MIME type. This + * view command would only be used if a non-fallback view command for + * the MIME type could not be found.<p> + * + * MailcapCommandMap aware mailcap files have the + * following general form:<p> + * <code> + * # Comments begin with a '#' and continue to the end of the line.<br> + * <mime type>; ; <parameter list><br> + * # Where a parameter list consists of one or more parameters,<br> + * # where parameters look like: x-java-view=com.sun.TextViewer<br> + * # and a parameter list looks like: <br> + * text/plain; ; x-java-view=com.sun.TextViewer; x-java-edit=com.sun.TextEdit + * <br> + * # Note that mailcap entries that do not contain 'x-java' parameters<br> + * # and comply to RFC 1524 are simply ignored:<br> + * image/gif; /usr/dt/bin/sdtimage %s<br> + * + * </code> + * <p> + * + * @author Bart Calder + * @author Bill Shannon + */ + +public class MailcapCommandMap extends CommandMap { + /* + * We manage a collection of databases, searched in order. + */ + private MailcapFile[] DB; + private static final int PROG = 0; // programmatically added entries + + private static final String confDir; + + static { + String dir = null; + try { + dir = (String)AccessController.doPrivileged( + new PrivilegedAction() { + public Object run() { + String home = System.getProperty("java.home"); + String newdir = home + File.separator + "conf"; + File conf = new File(newdir); + if (conf.exists()) + return newdir + File.separator; + else + return home + File.separator + "lib" + File.separator; + } + }); + } catch (Exception ex) { + // ignore any exceptions + } + confDir = dir; + } + + /** + * The default Constructor. + */ + public MailcapCommandMap() { + super(); + List dbv = new ArrayList(5); // usually 5 or less databases + MailcapFile mf = null; + dbv.add(null); // place holder for PROG entry + + LogSupport.log("MailcapCommandMap: load HOME"); + try { + String user_home = System.getProperty("user.home"); + + if (user_home != null) { + String path = user_home + File.separator + ".mailcap"; + mf = loadFile(path); + if (mf != null) + dbv.add(mf); + } + } catch (SecurityException ex) {} + + LogSupport.log("MailcapCommandMap: load SYS"); + try { + // check system's home + if (confDir != null) { + mf = loadFile(confDir + "mailcap"); + if (mf != null) + dbv.add(mf); + } + } catch (SecurityException ex) {} + + LogSupport.log("MailcapCommandMap: load JAR"); + // load from the app's jar file + loadAllResources(dbv, "META-INF/mailcap"); + + LogSupport.log("MailcapCommandMap: load DEF"); + mf = loadResource("/META-INF/mailcap.default"); + + if (mf != null) + dbv.add(mf); + + DB = new MailcapFile[dbv.size()]; + DB = (MailcapFile[])dbv.toArray(DB); + } + + /** + * Load from the named resource. + */ + private MailcapFile loadResource(String name) { + InputStream clis = null; + try { + clis = SecuritySupport.getResourceAsStream(this.getClass(), name); + if (clis != null) { + MailcapFile mf = new MailcapFile(clis); + if (LogSupport.isLoggable()) + LogSupport.log("MailcapCommandMap: successfully loaded " + + "mailcap file: " + name); + return mf; + } else { + if (LogSupport.isLoggable()) + LogSupport.log("MailcapCommandMap: not loading " + + "mailcap file: " + name); + } + } catch (IOException e) { + if (LogSupport.isLoggable()) + LogSupport.log("MailcapCommandMap: can't load " + name, e); + } catch (SecurityException sex) { + if (LogSupport.isLoggable()) + LogSupport.log("MailcapCommandMap: can't load " + name, sex); + } finally { + try { + if (clis != null) + clis.close(); + } catch (IOException ex) { } // ignore it + } + return null; + } + + /** + * Load all of the named resource. + */ + private void loadAllResources(List v, String name) { + boolean anyLoaded = false; + try { + URL[] urls; + ClassLoader cld = null; + // First try the "application's" class loader. + cld = SecuritySupport.getContextClassLoader(); + if (cld == null) + cld = this.getClass().getClassLoader(); + if (cld != null) + urls = SecuritySupport.getResources(cld, name); + else + urls = SecuritySupport.getSystemResources(name); + if (urls != null) { + if (LogSupport.isLoggable()) + LogSupport.log("MailcapCommandMap: getResources"); + for (int i = 0; i < urls.length; i++) { + URL url = urls[i]; + InputStream clis = null; + if (LogSupport.isLoggable()) + LogSupport.log("MailcapCommandMap: URL " + url); + try { + clis = SecuritySupport.openStream(url); + if (clis != null) { + v.add(new MailcapFile(clis)); + anyLoaded = true; + if (LogSupport.isLoggable()) + LogSupport.log("MailcapCommandMap: " + + "successfully loaded " + + "mailcap file from URL: " + + url); + } else { + if (LogSupport.isLoggable()) + LogSupport.log("MailcapCommandMap: " + + "not loading mailcap " + + "file from URL: " + url); + } + } catch (IOException ioex) { + if (LogSupport.isLoggable()) + LogSupport.log("MailcapCommandMap: can't load " + + url, ioex); + } catch (SecurityException sex) { + if (LogSupport.isLoggable()) + LogSupport.log("MailcapCommandMap: can't load " + + url, sex); + } finally { + try { + if (clis != null) + clis.close(); + } catch (IOException cex) { } + } + } + } + } catch (Exception ex) { + if (LogSupport.isLoggable()) + LogSupport.log("MailcapCommandMap: can't load " + name, ex); + } + + // if failed to load anything, fall back to old technique, just in case + if (!anyLoaded) { + if (LogSupport.isLoggable()) + LogSupport.log("MailcapCommandMap: !anyLoaded"); + MailcapFile mf = loadResource("/" + name); + if (mf != null) + v.add(mf); + } + } + + /** + * Load from the named file. + */ + private MailcapFile loadFile(String name) { + MailcapFile mtf = null; + + try { + mtf = new MailcapFile(name); + } catch (IOException e) { + // e.printStackTrace(); + } + return mtf; + } + + /** + * Constructor that allows the caller to specify the path + * of a <i>mailcap</i> file. + * + * @param fileName The name of the <i>mailcap</i> file to open + * @exception IOException if the file can't be accessed + */ + public MailcapCommandMap(String fileName) throws IOException { + this(); + + if (LogSupport.isLoggable()) + LogSupport.log("MailcapCommandMap: load PROG from " + fileName); + if (DB[PROG] == null) { + DB[PROG] = new MailcapFile(fileName); + } + } + + + /** + * Constructor that allows the caller to specify an <i>InputStream</i> + * containing a mailcap file. + * + * @param is InputStream of the <i>mailcap</i> file to open + */ + public MailcapCommandMap(InputStream is) { + this(); + + LogSupport.log("MailcapCommandMap: load PROG"); + if (DB[PROG] == null) { + try { + DB[PROG] = new MailcapFile(is); + } catch (IOException ex) { + // XXX - should throw it + } + } + } + + /** + * Get the preferred command list for a MIME Type. The MailcapCommandMap + * searches the mailcap files as described above under + * <i>Mailcap file search order</i>.<p> + * + * The result of the search is a proper subset of available + * commands in all mailcap files known to this instance of + * MailcapCommandMap. The first entry for a particular command + * is considered the preferred command. + * + * @param mimeType the MIME type + * @return the CommandInfo objects representing the preferred commands. + */ + public synchronized CommandInfo[] getPreferredCommands(String mimeType) { + List cmdList = new ArrayList(); + if (mimeType != null) + mimeType = mimeType.toLowerCase(Locale.ENGLISH); + + for (int i = 0; i < DB.length; i++) { + if (DB[i] == null) + continue; + Map cmdMap = DB[i].getMailcapList(mimeType); + if (cmdMap != null) + appendPrefCmdsToList(cmdMap, cmdList); + } + + // now add the fallback commands + for (int i = 0; i < DB.length; i++) { + if (DB[i] == null) + continue; + Map cmdMap = DB[i].getMailcapFallbackList(mimeType); + if (cmdMap != null) + appendPrefCmdsToList(cmdMap, cmdList); + } + + CommandInfo[] cmdInfos = new CommandInfo[cmdList.size()]; + cmdInfos = (CommandInfo[])cmdList.toArray(cmdInfos); + + return cmdInfos; + } + + /** + * Put the commands that are in the hash table, into the list. + */ + private void appendPrefCmdsToList(Map cmdHash, List cmdList) { + Iterator verb_enum = cmdHash.keySet().iterator(); + + while (verb_enum.hasNext()) { + String verb = (String)verb_enum.next(); + if (!checkForVerb(cmdList, verb)) { + List cmdList2 = (List)cmdHash.get(verb); // get the list + String className = (String)cmdList2.get(0); + cmdList.add(new CommandInfo(verb, className)); + } + } + } + + /** + * Check the cmdList to see if this command exists, return + * true if the verb is there. + */ + private boolean checkForVerb(List cmdList, String verb) { + Iterator ee = cmdList.iterator(); + while (ee.hasNext()) { + String enum_verb = + (String)((CommandInfo)ee.next()).getCommandName(); + if (enum_verb.equals(verb)) + return true; + } + return false; + } + + /** + * Get all the available commands in all mailcap files known to + * this instance of MailcapCommandMap for this MIME type. + * + * @param mimeType the MIME type + * @return the CommandInfo objects representing all the commands. + */ + public synchronized CommandInfo[] getAllCommands(String mimeType) { + List cmdList = new ArrayList(); + if (mimeType != null) + mimeType = mimeType.toLowerCase(Locale.ENGLISH); + + for (int i = 0; i < DB.length; i++) { + if (DB[i] == null) + continue; + Map cmdMap = DB[i].getMailcapList(mimeType); + if (cmdMap != null) + appendCmdsToList(cmdMap, cmdList); + } + + // now add the fallback commands + for (int i = 0; i < DB.length; i++) { + if (DB[i] == null) + continue; + Map cmdMap = DB[i].getMailcapFallbackList(mimeType); + if (cmdMap != null) + appendCmdsToList(cmdMap, cmdList); + } + + CommandInfo[] cmdInfos = new CommandInfo[cmdList.size()]; + cmdInfos = (CommandInfo[])cmdList.toArray(cmdInfos); + + return cmdInfos; + } + + /** + * Put the commands that are in the hash table, into the list. + */ + private void appendCmdsToList(Map typeHash, List cmdList) { + Iterator verb_enum = typeHash.keySet().iterator(); + + while (verb_enum.hasNext()) { + String verb = (String)verb_enum.next(); + List cmdList2 = (List)typeHash.get(verb); + Iterator cmd_enum = ((List)cmdList2).iterator(); + + while (cmd_enum.hasNext()) { + String cmd = (String)cmd_enum.next(); + cmdList.add(new CommandInfo(verb, cmd)); + // cmdList.add(0, new CommandInfo(verb, cmd)); + } + } + } + + /** + * Get the command corresponding to <code>cmdName</code> for the MIME type. + * + * @param mimeType the MIME type + * @param cmdName the command name + * @return the CommandInfo object corresponding to the command. + */ + public synchronized CommandInfo getCommand(String mimeType, + String cmdName) { + if (mimeType != null) + mimeType = mimeType.toLowerCase(Locale.ENGLISH); + + for (int i = 0; i < DB.length; i++) { + if (DB[i] == null) + continue; + Map cmdMap = DB[i].getMailcapList(mimeType); + if (cmdMap != null) { + // get the cmd list for the cmd + List v = (List)cmdMap.get(cmdName); + if (v != null) { + String cmdClassName = (String)v.get(0); + + if (cmdClassName != null) + return new CommandInfo(cmdName, cmdClassName); + } + } + } + + // now try the fallback list + for (int i = 0; i < DB.length; i++) { + if (DB[i] == null) + continue; + Map cmdMap = DB[i].getMailcapFallbackList(mimeType); + if (cmdMap != null) { + // get the cmd list for the cmd + List v = (List)cmdMap.get(cmdName); + if (v != null) { + String cmdClassName = (String)v.get(0); + + if (cmdClassName != null) + return new CommandInfo(cmdName, cmdClassName); + } + } + } + return null; + } + + /** + * Add entries to the registry. Programmatically + * added entries are searched before other entries.<p> + * + * The string that is passed in should be in mailcap + * format. + * + * @param mail_cap a correctly formatted mailcap string + */ + public synchronized void addMailcap(String mail_cap) { + // check to see if one exists + LogSupport.log("MailcapCommandMap: add to PROG"); + if (DB[PROG] == null) + DB[PROG] = new MailcapFile(); + + DB[PROG].appendToMailcap(mail_cap); + } + + /** + * Return the DataContentHandler for the specified MIME type. + * + * @param mimeType the MIME type + * @return the DataContentHandler + */ + public synchronized DataContentHandler createDataContentHandler( + String mimeType) { + if (LogSupport.isLoggable()) + LogSupport.log( + "MailcapCommandMap: createDataContentHandler for " + mimeType); + if (mimeType != null) + mimeType = mimeType.toLowerCase(Locale.ENGLISH); + + for (int i = 0; i < DB.length; i++) { + if (DB[i] == null) + continue; + if (LogSupport.isLoggable()) + LogSupport.log(" search DB #" + i); + Map cmdMap = DB[i].getMailcapList(mimeType); + if (cmdMap != null) { + List v = (List)cmdMap.get("content-handler"); + if (v != null) { + String name = (String)v.get(0); + DataContentHandler dch = getDataContentHandler(name); + if (dch != null) + return dch; + } + } + } + + // now try the fallback entries + for (int i = 0; i < DB.length; i++) { + if (DB[i] == null) + continue; + if (LogSupport.isLoggable()) + LogSupport.log(" search fallback DB #" + i); + Map cmdMap = DB[i].getMailcapFallbackList(mimeType); + if (cmdMap != null) { + List v = (List)cmdMap.get("content-handler"); + if (v != null) { + String name = (String)v.get(0); + DataContentHandler dch = getDataContentHandler(name); + if (dch != null) + return dch; + } + } + } + return null; + } + + private DataContentHandler getDataContentHandler(String name) { + if (LogSupport.isLoggable()) + LogSupport.log(" got content-handler"); + if (LogSupport.isLoggable()) + LogSupport.log(" class " + name); + try { + ClassLoader cld = null; + // First try the "application's" class loader. + cld = SecuritySupport.getContextClassLoader(); + if (cld == null) + cld = this.getClass().getClassLoader(); + Class cl = null; + try { + cl = cld.loadClass(name); + } catch (Exception ex) { + // if anything goes wrong, do it the old way + cl = Class.forName(name); + } + if (cl != null) // XXX - always true? + return (DataContentHandler)cl.newInstance(); + } catch (IllegalAccessException e) { + if (LogSupport.isLoggable()) + LogSupport.log("Can't load DCH " + name, e); + } catch (ClassNotFoundException e) { + if (LogSupport.isLoggable()) + LogSupport.log("Can't load DCH " + name, e); + } catch (InstantiationException e) { + if (LogSupport.isLoggable()) + LogSupport.log("Can't load DCH " + name, e); + } + return null; + } + + /** + * Get all the MIME types known to this command map. + * + * @return array of MIME types as strings + * @since JAF 1.1 + */ + public synchronized String[] getMimeTypes() { + List mtList = new ArrayList(); + + for (int i = 0; i < DB.length; i++) { + if (DB[i] == null) + continue; + String[] ts = DB[i].getMimeTypes(); + if (ts != null) { + for (int j = 0; j < ts.length; j++) { + // eliminate duplicates + if (!mtList.contains(ts[j])) + mtList.add(ts[j]); + } + } + } + + String[] mts = new String[mtList.size()]; + mts = (String[])mtList.toArray(mts); + + return mts; + } + + /** + * Get the native commands for the given MIME type. + * Returns an array of strings where each string is + * an entire mailcap file entry. The application + * will need to parse the entry to extract the actual + * command as well as any attributes it needs. See + * <A HREF="http://www.ietf.org/rfc/rfc1524.txt">RFC 1524</A> + * for details of the mailcap entry syntax. Only mailcap + * entries that specify a view command for the specified + * MIME type are returned. + * + * @param mimeType the MIME type + * @return array of native command entries + * @since JAF 1.1 + */ + public synchronized String[] getNativeCommands(String mimeType) { + List cmdList = new ArrayList(); + if (mimeType != null) + mimeType = mimeType.toLowerCase(Locale.ENGLISH); + + for (int i = 0; i < DB.length; i++) { + if (DB[i] == null) + continue; + String[] cmds = DB[i].getNativeCommands(mimeType); + if (cmds != null) { + for (int j = 0; j < cmds.length; j++) { + // eliminate duplicates + if (!cmdList.contains(cmds[j])) + cmdList.add(cmds[j]); + } + } + } + + String[] cmds = new String[cmdList.size()]; + cmds = (String[])cmdList.toArray(cmds); + + return cmds; + } + + /** + * for debugging... + * + public static void main(String[] argv) throws Exception { + MailcapCommandMap map = new MailcapCommandMap(); + CommandInfo[] cmdInfo; + + cmdInfo = map.getPreferredCommands(argv[0]); + System.out.println("Preferred Commands:"); + for (int i = 0; i < cmdInfo.length; i++) + System.out.println("Command " + cmdInfo[i].getCommandName() + " [" + + cmdInfo[i].getCommandClass() + "]"); + cmdInfo = map.getAllCommands(argv[0]); + System.out.println(); + System.out.println("All Commands:"); + for (int i = 0; i < cmdInfo.length; i++) + System.out.println("Command " + cmdInfo[i].getCommandName() + " [" + + cmdInfo[i].getCommandClass() + "]"); + DataContentHandler dch = map.createDataContentHandler(argv[0]); + if (dch != null) + System.out.println("DataContentHandler " + + dch.getClass().toString()); + System.exit(0); + } + */ +}
diff --git a/android/activation/src/main/java/javax/activation/MimeType.java b/android/activation/src/main/java/javax/activation/MimeType.java new file mode 100644 index 0000000..49fe6e5 --- /dev/null +++ b/android/activation/src/main/java/javax/activation/MimeType.java
@@ -0,0 +1,329 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package javax.activation; + +import java.io.*; +import java.util.Locale; + +/** + * A Multipurpose Internet Mail Extension (MIME) type, as defined + * in RFC 2045 and 2046. + */ +public class MimeType implements Externalizable { + + private String primaryType; + private String subType; + private MimeTypeParameterList parameters; + + /** + * A string that holds all the special chars. + */ + private static final String TSPECIALS = "()<>@,;:/[]?=\\\""; + + /** + * Default constructor. + */ + public MimeType() { + primaryType = "application"; + subType = "*"; + parameters = new MimeTypeParameterList(); + } + + /** + * Constructor that builds a MimeType from a String. + * + * @param rawdata the MIME type string + * @exception MimeTypeParseException if the MIME type can't be parsed + */ + public MimeType(String rawdata) throws MimeTypeParseException { + parse(rawdata); + } + + /** + * Constructor that builds a MimeType with the given primary and sub type + * but has an empty parameter list. + * + * @param primary the primary MIME type + * @param sub the MIME sub-type + * @exception MimeTypeParseException if the primary type or subtype + * is not a valid token + */ + public MimeType(String primary, String sub) throws MimeTypeParseException { + // check to see if primary is valid + if (isValidToken(primary)) { + primaryType = primary.toLowerCase(Locale.ENGLISH); + } else { + throw new MimeTypeParseException("Primary type is invalid."); + } + + // check to see if sub is valid + if (isValidToken(sub)) { + subType = sub.toLowerCase(Locale.ENGLISH); + } else { + throw new MimeTypeParseException("Sub type is invalid."); + } + + parameters = new MimeTypeParameterList(); + } + + /** + * A routine for parsing the MIME type out of a String. + */ + private void parse(String rawdata) throws MimeTypeParseException { + int slashIndex = rawdata.indexOf('/'); + int semIndex = rawdata.indexOf(';'); + if ((slashIndex < 0) && (semIndex < 0)) { + // neither character is present, so treat it + // as an error + throw new MimeTypeParseException("Unable to find a sub type."); + } else if ((slashIndex < 0) && (semIndex >= 0)) { + // we have a ';' (and therefore a parameter list), + // but no '/' indicating a sub type is present + throw new MimeTypeParseException("Unable to find a sub type."); + } else if ((slashIndex >= 0) && (semIndex < 0)) { + // we have a primary and sub type but no parameter list + primaryType = rawdata.substring(0, slashIndex).trim(). + toLowerCase(Locale.ENGLISH); + subType = rawdata.substring(slashIndex + 1).trim(). + toLowerCase(Locale.ENGLISH); + parameters = new MimeTypeParameterList(); + } else if (slashIndex < semIndex) { + // we have all three items in the proper sequence + primaryType = rawdata.substring(0, slashIndex).trim(). + toLowerCase(Locale.ENGLISH); + subType = rawdata.substring(slashIndex + 1, semIndex).trim(). + toLowerCase(Locale.ENGLISH); + parameters = new MimeTypeParameterList(rawdata.substring(semIndex)); + } else { + // we have a ';' lexically before a '/' which means we + // have a primary type and a parameter list but no sub type + throw new MimeTypeParseException("Unable to find a sub type."); + } + + // now validate the primary and sub types + + // check to see if primary is valid + if (!isValidToken(primaryType)) + throw new MimeTypeParseException("Primary type is invalid."); + + // check to see if sub is valid + if (!isValidToken(subType)) + throw new MimeTypeParseException("Sub type is invalid."); + } + + /** + * Retrieve the primary type of this object. + * + * @return the primary MIME type + */ + public String getPrimaryType() { + return primaryType; + } + + /** + * Set the primary type for this object to the given String. + * + * @param primary the primary MIME type + * @exception MimeTypeParseException if the primary type + * is not a valid token + */ + public void setPrimaryType(String primary) throws MimeTypeParseException { + // check to see if primary is valid + if (!isValidToken(primaryType)) + throw new MimeTypeParseException("Primary type is invalid."); + primaryType = primary.toLowerCase(Locale.ENGLISH); + } + + /** + * Retrieve the subtype of this object. + * + * @return the MIME subtype + */ + public String getSubType() { + return subType; + } + + /** + * Set the subtype for this object to the given String. + * + * @param sub the MIME subtype + * @exception MimeTypeParseException if the subtype + * is not a valid token + */ + public void setSubType(String sub) throws MimeTypeParseException { + // check to see if sub is valid + if (!isValidToken(subType)) + throw new MimeTypeParseException("Sub type is invalid."); + subType = sub.toLowerCase(Locale.ENGLISH); + } + + /** + * Retrieve this object's parameter list. + * + * @return a MimeTypeParameterList object representing the parameters + */ + public MimeTypeParameterList getParameters() { + return parameters; + } + + /** + * Retrieve the value associated with the given name, or null if there + * is no current association. + * + * @param name the parameter name + * @return the paramter's value + */ + public String getParameter(String name) { + return parameters.get(name); + } + + /** + * Set the value to be associated with the given name, replacing + * any previous association. + * + * @param name the parameter name + * @param value the paramter's value + */ + public void setParameter(String name, String value) { + parameters.set(name, value); + } + + /** + * Remove any value associated with the given name. + * + * @param name the parameter name + */ + public void removeParameter(String name) { + parameters.remove(name); + } + + /** + * Return the String representation of this object. + */ + public String toString() { + return getBaseType() + parameters.toString(); + } + + /** + * Return a String representation of this object + * without the parameter list. + * + * @return the MIME type and sub-type + */ + public String getBaseType() { + return primaryType + "/" + subType; + } + + /** + * Determine if the primary and sub type of this object is + * the same as what is in the given type. + * + * @param type the MimeType object to compare with + * @return true if they match + */ + public boolean match(MimeType type) { + return primaryType.equals(type.getPrimaryType()) + && (subType.equals("*") + || type.getSubType().equals("*") + || (subType.equals(type.getSubType()))); + } + + /** + * Determine if the primary and sub type of this object is + * the same as the content type described in rawdata. + * + * @param rawdata the MIME type string to compare with + * @return true if they match + * @exception MimeTypeParseException if the MIME type can't be parsed + */ + public boolean match(String rawdata) throws MimeTypeParseException { + return match(new MimeType(rawdata)); + } + + /** + * The object implements the writeExternal method to save its contents + * by calling the methods of DataOutput for its primitive values or + * calling the writeObject method of ObjectOutput for objects, strings + * and arrays. + * + * @param out the ObjectOutput object to write to + * @exception IOException Includes any I/O exceptions that may occur + */ + public void writeExternal(ObjectOutput out) throws IOException { + out.writeUTF(toString()); + out.flush(); + } + + /** + * The object implements the readExternal method to restore its + * contents by calling the methods of DataInput for primitive + * types and readObject for objects, strings and arrays. The + * readExternal method must read the values in the same sequence + * and with the same types as were written by writeExternal. + * + * @param in the ObjectInput object to read from + * @exception ClassNotFoundException If the class for an object being + * restored cannot be found. + */ + public void readExternal(ObjectInput in) + throws IOException, ClassNotFoundException { + try { + parse(in.readUTF()); + } catch (MimeTypeParseException e) { + throw new IOException(e.toString()); + } + } + + // below here be scary parsing related things + + /** + * Determine whether or not a given character belongs to a legal token. + */ + private static boolean isTokenChar(char c) { + return ((c > 040) && (c < 0177)) && (TSPECIALS.indexOf(c) < 0); + } + + /** + * Determine whether or not a given string is a legal token. + */ + private boolean isValidToken(String s) { + int len = s.length(); + if (len > 0) { + for (int i = 0; i < len; ++i) { + char c = s.charAt(i); + if (!isTokenChar(c)) { + return false; + } + } + return true; + } else { + return false; + } + } + + /** + * A simple parser test, + * for debugging... + * + public static void main(String[] args) + throws MimeTypeParseException, IOException { + for (int i = 0; i < args.length; ++i) { + System.out.println("Original: " + args[i]); + + MimeType type = new MimeType(args[i]); + + System.out.println("Short: " + type.getBaseType()); + System.out.println("Parsed: " + type.toString()); + System.out.println(); + } + } + */ +}
diff --git a/android/activation/src/main/java/javax/activation/MimeTypeParameterList.java b/android/activation/src/main/java/javax/activation/MimeTypeParameterList.java new file mode 100644 index 0000000..6298063 --- /dev/null +++ b/android/activation/src/main/java/javax/activation/MimeTypeParameterList.java
@@ -0,0 +1,324 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package javax.activation; + +import java.util.Hashtable; +import java.util.Enumeration; +import java.util.Locale; + +/** + * A parameter list of a MimeType + * as defined in RFC 2045 and 2046. The Primary type of the + * object must already be stripped off. + * + * @see javax.activation.MimeType + */ +public class MimeTypeParameterList { + private Hashtable parameters; + + /** + * A string that holds all the special chars. + */ + private static final String TSPECIALS = "()<>@,;:/[]?=\\\""; + + + /** + * Default constructor. + */ + public MimeTypeParameterList() { + parameters = new Hashtable(); + } + + /** + * Constructs a new MimeTypeParameterList with the passed in data. + * + * @param parameterList an RFC 2045, 2046 compliant parameter list. + * @exception MimeTypeParseException if the MIME type can't be parsed + */ + public MimeTypeParameterList(String parameterList) + throws MimeTypeParseException { + parameters = new Hashtable(); + + // now parse rawdata + parse(parameterList); + } + + /** + * A routine for parsing the parameter list out of a String. + * + * @param parameterList an RFC 2045, 2046 compliant parameter list. + * @exception MimeTypeParseException if the MIME type can't be parsed + */ + protected void parse(String parameterList) throws MimeTypeParseException { + if (parameterList == null) + return; + + int length = parameterList.length(); + if (length <= 0) + return; + + int i; + char c; + for (i = skipWhiteSpace(parameterList, 0); + i < length && (c = parameterList.charAt(i)) == ';'; + i = skipWhiteSpace(parameterList, i)) { + int lastIndex; + String name; + String value; + + // eat the ';' + i++; + + // now parse the parameter name + + // skip whitespace + i = skipWhiteSpace(parameterList, i); + + // tolerate trailing semicolon, even though it violates the spec + if (i >= length) + return; + + // find the end of the token char run + lastIndex = i; + while ((i < length) && isTokenChar(parameterList.charAt(i))) + i++; + + name = parameterList.substring(lastIndex, i). + toLowerCase(Locale.ENGLISH); + + // now parse the '=' that separates the name from the value + i = skipWhiteSpace(parameterList, i); + + if (i >= length || parameterList.charAt(i) != '=') + throw new MimeTypeParseException( + "Couldn't find the '=' that separates a " + + "parameter name from its value."); + + // eat it and parse the parameter value + i++; + i = skipWhiteSpace(parameterList, i); + + if (i >= length) + throw new MimeTypeParseException( + "Couldn't find a value for parameter named " + name); + + // now find out whether or not we have a quoted value + c = parameterList.charAt(i); + if (c == '"') { + // yup it's quoted so eat it and capture the quoted string + i++; + if (i >= length) + throw new MimeTypeParseException( + "Encountered unterminated quoted parameter value."); + + lastIndex = i; + + // find the next unescaped quote + while (i < length) { + c = parameterList.charAt(i); + if (c == '"') + break; + if (c == '\\') { + // found an escape sequence + // so skip this and the + // next character + i++; + } + i++; + } + if (c != '"') + throw new MimeTypeParseException( + "Encountered unterminated quoted parameter value."); + + value = unquote(parameterList.substring(lastIndex, i)); + // eat the quote + i++; + } else if (isTokenChar(c)) { + // nope it's an ordinary token so it + // ends with a non-token char + lastIndex = i; + while (i < length && isTokenChar(parameterList.charAt(i))) + i++; + value = parameterList.substring(lastIndex, i); + } else { + // it ain't a value + throw new MimeTypeParseException( + "Unexpected character encountered at index " + i); + } + + // now put the data into the hashtable + parameters.put(name, value); + } + if (i < length) { + throw new MimeTypeParseException( + "More characters encountered in input than expected."); + } + } + + /** + * Return the number of name-value pairs in this list. + * + * @return the number of parameters + */ + public int size() { + return parameters.size(); + } + + /** + * Determine whether or not this list is empty. + * + * @return true if there are no parameters + */ + public boolean isEmpty() { + return parameters.isEmpty(); + } + + /** + * Retrieve the value associated with the given name, or null if there + * is no current association. + * + * @param name the parameter name + * @return the parameter's value + */ + public String get(String name) { + return (String)parameters.get(name.trim().toLowerCase(Locale.ENGLISH)); + } + + /** + * Set the value to be associated with the given name, replacing + * any previous association. + * + * @param name the parameter name + * @param value the parameter's value + */ + public void set(String name, String value) { + parameters.put(name.trim().toLowerCase(Locale.ENGLISH), value); + } + + /** + * Remove any value associated with the given name. + * + * @param name the parameter name + */ + public void remove(String name) { + parameters.remove(name.trim().toLowerCase(Locale.ENGLISH)); + } + + /** + * Retrieve an enumeration of all the names in this list. + * + * @return an enumeration of all parameter names + */ + public Enumeration getNames() { + return parameters.keys(); + } + + /** + * Return a string representation of this object. + */ + public String toString() { + StringBuffer buffer = new StringBuffer(); + buffer.ensureCapacity(parameters.size() * 16); + // heuristic: 8 characters per field + + Enumeration keys = parameters.keys(); + while (keys.hasMoreElements()) { + String key = (String)keys.nextElement(); + buffer.append("; "); + buffer.append(key); + buffer.append('='); + buffer.append(quote((String)parameters.get(key))); + } + + return buffer.toString(); + } + + // below here be scary parsing related things + + /** + * Determine whether or not a given character belongs to a legal token. + */ + private static boolean isTokenChar(char c) { + return ((c > 040) && (c < 0177)) && (TSPECIALS.indexOf(c) < 0); + } + + /** + * return the index of the first non white space character in + * rawdata at or after index i. + */ + private static int skipWhiteSpace(String rawdata, int i) { + int length = rawdata.length(); + while ((i < length) && Character.isWhitespace(rawdata.charAt(i))) + i++; + return i; + } + + /** + * A routine that knows how and when to quote and escape the given value. + */ + private static String quote(String value) { + boolean needsQuotes = false; + + // check to see if we actually have to quote this thing + int length = value.length(); + for (int i = 0; (i < length) && !needsQuotes; i++) { + needsQuotes = !isTokenChar(value.charAt(i)); + } + + if (needsQuotes) { + StringBuffer buffer = new StringBuffer(); + buffer.ensureCapacity((int)(length * 1.5)); + + // add the initial quote + buffer.append('"'); + + // add the properly escaped text + for (int i = 0; i < length; ++i) { + char c = value.charAt(i); + if ((c == '\\') || (c == '"')) + buffer.append('\\'); + buffer.append(c); + } + + // add the closing quote + buffer.append('"'); + + return buffer.toString(); + } else { + return value; + } + } + + /** + * A routine that knows how to strip the quotes and + * escape sequences from the given value. + */ + private static String unquote(String value) { + int valueLength = value.length(); + StringBuffer buffer = new StringBuffer(); + buffer.ensureCapacity(valueLength); + + boolean escaped = false; + for (int i = 0; i < valueLength; ++i) { + char currentChar = value.charAt(i); + if (!escaped && (currentChar != '\\')) { + buffer.append(currentChar); + } else if (escaped) { + buffer.append(currentChar); + escaped = false; + } else { + escaped = true; + } + } + + return buffer.toString(); + } +}
diff --git a/android/activation/src/main/java/javax/activation/MimeTypeParseException.java b/android/activation/src/main/java/javax/activation/MimeTypeParseException.java new file mode 100644 index 0000000..fd000e2 --- /dev/null +++ b/android/activation/src/main/java/javax/activation/MimeTypeParseException.java
@@ -0,0 +1,33 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package javax.activation; + +/** + * A class to encapsulate MimeType parsing related exceptions. + */ +public class MimeTypeParseException extends Exception { + + /** + * Constructs a MimeTypeParseException with no specified detail message. + */ + public MimeTypeParseException() { + super(); + } + + /** + * Constructs a MimeTypeParseException with the specified detail message. + * + * @param s the detail message. + */ + public MimeTypeParseException(String s) { + super(s); + } +}
diff --git a/android/activation/src/main/java/javax/activation/MimetypesFileTypeMap.java b/android/activation/src/main/java/javax/activation/MimetypesFileTypeMap.java new file mode 100644 index 0000000..e2dffb7 --- /dev/null +++ b/android/activation/src/main/java/javax/activation/MimetypesFileTypeMap.java
@@ -0,0 +1,343 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package javax.activation; + +import java.io.*; +import java.net.*; +import java.util.*; +import java.security.AccessController; +import java.security.PrivilegedAction; +import com.sun.activation.registries.MimeTypeFile; +import com.sun.activation.registries.LogSupport; + +/** + * This class extends FileTypeMap and provides data typing of files + * via their file extension. It uses the <code>.mime.types</code> format. <p> + * + * <b>MIME types file search order:</b><p> + * The MimetypesFileTypeMap looks in various places in the user's + * system for MIME types file entries. When requests are made + * to search for MIME types in the MimetypesFileTypeMap, it searches + * MIME types files in the following order: + * <ol> + * <li> Programmatically added entries to the MimetypesFileTypeMap instance. + * <li> The file <code>.mime.types</code> in the user's home directory. + * <li> The file <code>mime.types</code> in the Java runtime. + * <li> The file or resources named <code>META-INF/mime.types</code>. + * <li> The file or resource named <code>META-INF/mimetypes.default</code> + * (usually found only in the <code>activation.jar</code> file). + * </ol> + * <p> + * (The current implementation looks for the <code>mime.types</code> file + * in the Java runtime in the directory <code><i>java.home</i>/conf</code> + * if it exists, and otherwise in the directory + * <code><i>java.home</i>/lib</code>, where <i>java.home</i> is the value + * of the "java.home" System property. Note that the "conf" directory was + * introduced in JDK 9.) + * <p> + * <b>MIME types file format:</b><p> + * + * <code> + * # comments begin with a '#'<br> + * # the format is <mime type> <space separated file extensions><br> + * # for example:<br> + * text/plain txt text TXT<br> + * # this would map file.txt, file.text, and file.TXT to<br> + * # the mime type "text/plain"<br> + * </code> + * + * @author Bart Calder + * @author Bill Shannon + */ +public class MimetypesFileTypeMap extends FileTypeMap { + /* + * We manage a collection of databases, searched in order. + */ + private MimeTypeFile[] DB; + private static final int PROG = 0; // programmatically added entries + + private static final String defaultType = "application/octet-stream"; + + private static final String confDir; + + static { + String dir = null; + try { + dir = (String)AccessController.doPrivileged( + new PrivilegedAction() { + public Object run() { + String home = System.getProperty("java.home"); + String newdir = home + File.separator + "conf"; + File conf = new File(newdir); + if (conf.exists()) + return newdir + File.separator; + else + return home + File.separator + "lib" + File.separator; + } + }); + } catch (Exception ex) { + // ignore any exceptions + } + confDir = dir; + } + + /** + * The default constructor. + */ + public MimetypesFileTypeMap() { + Vector dbv = new Vector(5); // usually 5 or less databases + MimeTypeFile mf = null; + dbv.addElement(null); // place holder for PROG entry + + LogSupport.log("MimetypesFileTypeMap: load HOME"); + try { + String user_home = System.getProperty("user.home"); + + if (user_home != null) { + String path = user_home + File.separator + ".mime.types"; + mf = loadFile(path); + if (mf != null) + dbv.addElement(mf); + } + } catch (SecurityException ex) {} + + LogSupport.log("MimetypesFileTypeMap: load SYS"); + try { + // check system's home + if (confDir != null) { + mf = loadFile(confDir + "mime.types"); + if (mf != null) + dbv.addElement(mf); + } + } catch (SecurityException ex) {} + + LogSupport.log("MimetypesFileTypeMap: load JAR"); + // load from the app's jar file + loadAllResources(dbv, "META-INF/mime.types"); + + LogSupport.log("MimetypesFileTypeMap: load DEF"); + mf = loadResource("/META-INF/mimetypes.default"); + + if (mf != null) + dbv.addElement(mf); + + DB = new MimeTypeFile[dbv.size()]; + dbv.copyInto(DB); + } + + /** + * Load from the named resource. + */ + private MimeTypeFile loadResource(String name) { + InputStream clis = null; + try { + clis = SecuritySupport.getResourceAsStream(this.getClass(), name); + if (clis != null) { + MimeTypeFile mf = new MimeTypeFile(clis); + if (LogSupport.isLoggable()) + LogSupport.log("MimetypesFileTypeMap: successfully " + + "loaded mime types file: " + name); + return mf; + } else { + if (LogSupport.isLoggable()) + LogSupport.log("MimetypesFileTypeMap: not loading " + + "mime types file: " + name); + } + } catch (IOException e) { + if (LogSupport.isLoggable()) + LogSupport.log("MimetypesFileTypeMap: can't load " + name, e); + } catch (SecurityException sex) { + if (LogSupport.isLoggable()) + LogSupport.log("MimetypesFileTypeMap: can't load " + name, sex); + } finally { + try { + if (clis != null) + clis.close(); + } catch (IOException ex) { } // ignore it + } + return null; + } + + /** + * Load all of the named resource. + */ + private void loadAllResources(Vector v, String name) { + boolean anyLoaded = false; + try { + URL[] urls; + ClassLoader cld = null; + // First try the "application's" class loader. + cld = SecuritySupport.getContextClassLoader(); + if (cld == null) + cld = this.getClass().getClassLoader(); + if (cld != null) + urls = SecuritySupport.getResources(cld, name); + else + urls = SecuritySupport.getSystemResources(name); + if (urls != null) { + if (LogSupport.isLoggable()) + LogSupport.log("MimetypesFileTypeMap: getResources"); + for (int i = 0; i < urls.length; i++) { + URL url = urls[i]; + InputStream clis = null; + if (LogSupport.isLoggable()) + LogSupport.log("MimetypesFileTypeMap: URL " + url); + try { + clis = SecuritySupport.openStream(url); + if (clis != null) { + v.addElement(new MimeTypeFile(clis)); + anyLoaded = true; + if (LogSupport.isLoggable()) + LogSupport.log("MimetypesFileTypeMap: " + + "successfully loaded " + + "mime types from URL: " + url); + } else { + if (LogSupport.isLoggable()) + LogSupport.log("MimetypesFileTypeMap: " + + "not loading " + + "mime types from URL: " + url); + } + } catch (IOException ioex) { + if (LogSupport.isLoggable()) + LogSupport.log("MimetypesFileTypeMap: can't load " + + url, ioex); + } catch (SecurityException sex) { + if (LogSupport.isLoggable()) + LogSupport.log("MimetypesFileTypeMap: can't load " + + url, sex); + } finally { + try { + if (clis != null) + clis.close(); + } catch (IOException cex) { } + } + } + } + } catch (Exception ex) { + if (LogSupport.isLoggable()) + LogSupport.log("MimetypesFileTypeMap: can't load " + name, ex); + } + + // if failed to load anything, fall back to old technique, just in case + if (!anyLoaded) { + LogSupport.log("MimetypesFileTypeMap: !anyLoaded"); + MimeTypeFile mf = loadResource("/" + name); + if (mf != null) + v.addElement(mf); + } + } + + /** + * Load the named file. + */ + private MimeTypeFile loadFile(String name) { + MimeTypeFile mtf = null; + + try { + mtf = new MimeTypeFile(name); + } catch (IOException e) { + // e.printStackTrace(); + } + return mtf; + } + + /** + * Construct a MimetypesFileTypeMap with programmatic entries + * added from the named file. + * + * @param mimeTypeFileName the file name + * @exception IOException for errors reading the file + */ + public MimetypesFileTypeMap(String mimeTypeFileName) throws IOException { + this(); + DB[PROG] = new MimeTypeFile(mimeTypeFileName); + } + + /** + * Construct a MimetypesFileTypeMap with programmatic entries + * added from the InputStream. + * + * @param is the input stream to read from + */ + public MimetypesFileTypeMap(InputStream is) { + this(); + try { + DB[PROG] = new MimeTypeFile(is); + } catch (IOException ex) { + // XXX - really should throw it + } + } + + /** + * Prepend the MIME type values to the registry. + * + * @param mime_types A .mime.types formatted string of entries. + */ + public synchronized void addMimeTypes(String mime_types) { + // check to see if we have created the registry + if (DB[PROG] == null) + DB[PROG] = new MimeTypeFile(); // make one + + DB[PROG].appendToRegistry(mime_types); + } + + /** + * Return the MIME type of the file object. + * The implementation in this class calls + * <code>getContentType(f.getName())</code>. + * + * @param f the file + * @return the file's MIME type + */ + public String getContentType(File f) { + return this.getContentType(f.getName()); + } + + /** + * Return the MIME type based on the specified file name. + * The MIME type entries are searched as described above under + * <i>MIME types file search order</i>. + * If no entry is found, the type "application/octet-stream" is returned. + * + * @param filename the file name + * @return the file's MIME type + */ + public synchronized String getContentType(String filename) { + int dot_pos = filename.lastIndexOf("."); // period index + + if (dot_pos < 0) + return defaultType; + + String file_ext = filename.substring(dot_pos + 1); + if (file_ext.length() == 0) + return defaultType; + + for (int i = 0; i < DB.length; i++) { + if (DB[i] == null) + continue; + String result = DB[i].getMIMETypeString(file_ext); + if (result != null) + return result; + } + return defaultType; + } + + /** + * for debugging... + * + public static void main(String[] argv) throws Exception { + MimetypesFileTypeMap map = new MimetypesFileTypeMap(); + System.out.println("File " + argv[0] + " has MIME type " + + map.getContentType(argv[0])); + System.exit(0); + } + */ +}
diff --git a/android/activation/src/main/java/javax/activation/SecuritySupport.java b/android/activation/src/main/java/javax/activation/SecuritySupport.java new file mode 100644 index 0000000..c37174b --- /dev/null +++ b/android/activation/src/main/java/javax/activation/SecuritySupport.java
@@ -0,0 +1,114 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package javax.activation; + +import java.security.*; +import java.net.*; +import java.io.*; +import java.util.*; + +/** + * Security related methods that only work on J2SE 1.2 and newer. + */ +class SecuritySupport { + + private SecuritySupport() { + // private constructor, can't create an instance + } + + public static ClassLoader getContextClassLoader() { + return (ClassLoader) + AccessController.doPrivileged(new PrivilegedAction() { + public Object run() { + ClassLoader cl = null; + try { + cl = Thread.currentThread().getContextClassLoader(); + } catch (SecurityException ex) { } + return cl; + } + }); + } + + public static InputStream getResourceAsStream(final Class c, + final String name) throws IOException { + try { + return (InputStream) + AccessController.doPrivileged(new PrivilegedExceptionAction() { + public Object run() throws IOException { + return c.getResourceAsStream(name); + } + }); + } catch (PrivilegedActionException e) { + throw (IOException)e.getException(); + } + } + + public static URL[] getResources(final ClassLoader cl, final String name) { + return (URL[]) + AccessController.doPrivileged(new PrivilegedAction() { + public Object run() { + URL[] ret = null; + try { + List v = new ArrayList(); + Enumeration e = cl.getResources(name); + while (e != null && e.hasMoreElements()) { + URL url = (URL)e.nextElement(); + if (url != null) + v.add(url); + } + if (v.size() > 0) { + ret = new URL[v.size()]; + ret = (URL[])v.toArray(ret); + } + } catch (IOException ioex) { + } catch (SecurityException ex) { } + return ret; + } + }); + } + + public static URL[] getSystemResources(final String name) { + return (URL[]) + AccessController.doPrivileged(new PrivilegedAction() { + public Object run() { + URL[] ret = null; + try { + List v = new ArrayList(); + Enumeration e = ClassLoader.getSystemResources(name); + while (e != null && e.hasMoreElements()) { + URL url = (URL)e.nextElement(); + if (url != null) + v.add(url); + } + if (v.size() > 0) { + ret = new URL[v.size()]; + ret = (URL[])v.toArray(ret); + } + } catch (IOException ioex) { + } catch (SecurityException ex) { } + return ret; + } + }); + } + + public static InputStream openStream(final URL url) throws IOException { + try { + return (InputStream) + AccessController.doPrivileged(new PrivilegedExceptionAction() { + public Object run() throws IOException { + return url.openStream(); + } + }); + } catch (PrivilegedActionException e) { + throw (IOException)e.getException(); + } + } +}
diff --git a/android/activation/src/main/java/javax/activation/URLDataSource.java b/android/activation/src/main/java/javax/activation/URLDataSource.java new file mode 100644 index 0000000..ad2a13c --- /dev/null +++ b/android/activation/src/main/java/javax/activation/URLDataSource.java
@@ -0,0 +1,120 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package javax.activation; + +import java.net.URL; +import java.net.URLConnection; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.IOException; + +/** + * The URLDataSource class provides an object that wraps a <code>URL</code> + * object in a DataSource interface. URLDataSource simplifies the handling + * of data described by URLs within the JavaBeans Activation Framework + * because this class can be used to create new DataHandlers. <i>NOTE: The + * DataHandler object creates a URLDataSource internally, + * when it is constructed with a URL.</i> + * + * @see javax.activation.DataSource + * @see javax.activation.DataHandler + */ +public class URLDataSource implements DataSource { + private URL url = null; + private URLConnection url_conn = null; + + /** + * URLDataSource constructor. The URLDataSource class will + * not open a connection to the URL until a method requiring it + * to do so is called. + * + * @param url The URL to be encapsulated in this object. + */ + public URLDataSource(URL url) { + this.url = url; + } + + /** + * Returns the value of the URL content-type header field. + * It calls the URL's <code>URLConnection.getContentType</code> method + * after retrieving a URLConnection object. + * <i>Note: this method attempts to call the <code>openConnection</code> + * method on the URL. If this method fails, or if a content type is not + * returned from the URLConnection, getContentType returns + * "application/octet-stream" as the content type.</i> + * + * @return the content type. + */ + public String getContentType() { + String type = null; + + try { + if (url_conn == null) + url_conn = url.openConnection(); + } catch (IOException e) { } + + if (url_conn != null) + type = url_conn.getContentType(); + + if (type == null) + type = "application/octet-stream"; + + return type; + } + + /** + * Calls the <code>getFile</code> method on the URL used to + * instantiate the object. + * + * @return the result of calling the URL's getFile method. + */ + public String getName() { + return url.getFile(); + } + + /** + * The getInputStream method from the URL. Calls the + * <code>openStream</code> method on the URL. + * + * @return the InputStream. + */ + public InputStream getInputStream() throws IOException { + return url.openStream(); + } + + /** + * The getOutputStream method from the URL. First an attempt is + * made to get the URLConnection object for the URL. If that + * succeeds, the getOutputStream method on the URLConnection + * is returned. + * + * @return the OutputStream. + */ + public OutputStream getOutputStream() throws IOException { + // get the url connection if it is available + url_conn = url.openConnection(); + + if (url_conn != null) { + url_conn.setDoOutput(true); + return url_conn.getOutputStream(); + } else + return null; + } + + /** + * Return the URL used to create this DataSource. + * + * @return The URL. + */ + public URL getURL() { + return url; + } +}
diff --git a/android/activation/src/main/java/javax/activation/UnsupportedDataTypeException.java b/android/activation/src/main/java/javax/activation/UnsupportedDataTypeException.java new file mode 100644 index 0000000..b24a788 --- /dev/null +++ b/android/activation/src/main/java/javax/activation/UnsupportedDataTypeException.java
@@ -0,0 +1,40 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package javax.activation; + +import java.io.IOException; + +/** + * Signals that the requested operation does not support the + * requested data type. + * + * @see javax.activation.DataHandler + */ + +public class UnsupportedDataTypeException extends IOException { + /** + * Constructs an UnsupportedDataTypeException with no detail + * message. + */ + public UnsupportedDataTypeException() { + super(); + } + + /** + * Constructs an UnsupportedDataTypeException with the specified + * message. + * + * @param s The detail message. + */ + public UnsupportedDataTypeException(String s) { + super(s); + } +}
diff --git a/android/activation/src/main/java/javax/activation/package.html b/android/activation/src/main/java/javax/activation/package.html new file mode 100644 index 0000000..37bb34e --- /dev/null +++ b/android/activation/src/main/java/javax/activation/package.html
@@ -0,0 +1,27 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> +<HTML> +<HEAD> +<meta http-equiv="Content-Type" content="text/html; charset=utf-8"> +<!-- + + Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Distribution License v. 1.0, which is available at + http://www.eclipse.org/org/documents/edl-v10.php. + + SPDX-License-Identifier: BSD-3-Clause + +--> + +<TITLE>javax.activation package</TITLE> +</HEAD> +<BODY BGCOLOR="white"> + +<P> +The JavaBeans(TM) Activation Framework is used by the JavaMail(TM) +API to manage MIME data. +</P> + +</BODY> +</HTML>
diff --git a/android/activation/src/main/resources/META-INF/mailcap.default b/android/activation/src/main/resources/META-INF/mailcap.default new file mode 100644 index 0000000..73ce2fc --- /dev/null +++ b/android/activation/src/main/resources/META-INF/mailcap.default
@@ -0,0 +1,9 @@ +# +# This is a very simple 'mailcap' file +# +# No default viewers are included so these entries are commented out. +# +#image/gif;; x-java-view=com.sun.activation.viewers.ImageViewer +#image/jpeg;; x-java-view=com.sun.activation.viewers.ImageViewer +#text/*;; x-java-view=com.sun.activation.viewers.TextViewer +#text/*;; x-java-edit=com.sun.activation.viewers.TextEditor
diff --git a/android/activation/src/main/resources/META-INF/mimetypes.default b/android/activation/src/main/resources/META-INF/mimetypes.default new file mode 100644 index 0000000..1b4056b --- /dev/null +++ b/android/activation/src/main/resources/META-INF/mimetypes.default
@@ -0,0 +1,25 @@ +# +# A simple, old format, mime.types file +# +text/html html htm HTML HTM +text/plain txt text TXT TEXT +image/gif gif GIF +image/ief ief +image/jpeg jpeg jpg jpe JPG +image/tiff tiff tif +image/png png PNG +image/x-xwindowdump xwd +application/postscript ai eps ps +application/rtf rtf +application/x-tex tex +application/x-texinfo texinfo texi +application/x-troff t tr roff +audio/basic au +audio/midi midi mid +audio/x-aifc aifc +audio/x-aiff aif aiff +audio/x-mpeg mpeg mpg +audio/x-wav wav +video/mpeg mpeg mpg mpe +video/quicktime qt mov +video/x-msvideo avi
diff --git a/android/mail/pom.xml b/android/mail/pom.xml new file mode 100644 index 0000000..50a9ee1 --- /dev/null +++ b/android/mail/pom.xml
@@ -0,0 +1,151 @@ +<?xml version="1.0" encoding="iso-8859-1"?> +<!-- + + Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<!-- + This project builds the JavaMail Android jar file. +--> + +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 + http://maven.apache.org/maven-v4_0_0.xsd"> + <parent> + <groupId>com.sun.mail</groupId> + <artifactId>android</artifactId> + <version>1.6.3</version> + </parent> + <modelVersion>4.0.0</modelVersion> + <groupId>com.sun.mail</groupId> + <artifactId>android-mail</artifactId> + <packaging>jar</packaging> + <name>JavaMail API for Android</name> + + <properties> + <mail.extensionName> + jakarta.mail.android + </mail.extensionName> + <mail.specificationTitle> + JavaMail(TM) API Design Specification + </mail.specificationTitle> + <mail.implementationTitle> + javax.mail.android + </mail.implementationTitle> + <mail.packages.export> + javax.mail.*; version=${mail.spec.version}, + com.sun.mail.imap; version=${mail.osgiversion}, + com.sun.mail.imap.protocol; version=${mail.osgiversion}, + com.sun.mail.iap; version=${mail.osgiversion}, + com.sun.mail.pop3; version=${mail.osgiversion}, + com.sun.mail.smtp; version=${mail.osgiversion}, + com.sun.mail.util; version=${mail.osgiversion}, + com.sun.mail.util.logging; version=${mail.osgiversion}, + com.sun.mail.handlers; version=${mail.osgiversion} + </mail.packages.export> + </properties> + + <build> + <resources> + <resource> + <directory>src/main/resources</directory> + <filtering>true</filtering> + </resource> + </resources> + <plugins> + <plugin> + <artifactId>maven-dependency-plugin</artifactId> + <executions> + <execution> + <!-- download the binaries --> + <id>get-binaries</id> + <phase>compile</phase> + <goals> + <goal>unpack</goal> + </goals> + </execution> + <execution> + <!-- download the sources --> + <id>get-sources</id> + <phase>compile</phase> + <goals> + <goal>unpack</goal> + </goals> + <configuration> + <artifactItems> + <artifactItem> + <groupId>com.sun.mail</groupId> + <artifactId>jakarta.mail</artifactId> + <version>${mail.version}</version> + <classifier>sources</classifier> + <outputDirectory> + ${project.build.directory}/sources + </outputDirectory> + </artifactItem> + </artifactItems> + </configuration> + </execution> + </executions> + <configuration> + <artifactItems> + <artifactItem> + <groupId>com.sun.mail</groupId> + <artifactId>jakarta.mail</artifactId> + <version>${mail.version}</version> + </artifactItem> + </artifactItems> + <outputDirectory> + ${project.build.outputDirectory} + </outputDirectory> + <excludes> + com/sun/mail/handlers/handler_base.*, + com/sun/mail/handlers/image_*, + com/sun/mail/auth/OAuth2Sasl*, + com/sun/mail/imap/protocol/IMAPSaslAuthenticator*, + com/sun/mail/smtp/SMTPSaslAuthenticator* + </excludes> + </configuration> + </plugin> + <plugin> + <artifactId>maven-jar-plugin</artifactId> + <configuration> + <finalName>${project.artifactId}</finalName> + <archive> + <manifestFile> + ${project.build.outputDirectory}/META-INF/MANIFEST.MF + </manifestFile> + </archive> + </configuration> + </plugin> + </plugins> + </build> + + <dependencies> + <dependency> + <groupId>com.sun.mail</groupId> + <artifactId>android-activation</artifactId> + <version>${mail.version}</version> + </dependency> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <version>4.12</version> + <scope>test</scope> + <optional>true</optional> + </dependency> + </dependencies> +</project>
diff --git a/android/mail/src/main/java/com/sun/mail/handlers/handler_base.java b/android/mail/src/main/java/com/sun/mail/handlers/handler_base.java new file mode 100644 index 0000000..dae159f --- /dev/null +++ b/android/mail/src/main/java/com/sun/mail/handlers/handler_base.java
@@ -0,0 +1,77 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.handlers; + +import java.io.IOException; +import javax.activation.*; + +/** + * Base class for other DataContentHandlers. + */ +public abstract class handler_base implements DataContentHandler { + + /** + * Return an array of ActivationDataFlavors that we support. + * Usually there will be only one. + * + * @return array of ActivationDataFlavors that we support + */ + protected abstract ActivationDataFlavor[] getDataFlavors(); + + /** + * Given the flavor that matched, return the appropriate type of object. + * Usually there's only one flavor so just call getContent. + * + * @param aFlavor the ActivationDataFlavor + * @param ds DataSource containing the data + * @return the object + * @exception IOException for errors reading the data + */ + protected Object getData(ActivationDataFlavor aFlavor, DataSource ds) + throws IOException { + return getContent(ds); + } + + /** + * Return the DataFlavors for this <code>DataContentHandler</code>. + * + * @return The DataFlavors + */ + public ActivationDataFlavor[] getTransferDataFlavors() { + return getDataFlavors().clone(); + } + + /** + * Return the Transfer Data of type DataFlavor from InputStream. + * + * @param df The DataFlavor + * @param ds The DataSource corresponding to the data + * @return the object + * @exception IOException for errors reading the data + */ + public Object getTransferData(ActivationDataFlavor df, DataSource ds) + throws IOException { + ActivationDataFlavor[] adf = getDataFlavors(); + for (int i = 0; i < adf.length; i++) { + // use ActivationDataFlavor.equals, which properly + // ignores Content-Type parameters in comparison + if (adf[i].equals(df)) + return getData(adf[i], ds); + } + return null; + } +}
diff --git a/android/pom.xml b/android/pom.xml new file mode 100644 index 0000000..ed98685 --- /dev/null +++ b/android/pom.xml
@@ -0,0 +1,90 @@ +<?xml version="1.0" encoding="iso-8859-1"?> +<!-- + + Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 + http://maven.apache.org/maven-v4_0_0.xsd"> + <parent> + <groupId>com.sun.mail</groupId> + <artifactId>all</artifactId> + <version>1.6.3</version> + </parent> + <modelVersion>4.0.0</modelVersion> + <groupId>com.sun.mail</groupId> + <artifactId>android</artifactId> + <packaging>pom</packaging> + <name>Android support for JavaMail</name> + + <modules> + <module>activation</module> + <module>mail</module> + </modules> + + <properties> + <!-- + The minimum required Android API version. + This corresponds to Android KitKat. + https://en.wikipedia.org/wiki/Android_version_history + Older versions may also work but this seems old enough. + See the Maven repository for exact versions available: + https://repo1.maven.org/maven2/net/sf/androidscents/signature/ + This only checks API signatures; it doesn't imply that it + works correctly in a runtime of that version. + --> + <android.api.level>19</android.api.level> + <android.version>4.4_r1</android.version> + </properties> + + <build> + <plugins> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>animal-sniffer-maven-plugin</artifactId> + <executions> + <execution> + <id>check-sig</id> + <phase>package</phase> + <goals> + <goal>check</goal> + </goals> + </execution> + </executions> + <configuration> + <signature> + <groupId>net.sf.androidscents.signature</groupId> + <artifactId>android-api-level-${android.api.level}</artifactId> + <version>${android.version}</version> + </signature> + </configuration> + </plugin> + </plugins> + + <pluginManagement> + <plugins> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>animal-sniffer-maven-plugin</artifactId> + <version>1.16</version> + </plugin> + </plugins> + </pluginManagement> + </build> + +</project>
diff --git a/ant-common.xml b/ant-common.xml new file mode 100644 index 0000000..25a01bb --- /dev/null +++ b/ant-common.xml
@@ -0,0 +1,165 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<!-- +Contains common definition that are platform dependent +--> +<target name="tools.init"> + <condition property="windows"> + <os family="windows" /> + </condition> + <condition property="unix"> + <os family="unix" /> + </condition> + <condition property="solaris"> + <os name="SunOS"/> + </condition> + <condition property="linux"> + <os name="Linux"/> + </condition> + <condition property="mac"> + <os name="Mac OS X"/> + </condition> +</target> +<target name="tools.init.windows" if="windows"> + <property name="JAVAH" value="${java.home}/../bin/javah.exe"/> + <property name="MAKE" value="gmake"/> +</target> +<target name="tools.init.unix" if="unix" unless="mac"> + <property name="JAVAH" value="${java.home}/../bin/javah"/> + <property name="MAKE" value="gmake"/> +</target> + <target name="tools.init.mac" if="mac"> + <property name="JAVAH" value="${java.home}/bin/javah"/> + <property name="MAKE" value="make"/> +</target> +<target name="tools.init.platform" + depends="tools.init, tools.init.windows, tools.init.unix, tools.init.mac"> +</target> + + +<!-- + Definitions for Checkstyle +--> +<target name="checkstyle" + description="Generates a report of coding convention violations."> + + <!-- Get properties from environment --> + <property environment="env"/> + + <!-- Set default values if these are not in the environment / cmd line --> + <!-- Order of preference is cmd line, environment, default --> + <property name="env.JWS_EXTERNAL_COMPONENTS_DIR" value="/net/koori.sfbay/onestop/s1aspe/8.0/external"/> + <property name="JWS_EXTERNAL_COMPONENTS_DIR" value="${env.JWS_EXTERNAL_COMPONENTS_DIR}"/> + <property name="env.CHECKSTYLE_HOME" value="${JWS_EXTERNAL_COMPONENTS_DIR}/checkstyle"/> + + <!-- CHECKSTYLE_HOME contains Checkstyle jars and config files --> + <property name="CHECKSTYLE_HOME" value="${env.CHECKSTYLE_HOME}"/> + + <taskdef resource="checkstyletask.properties" classpath="${CHECKSTYLE_HOME}/checkstyle-all-3.5.jar:${CHECKSTYLE_HOME}/checkstyle-sun-modules.jar"/> + + + <!-- CHECKSTYLE_DIR, CHECKSTYLE_FILES, CHECKSTYLE_STYLESHEET, + CHECKSTYLE_CONFIG and CHECKSTYLE_OUTPUT variables have defaults below. + They can be overridden by setting corresponding env variables + in the shell, or from the command line: + % ant -DCHECKSTYLE_DIR=somedir -DCHECKSTYLE_FILES="*.java" .... + (command-line overrides environment vars). --> + <condition property="CHECKSTYLE_FILES" + value="${env.CHECKSTYLE_FILES}"> + <isset property="env.CHECKSTYLE_FILES"/> + </condition> + <property name="CHECKSTYLE_FILES" value="**/*.java"/> + + <condition property="CHECKSTYLE_DIR" + value="${env.CHECKSTYLE_DIR}"> + <isset property="env.CHECKSTYLE_DIR"/> + </condition> + <property name="CHECKSTYLE_DIR" value="."/> + + <condition property="CHECKSTYLE_STYLESHEET" + value="${env.CHECKSTYLE_STYLESHEET}"> + <isset property="env.CHECKSTYLE_STYLESHEET"/> + </condition> + <property name="CHECKSTYLE_STYLESHEET" + value="${CHECKSTYLE_HOME}/checkstyle-noframes-sorted.xsl"/> + + <condition property="CHECKSTYLE_OUTPUT" + value="${env.CHECKSTYLE_OUTPUT}"> + <isset property="env.CHECKSTYLE_OUTPUT"/> + </condition> + <property name="CHECKSTYLE_OUTPUT" value="checkstyle-report.html"/> + + <condition property="CHECKSTYLE_CONFIG" + value="${env.CHECKSTYLE_CONFIG}"> + <isset property="env.CHECKSTYLE_CONFIG"/> + </condition> + <property name="CHECKSTYLE_CONFIG" value="${CHECKSTYLE_HOME}/as-checks.xml"/> + + + <echo message="Running Checkstyle on ${CHECKSTYLE_DIR}/${CHECKSTYLE_FILES} using configuration ${CHECKSTYLE_CONFIG} and writing report to ${CHECKSTYLE_OUTPUT}"/> + + <checkstyle config="${CHECKSTYLE_CONFIG}" + failOnViolation="false"> + <formatter type="xml" tofile="checkstyle-report.xml"/> + <fileset dir="${CHECKSTYLE_DIR}" includes="${CHECKSTYLE_FILES}"/> + </checkstyle> + + <style in="checkstyle-report.xml" out="${CHECKSTYLE_OUTPUT}" style="${CHECKSTYLE_STYLESHEET}"/> + + <delete file="checkstyle-report.xml"/> + +</target> + + +<target name="-push-to-maven-init" description="define a task for pushing bits to the maven repository"> + <echo>${maven.repo.local}/com.sun.wts.tools.mri/jars/maven-repository-importer-${glassfish.maven_repository_importer.version}.jar</echo> + <taskdef resource="maven-repository-importer.properties"> + <classpath> + <pathelement path="${maven.repo.local}/com.sun.wts.tools.mri/jars/maven-repository-importer-${glassfish.maven_repository_importer.version}.jar" /> + </classpath> + </taskdef> +</target> + +<!-- + import files to CVS + + For example, <cvs-import src="build/doc" dest="jaxb/www/doc" /> +--> +<macrodef name="cvs-import"> + <attribute name="src"/> + <attribute name="dest"/> + <sequential> + <tstamp /> + <echo>importing to CVS...</echo> + <cvs dest="@{src}"> + <commandline> + <argument value="-d${glassfish.cvsroot}"/> + <argument line="-z9 import -ko"/> + <argument value="-W"/> + <argument line="*.jar -kb"/> + <argument value="-m"/> + <argument value="deploying new jars to the java.net maven repository"/> + + <argument value="@{dest}"/> + <argument line="deployment-to-maven-repository t${DSTAMP}${TSTAMP}" /> + </commandline> + </cvs> + </sequential> +</macrodef>
diff --git a/build.properties b/build.properties new file mode 100644 index 0000000..25e1398 --- /dev/null +++ b/build.properties
@@ -0,0 +1,108 @@ +# +# Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. +# +# This program and the accompanying materials are made available under the +# terms of the Eclipse Public License v. 2.0, which is available at +# http://www.eclipse.org/legal/epl-2.0. +# +# This Source Code may also be made available under the following Secondary +# Licenses when the conditions for such availability set forth in the +# Eclipse Public License v. 2.0 are satisfied: GNU General Public License, +# version 2 with the GNU Classpath Exception, which is available at +# https://www.gnu.org/software/classpath/license.html. +# +# SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +# + +### Component Properties ### +src.dir=mail/src/main/java +resources.dir=mail/src/main/resources +component.classes.dir=mail/target/classes +### Subcomponent Properties ### +# dsn +src.dsn.dir=dsn/src/main/java +resources.dsn.dir=dsn/src/main/resources +component.classes.dsn.dir=dsn/target/classes +# gimap +src.gimap.dir=gimap/src/main/java +resources.gimap.dir=gimap/src/main/resources +component.classes.gimap.dir=gimap/target/classes +# imap +resources.imap.dir=imap/src/main/resources +component.classes.imap.dir=imap/target/classes +# pop3 +resources.pop3.dir=pop3/src/main/resources +component.classes.pop3.dir=pop3/target/classes +# smtp +resources.smtp.dir=smtp/src/main/resources +component.classes.smtp.dir=smtp/target/classes +# mbox +src.mbox.dir=mbox/src/main/java +resources.mbox.dir=mbox/src/main/resources +component.classes.mbox.dir=mbox/target/classes +# demo +src.demo.dir=demo/src/main/java +component.classes.demo.dir=demo/target/classes +# client +src.client.dir=client/src/main/java +component.classes.client.dir=client/target/classes +# servlet +src.servlet.dir=servlet/src/main/java +component.classes.servlet.dir=servlet/target/classes +# webapp +src.webapp.dir=webapp/src/main/java +docroot.webapp.dir=webapp/src/main/webapp +# taglib +src.taglib.dir=taglib/src/main/java +resources.taglib.dir=taglib/src/main/resources +# logging +src.logging.dir=logging/src/main/java +component.classes.logging.dir=logging/target/classes +# legal +resources.legal.dir=mail/src/main/resources +# +maven.netbeans.exec.build=target/classes + +# needed to build everything on JDK 1.5 +activation.jar=activation.jar +# needed to build servlet and webapp demo programs +javaee.jar=javaee.jar +# external URL references for javadocs +javase.url=http://docs.oracle.com/javase/1.5.0/docs/api +jaf.url=http://docs.oracle.com/javase/6/docs/api + +### Additonal Component Properties for standalone release ### +release.dir=target/release +release.specversion=1.5 +release.version=1.5.1-SNAPSHOT +release.mail.jar=${release.dir}/mail.jar +release.mailapi.jar=${release.dir}/lib/mailapi.jar +release.imap.jar=${release.dir}/lib/imap.jar +release.smtp.jar=${release.dir}/lib/smtp.jar +release.pop3.jar=${release.dir}/lib/pop3.jar +release.gimap.jar=${release.dir}/lib/gimap.jar +release.dsn.jar=${release.dir}/lib/dsn.jar +release.mbox.jar=${release.dir}/lib/mbox.jar +release.docs.dir=${release.dir}/docs +release.javadocs.dir=${release.docs.dir}/javadocs + +# Following from bootstrap/project.properties + +# To support NetBeans' default compile single file target. +build.dir=build +build.classes.dir=${build.dir}/classes + +## Maven repository importer +glassfish.maven_repository_importer.version=1.1 + +########################################################### +# Compilation Flags # +########################################################### +javac.debug=on +javac.optimize=off +javac.deprecation=off +javac.source=1.5 +javac.target=1.5 + +### Use ant.verbose=-verbose for debugging ant targets +ant.verbose=
diff --git a/build.xml b/build.xml new file mode 100644 index 0000000..9f16937 --- /dev/null +++ b/build.xml
@@ -0,0 +1,588 @@ +<?xml version="1.0" encoding="iso-8859-1"?> +<!-- + + Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<!DOCTYPE project [ + <!ENTITY commonBuild SYSTEM "ant-common.xml"> +]> + +<!-- + + NOTE: JavaMail is now built using Maven: + https://java.net/projects/javamail/pages/BuildInstructions + + This ant build file is obsolete and may not work. + +--> + +<project name="JavaMail" default="all" basedir="."> + +<!-- ========== Initialize Properties =================================== --> + + <!-- + component.name: required property. the value should be the + name of the component directory + --> + <property name="component.name" value="mail"/> + + <property file="./build.properties"/> + + &commonBuild; + + <!-- all --> + <target name="all" depends="compile, assemble" + description="Build entire component"> + </target> + + <!-- build --> + <target name="build" depends="compile, assemble" + description="Build entire component"> + </target> + + <!-- init. Initialization involves creating publishing directories and + OS specific targets. --> + <target name="init" description="${component.name} initialization"> + <tstamp> + <format property="start.time" pattern="MM/dd/yyyy hh:mm aa"/> + </tstamp> + <echo message="Building component ${component.name}"/> + <mkdir dir="${component.classes.dir}"/> + </target> + + <!-- compile --> + <target name="compile" depends="init" + description="Compile JavaMail sources"> + + <copy file="${resources.dir}/javax/mail/Version.java" + toFile="${src.dir}/javax/mail/Version.java"> + <filterset begintoken="$${" endtoken="}"> + <filter token="mail.version" value="${release.version}"/> + </filterset> + </copy> + <javac srcdir="${src.dir}" + destdir="${component.classes.dir}" + debug="${javac.debug}" + optimize="${javac.optimize}" + source="${javac.source}" + deprecation="${javac.deprecation}" + failonerror="true" + target="${javac.target}" + includeantruntime="false"> + <classpath> + <pathelement location="${activation.jar}"/> + </classpath> + <include name="javax/mail/**"/> + <include name="com/sun/mail/**"/> + </javac> + </target> + + <!-- prepare manifest files for jars --> + <target name="cook-manifest" depends="init" + description="Generate MANIFEST.MF files"> + <!-- + + Could make this simpler with ant-contrib foreach task... + + <foreach list=",imap.,pop3.,smtp.,dsn." target="cook-manifest-file" + param="mf"/> + --> + <mkdir dir="${component.classes.dir}/manifest"/> + <copy file="${resources.dir}/META-INF/MANIFEST.MF" + tofile="${component.classes.dir}/manifest/MANIFEST.MF"> + <filterset begintoken="{" endtoken="}"> + <filter token="mail.spec.version" + value="${release.specversion}"/> + <filter token="mail.version" value="${release.version}"/> + </filterset> + </copy> + <mkdir dir="${component.classes.imap.dir}/manifest"/> + <copy file="${resources.imap.dir}/META-INF/MANIFEST.MF" + tofile="${component.classes.imap.dir}/manifest/MANIFEST.MF"> + <filterset begintoken="{" endtoken="}"> + <filter token="mail.spec.version" + value="${release.specversion}"/> + <filter token="mail.version" value="${release.version}"/> + </filterset> + </copy> + <mkdir dir="${component.classes.pop3.dir}/manifest"/> + <copy file="${resources.pop3.dir}/META-INF/MANIFEST.MF" + tofile="${component.classes.pop3.dir}/manifest/MANIFEST.MF"> + <filterset begintoken="{" endtoken="}"> + <filter token="mail.spec.version" + value="${release.specversion}"/> + <filter token="mail.version" value="${release.version}"/> + </filterset> + </copy> + <mkdir dir="${component.classes.smtp.dir}/manifest"/> + <copy file="${resources.smtp.dir}/META-INF/MANIFEST.MF" + tofile="${component.classes.smtp.dir}/manifest/MANIFEST.MF"> + <filterset begintoken="{" endtoken="}"> + <filter token="mail.spec.version" + value="${release.specversion}"/> + <filter token="mail.version" value="${release.version}"/> + </filterset> + </copy> + <mkdir dir="${component.classes.dsn.dir}/manifest"/> + <copy file="${resources.dsn.dir}/META-INF/MANIFEST.MF" + tofile="${component.classes.dsn.dir}/manifest/MANIFEST.MF"> + <filterset begintoken="{" endtoken="}"> + <filter token="mail.spec.version" + value="${release.specversion}"/> + <filter token="mail.version" value="${release.version}"/> + </filterset> + </copy> + <mkdir dir="${component.classes.gimap.dir}/manifest"/> + <copy file="${resources.gimap.dir}/META-INF/MANIFEST.MF" + tofile="${component.classes.gimap.dir}/manifest/MANIFEST.MF"> + <filterset begintoken="{" endtoken="}"> + <filter token="mail.spec.version" + value="${release.specversion}"/> + <filter token="mail.version" value="${release.version}"/> + </filterset> + </copy> + </target> + + <!-- + + This could be used with foreach, above. + XXX - do nested variable references work? + + <target name="cook-manifest-file"> + <mkdir dir="${component.classes.${mf}dir}/manifest"/> + <copy file="${resources.${mf}dir}/META-INF/MANIFEST.MF" + tofile="${component.classes.${mf}dir}/manifest/MANIFEST.MF"> + <filterset begintoken="{" endtoken="}"> + <filter token="mail.spec.version" + value="${release.specversion}"/> + <filter token="mail.version" value="${release.version}"/> + </filterset> + </copy> + </target> + --> + + <!-- gimap compile --> + <target name="gimapcompile" depends="init" + description="Compile com/sun/mail/gimap sources"> + + <mkdir dir="${component.classes.gimap.dir}"/> + <javac srcdir="${src.gimap.dir}" + destdir="${component.classes.gimap.dir}" + debug="${javac.debug}" + optimize="${javac.optimize}" + source="${javac.source}" + deprecation="${javac.deprecation}" + failonerror="true" + target="${javac.target}" + includeantruntime="false"> + <classpath> + <pathelement location="${component.classes.dir}"/> + <pathelement location="${activation.jar}"/> + </classpath> + <include name="com/sun/mail/gimap/**"/> + </javac> + </target> + + <!-- dsn compile --> + <target name="dsncompile" depends="init" + description="Compile com/sun/mail/dsn sources"> + + <mkdir dir="${component.classes.dsn.dir}"/> + <javac srcdir="${src.dsn.dir}" + destdir="${component.classes.dsn.dir}" + debug="${javac.debug}" + optimize="${javac.optimize}" + source="${javac.source}" + deprecation="${javac.deprecation}" + failonerror="true" + target="${javac.target}" + includeantruntime="false"> + <classpath> + <pathelement location="${component.classes.dir}"/> + <pathelement location="${activation.jar}"/> + </classpath> + <include name="com/sun/mail/dsn/**"/> + </javac> + </target> + + <!-- demo compile --> + <target name="democompile" depends="init" + description="Compile demo sources"> + + <mkdir dir="${component.classes.demo.dir}"/> + <javac srcdir="${src.demo.dir}" + destdir="${component.classes.demo.dir}" + debug="${javac.debug}" + optimize="${javac.optimize}" + source="${javac.source}" + deprecation="${javac.deprecation}" + failonerror="true" + target="${javac.target}" + includeantruntime="false"> + <classpath> + <pathelement location="${component.classes.dir}"/> + <pathelement location="${activation.jar}"/> + </classpath> + <include name="*"/> + </javac> + + <mkdir dir="${component.classes.client.dir}"/> + <javac srcdir="${src.client.dir}" + destdir="${component.classes.client.dir}" + debug="${javac.debug}" + optimize="${javac.optimize}" + source="${javac.source}" + deprecation="${javac.deprecation}" + failonerror="true" + target="${javac.target}" + includeantruntime="false"> + <classpath> + <pathelement location="${component.classes.dir}"/> + <pathelement location="${activation.jar}"/> + </classpath> + <include name="*"/> + </javac> + + <mkdir dir="${component.classes.servlet.dir}"/> + <javac srcdir="${src.servlet.dir}" + destdir="${component.classes.servlet.dir}" + debug="${javac.debug}" + optimize="${javac.optimize}" + source="${javac.source}" + deprecation="${javac.deprecation}" + failonerror="true" + target="${javac.target}" + includeantruntime="false"> + <classpath> + <pathelement location="${component.classes.dir}"/> + <pathelement location="${activation.jar}"/> + <pathelement location="${javaee.jar}"/> + </classpath> + <include name="*"/> + </javac> + + <mkdir dir="${component.classes.logging.dir}"/> + <javac srcdir="${src.logging.dir}" + destdir="${component.classes.logging.dir}" + debug="${javac.debug}" + optimize="${javac.optimize}" + source="${javac.source}" + deprecation="${javac.deprecation}" + failonerror="true" + target="${javac.target}" + includeantruntime="false"> + <classpath> + <pathelement location="${component.classes.dir}"/> + <pathelement location="${component.classes.demo.dir}"/> + <pathelement location="${activation.jar}"/> + </classpath> + <include name="*"/> + </javac> + + <!-- XXX - still need to add rules to build webapp --> + + </target> + + <!-- assemble --> + <target name="assemble" depends="jar"/> + + <target name="clean" description="Clean the build"> + <delete includeEmptyDirs="true" failonerror="false"> + <fileset dir="${component.classes.dir}"/> + <fileset dir="${component.classes.dsn.dir}"/> + <fileset dir="${component.classes.gimap.dir}"/> + <fileset dir="${component.classes.demo.dir}"/> + <fileset dir="${component.classes.client.dir}"/> + <fileset dir="${component.classes.servlet.dir}"/> + <fileset dir="${component.classes.logging.dir}"/> + </delete> + </target> + + <target name="clobber" description="Clobber all derived files"> + <delete includeEmptyDirs="true" failonerror="false"> + <fileset dir="target"/> + </delete> + </target> + + <!-- JavaMail bundle build targets --> + + <target name="release" depends="init, jars, democompile, docs"> + <!-- XXX - need to replace "." with "_" in release.version --> + <property name="rel" value="javamail-${release.version}"/> + <property name="zipname" value="${rel}.zip"/> + <delete file="${basedir}/target/${zipname}"/> + <echo message="Creating JavaMail bundle ${basedir}/target/${zipname}"/> + <zip destfile="${basedir}/target/${zipname}"> + <zipfileset dir="${release.dir}" prefix="${rel}"/> + <zipfileset dir="${src.demo.dir}" prefix="${rel}/demo" + excludes="**/internal/**"/> + <zipfileset dir="${src.client.dir}" prefix="${rel}/demo/client"/> + <zipfileset dir="${src.servlet.dir}" + prefix="${rel}/demo/servlet"/> + <zipfileset dir="webapp" prefix="${rel}/demo/webapp" + excludes="src/**,pom.xml"/> + <zipfileset dir="${src.webapp.dir}" + prefix="${rel}/demo/webapp/src/classes"/> + <zipfileset dir="${docroot.webapp.dir}" + prefix="${rel}/demo/webapp/src/docroot"/> + <zipfileset dir="${src.taglib.dir}" + prefix="${rel}/demo/webapp/src/taglib"/> + <zipfileset dir="${resources.taglib.dir}" + prefix="${rel}/demo/webapp/src/taglib"/> + <zipfileset dir="${src.logging.dir}" + prefix="${rel}/demo/logging"/> + </zip> + </target> + + <!-- Build all jars needed for release --> + <target name="jars" + depends="jar, mailapijar, pop3jar, imapjar, smtpjar, gimapjar, dsnjar"/> + + <!-- Assemble mail.jar --> + <target name="jar" depends="init, compile, cook-manifest"> + <mkdir dir="${release.dir}"/> + <jar jarfile="${release.mail.jar}" + manifest="${component.classes.dir}/manifest/MANIFEST.MF"> + <metainf dir="${resources.legal.dir}/META-INF" + includes="LICENSE.txt"/> + <metainf dir="${resources.dir}/META-INF" + includes="javamail.charset.map,javamail.default.*,mailcap"/> + <fileset dir="${component.classes.dir}"> + <include name="javax/mail/**/*.class"/> + <include name="com/sun/mail/**/*.class"/> + </fileset> + </jar> + </target> + + <!-- Create zip file of source code to publish to maven repo --> + <target name="src"> + <zip file="target/mail.src.zip"> + <fileset dir="${src.dir}"> + <include name="javax/mail/**/*.java"/> + <include name="com/sun/mail/**/*.java"/> + </fileset> + </zip> + </target> + + <target name="imapjar" depends="init, compile, cook-manifest"> + <mkdir dir="${release.dir}/lib"/> + <jar jarfile="${release.imap.jar}" + manifest="${component.classes.imap.dir}/manifest/MANIFEST.MF"> + <metainf dir="${resources.legal.dir}/META-INF" + includes="LICENSE.txt"/> + <metainf dir="${resources.imap.dir}/META-INF" + includes="javamail.providers"/> + <fileset dir="${component.classes.dir}"> + <include name="com/sun/mail/iap/*.class"/> + <include name="com/sun/mail/imap/*.class"/> + <include name="com/sun/mail/imap/protocol/*.class"/> + </fileset> + </jar> + </target> + + <target name="pop3jar" depends="init, compile, cook-manifest"> + <mkdir dir="${release.dir}/lib"/> + <jar jarfile="${release.pop3.jar}" + manifest="${component.classes.pop3.dir}/manifest/MANIFEST.MF"> + <metainf dir="${resources.legal.dir}/META-INF" + includes="LICENSE.txt"/> + <metainf dir="${resources.pop3.dir}/META-INF" + includes="javamail.providers"/> + <fileset dir="${component.classes.dir}"> + <include name="com/sun/mail/pop3/*.class"/> + </fileset> + </jar> + </target> + + <target name="smtpjar" depends="init, compile, cook-manifest"> + <mkdir dir="${release.dir}/lib"/> + <jar jarfile="${release.smtp.jar}" + manifest="${component.classes.smtp.dir}/manifest/MANIFEST.MF"> + <metainf dir="${resources.legal.dir}/META-INF" + includes="LICENSE.txt"/> + <metainf dir="${resources.smtp.dir}/META-INF" + includes="javamail.providers,javamail.address.map"/> + <fileset dir="${component.classes.dir}"> + <include name="com/sun/mail/smtp/*.class"/> + </fileset> + </jar> + </target> + + <target name="gimapjar" depends="init, gimapcompile, cook-manifest"> + <mkdir dir="${release.dir}/lib"/> + <jar jarfile="${release.gimap.jar}" + manifest="${component.classes.gimap.dir}/manifest/MANIFEST.MF"> + <metainf dir="${resources.legal.dir}/META-INF" + includes="LICENSE.txt"/> + <metainf dir="${resources.gimap.dir}/META-INF" + includes="mailcap"/> + <fileset dir="${component.classes.gimap.dir}"> + <include name="com/sun/mail/gimap/*.class"/> + </fileset> + </jar> + </target> + + <target name="dsnjar" depends="init, dsncompile, cook-manifest"> + <mkdir dir="${release.dir}/lib"/> + <jar jarfile="${release.dsn.jar}" + manifest="${component.classes.dsn.dir}/manifest/MANIFEST.MF"> + <metainf dir="${resources.legal.dir}/META-INF" + includes="LICENSE.txt"/> + <metainf dir="${resources.dsn.dir}/META-INF" + includes="mailcap"/> + <fileset dir="${component.classes.dsn.dir}"> + <include name="com/sun/mail/dsn/*.class"/> + </fileset> + </jar> + </target> + + <target name="mailapijar" depends="init, compile, cook-manifest"> + <mkdir dir="${release.dir}/lib"/> + <jar jarfile="${release.mailapi.jar}" + manifest="${component.classes.dir}/manifest/MANIFEST.MF"> + <metainf dir="${resources.legal.dir}/META-INF" + includes="LICENSE.txt"/> + <metainf dir="${resources.dir}/META-INF" + includes="javamail.charset.map, mailcap"/> + <fileset dir="${component.classes.dir}"> + <include name="javax/mail/**/*.class"/> + <include name="com/sun/mail/util/*.class"/> + <include name="com/sun/mail/handlers/*.class"/> + </fileset> + </jar> + </target> + + <!-- javadocs --> + <target name="docs" depends="init, jar, jar"> + <copy todir="${release.dir}"> + <fileset dir="${basedir}/doc/release" includes="*.txt" /> + </copy> + <mkdir dir="${release.docs.dir}"/> + <copy todir="${release.docs.dir}"> + <fileset dir="${basedir}/doc/spec" includes="*.txt" /> + </copy> + <!-- + To allow us to generate javadocs that only include some + classes in certain packages, we need to copy the sources + to another location and run javadoc against that subset + of the sources. + --> + <copy todir="${release.dir}/javadoc"> + <fileset dir="${src.dir}"> + <include name="**/*.html"/> + <include name="javax/mail/**"/> + </fileset> + <fileset dir="${src.dir}" + includes=" + com/sun/mail/imap/IMAPFolder.java, + com/sun/mail/imap/IMAPMessage.java, + com/sun/mail/imap/IMAPStore.java, + com/sun/mail/imap/IMAPSSLStore.java + com/sun/mail/imap/ACL.java, + com/sun/mail/imap/Rights.java, + com/sun/mail/imap/Quota.java, + com/sun/mail/imap/SortTerm.java, + com/sun/mail/pop3/POP3Store.java, + com/sun/mail/pop3/POP3SSLStore.java, + com/sun/mail/pop3/POP3Folder.java, + com/sun/mail/pop3/POP3Message.java, + com/sun/mail/smtp/SMTPMessage.java, + com/sun/mail/smtp/SMTPAddressFailedException.java, + com/sun/mail/smtp/SMTPAddressSucceededException.java, + com/sun/mail/smtp/SMTPSendFailedException.java, + com/sun/mail/smtp/SMTPSenderFailedException.java, + com/sun/mail/smtp/SMTPTransport.java, + com/sun/mail/smtp/SMTPSSLTransport.java, + com/sun/mail/util/MailConnectException.java, + com/sun/mail/util/MailSSLSocketFactory.java, + com/sun/mail/util/ReadableMime.java, + com/sun/mail/util/logging/MailHandler.java + "/> + <fileset dir="${src.dsn.dir}" + includes=" + com/sun/mail/dsn/package.html, + com/sun/mail/dsn/DeliveryStatus.java, + com/sun/mail/dsn/DispositionNotification.java, + com/sun/mail/dsn/MessageHeaders.java, + com/sun/mail/dsn/MultipartReport.java, + com/sun/mail/dsn/Report.java + "/> + <fileset dir="${src.dsn.dir}" + includes=" + com/sun/mail/gimap/package.html, + com/sun/mail/gimap/GmailStore.java, + com/sun/mail/gimap/GmailSSLStore.java, + com/sun/mail/gimap/GmailFolder.java, + com/sun/mail/gimap/GmailMessage.java, + com/sun/mail/gimap/GmailMsgIdTerm.java, + com/sun/mail/gimap/GmailThrIdTerm.java, + com/sun/mail/gimap/GmailRawSearchTerm.java + "/> + </copy> + <mkdir dir="${release.javadocs.dir}"/> + <javadoc packagenames="javax.mail, javax.mail.internet, + javax.mail.search, javax.mail.event, + javax.mail.util, + com.sun.mail.imap, + com.sun.mail.pop3, + com.sun.mail.smtp, + com.sun.mail.gimap, + com.sun.mail.util, com.sun.mail.util.logging, + com.sun.mail.dsn" + destdir="${release.javadocs.dir}" + sourcepath="${release.dir}/javadoc" + classpath="${activation.jar}:${release.mail.jar}:${release.dsn.jar}:${release.gimap.jar}" + author="false" + version="false" + use="true" + overview="${release.dir}/javadoc/overview.html" + windowtitle="JavaMail API documentation" + doctitle="JavaMail API documentation"> + <group title="JavaMail API Packages" packages="javax.*"/> + <group title="Sun-specific Packages" packages="com.sun.*"/> + <bottom> +<![CDATA[Copyright © 1996-2013, + <a href="http://www.oracle.com">Oracle</a> + and/or its affiliates. All Rights Reserved. + Use is subject to + <a href="{@docRoot}/doc-files/speclicense.html" target="_top">license terms< + /a>. +]]> + + </bottom> + <!-- following can really slow down javadoc generation + <link href="${javase.url}"/> + <link href="${jaf.url}"/> + --> + </javadoc> + </target> + + <target name="push-to-maven-prepare" depends="-push-to-maven-init, jar, src" + description="creates an image for the 'push-to-maven' goal"> + <delete dir="target/maven-repo" /><!-- clean it --> + <maven-repository-importer + destdir="target/maven-repo" version="${release.version}"> + <artifact jar="target/release/mail.jar" pom="mail.pom" + srczip="target/mail.src.zip" /> + </maven-repository-importer> + </target> + + <target name="push-to-maven" depends="push-to-maven-prepare" + description="pushes jars to the java.net maven repository"> + <cvs-import src="target/maven-repo" dest="glassfish/repo" /> + </target> +</project>
diff --git a/client/pom.xml b/client/pom.xml new file mode 100644 index 0000000..d2c3d78 --- /dev/null +++ b/client/pom.xml
@@ -0,0 +1,74 @@ +<?xml version="1.0" encoding="iso-8859-1"?> +<!-- + + Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 + http://maven.apache.org/maven-v4_0_0.xsd"> + <parent> + <groupId>com.sun.mail</groupId> + <artifactId>all</artifactId> + <version>1.6.3</version> + </parent> + <modelVersion>4.0.0</modelVersion> + <groupId>com.sun.mail</groupId> + <artifactId>client</artifactId> + <packaging>jar</packaging> + <name>JavaMail API demo client</name> + + <build> + <plugins> + <!-- + Need to disable the maven-bundle-plugin because the + new version doesn't like classes in the default package. + --> + <plugin> + <groupId>org.apache.felix</groupId> + <artifactId>maven-bundle-plugin</artifactId> + <executions> + <execution> + <id>osgi-manifest</id> + <phase>none</phase> + </execution> + </executions> + </plugin> + + <!-- + Because the maven-jar-plugin depends on the manifest file + created by the maven-bundle-plugin, we need to disable it too. + --> + <plugin> + <artifactId>maven-jar-plugin</artifactId> + <executions> + <execution> + <id>default-jar</id> + <phase>none</phase> + </execution> + </executions> + </plugin> + </plugins> + </build> + + <dependencies> + <dependency> + <groupId>com.sun.mail</groupId> + <artifactId>jakarta.mail</artifactId> + </dependency> + </dependencies> +</project>
diff --git a/client/src/main/java/ComponentFrame.java b/client/src/main/java/ComponentFrame.java new file mode 100644 index 0000000..c82b56e --- /dev/null +++ b/client/src/main/java/ComponentFrame.java
@@ -0,0 +1,53 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +import java.awt.*; +import java.awt.event.*; +import javax.swing.JFrame; +import javax.swing.WindowConstants; + + +/** + * this Frame provides a utility class for displaying a single + * Component in a Frame. + * + * @author Christopher Cotton + */ + +public class ComponentFrame extends JFrame { + + /** + * creates the frame + * @param what the component to display + */ + public ComponentFrame(Component what) { + this(what, "Component Frame"); + } + + /** + * creates the frame with the given name + * @param what the component to display + * @param name the name of the Frame + */ + public ComponentFrame(Component what, String name) { + super(name); + + // make sure that we close and dispose ourselves when needed + setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); + + // default size of the frame + setSize(700,600); + + // we want to display just the component in the entire frame + if (what != null) { + getContentPane().add("Center", what); + } + } +}
diff --git a/client/src/main/java/FolderModel.java b/client/src/main/java/FolderModel.java new file mode 100644 index 0000000..e7a2ec3 --- /dev/null +++ b/client/src/main/java/FolderModel.java
@@ -0,0 +1,139 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +import javax.mail.*; +import java.util.Date; +import javax.swing.table.AbstractTableModel; + +/** + * Maps the messages in a Folder to the Swing's Table Model + * + * @author Christopher Cotton + * @author Bill Shannon + */ + +public class FolderModel extends AbstractTableModel { + + Folder folder; + Message[] messages; + + String[] columnNames = { "Date", "From", "Subject"}; + Class[] columnTypes = { String.class, String.class, String.class }; + + public void setFolder(Folder what) throws MessagingException { + if (what != null) { + + // opened if needed + if (!what.isOpen()) { + what.open(Folder.READ_WRITE); + } + + // get the messages + messages = what.getMessages(); + cached = new String[messages.length][]; + } else { + messages = null; + cached = null; + } + // close previous folder and switch to new folder + if (folder != null) + folder.close(true); + folder = what; + fireTableDataChanged(); + } + + public Message getMessage(int which) { + return messages[which]; + } + + //--------------------- + // Implementation of the TableModel methods + //--------------------- + + public String getColumnName(int column) { + return columnNames[column]; + } + + public Class getColumnClass(int column) { + return columnTypes[column]; + } + + + public int getColumnCount() { + return columnNames.length; + } + + public int getRowCount() { + if (messages == null) + return 0; + + return messages.length; + } + + public Object getValueAt(int aRow, int aColumn) { + switch(aColumn) { + case 0: // date + case 1: // From String[] what = getCachedData(aRow); + case 2: // Subject + String[] what = getCachedData(aRow); + if (what != null) { + return what[aColumn]; + } else { + return ""; + } + + default: + return ""; + } + } + + protected static String[][] cached; + + protected String[] getCachedData(int row) { + if (cached[row] == null) { + try{ + Message m = messages[row]; + + String[] theData = new String[4]; + + // Date + Date date = m.getSentDate(); + if (date == null) { + theData[0] = "Unknown"; + } else { + theData[0] = date.toString(); + } + + // From + Address[] adds = m.getFrom(); + if (adds != null && adds.length != 0) { + theData[1] = adds[0].toString(); + } else { + theData[1] = ""; + } + + // Subject + String subject = m.getSubject(); + if (subject != null) { + theData[2] = subject; + } else { + theData[2] = "(No Subject)"; + } + + cached[row] = theData; + } + catch (MessagingException e) { + e.printStackTrace(); + } + } + + return cached[row]; + } +}
diff --git a/client/src/main/java/FolderTreeNode.java b/client/src/main/java/FolderTreeNode.java new file mode 100644 index 0000000..3e6b925 --- /dev/null +++ b/client/src/main/java/FolderTreeNode.java
@@ -0,0 +1,108 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +import javax.swing.tree.DefaultMutableTreeNode; +import javax.mail.Store; +import javax.mail.Folder; +import javax.mail.MessagingException; + +/** + * Node which represents a Folder in the javax.mail apis. + * + * @author Christopher Cotton + */ +public class FolderTreeNode extends DefaultMutableTreeNode { + + protected Folder folder = null; + protected boolean hasLoaded = false; + + /** + * creates a tree node that points to the particular Store. + * + * @param what the store for this node + */ + public FolderTreeNode(Folder what) { + super(what); + folder = what; + } + + + /** + * a Folder is a leaf if it cannot contain sub folders + */ + public boolean isLeaf() { + try { + if ((folder.getType() & Folder.HOLDS_FOLDERS) == 0) + return true; + } catch (MessagingException me) { } + + // otherwise it does hold folders, and therefore not + // a leaf + return false; + } + + /** + * returns the folder for this node + */ + public Folder getFolder() { + return folder; + } + + + + /** + * return the number of children for this folder node. The first + * time this method is called we load up all of the folders + * under the store's defaultFolder + */ + + public int getChildCount() { + if (!hasLoaded) { + loadChildren(); + } + return super.getChildCount(); + } + + protected void loadChildren() { + // if it is a leaf, just say we have loaded them + if (isLeaf()) { + hasLoaded = true; + return; + } + + try { + // Folder[] sub = folder.listSubscribed(); + Folder[] sub = folder.list(); + + // add a FolderTreeNode for each Folder + int num = sub.length; + for(int i = 0; i < num; i++) { + FolderTreeNode node = new FolderTreeNode(sub[i]); + // we used insert here, since add() would make + // another recursive call to getChildCount(); + insert(node, i); + } + + } catch (MessagingException me) { + me.printStackTrace(); + } + } + + + /** + * override toString() since we only want to display a folder's + * name, and not the full path of the folder + */ + public String toString() { + return folder.getName(); + } + +} +
diff --git a/client/src/main/java/FolderViewer.java b/client/src/main/java/FolderViewer.java new file mode 100644 index 0000000..c762a26 --- /dev/null +++ b/client/src/main/java/FolderViewer.java
@@ -0,0 +1,82 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +import java.awt.*; +import javax.mail.*; +import javax.swing.*; +import javax.swing.event.*; +import javax.swing.table.*; + +/** + * @author Christopher Cotton + * @author Bill Shannon + */ + +public class FolderViewer extends JPanel { + + FolderModel model = new FolderModel(); + JScrollPane scrollpane; + JTable table; + + public FolderViewer() { + this(null); + } + + public FolderViewer(Folder what) { + super(new GridLayout(1,1)); + + table = new JTable(model); + table.setShowGrid(false); + + scrollpane = new JScrollPane(table); + + // setup the folder we were given + setFolder(what); + + // find out what is pressed + table.getSelectionModel().addListSelectionListener( + new FolderPressed()); + scrollpane.setPreferredSize(new Dimension(700, 300)); + add(scrollpane); + } + + /** + * Change the current Folder for the Viewer + * + * @param what the folder to be viewed + */ + public void setFolder(Folder what) { + try { + table.getSelectionModel().clearSelection(); + if (SimpleClient.mv != null) + SimpleClient.mv.setMessage(null); + model.setFolder(what); + scrollpane.invalidate(); + scrollpane.validate(); + } catch (MessagingException me) { + me.printStackTrace(); + } + } + + class FolderPressed implements ListSelectionListener { + + public void valueChanged(ListSelectionEvent e) { + if (model != null && !e.getValueIsAdjusting()) { + ListSelectionModel lm = (ListSelectionModel) e.getSource(); + int which = lm.getMaxSelectionIndex(); + if (which != -1) { + // get the message and display it + Message msg = model.getMessage(which); + SimpleClient.mv.setMessage(msg); + } + } + } + } +}
diff --git a/client/src/main/java/MessageViewer.java b/client/src/main/java/MessageViewer.java new file mode 100644 index 0000000..7b6474f --- /dev/null +++ b/client/src/main/java/MessageViewer.java
@@ -0,0 +1,234 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +import java.awt.*; +import java.awt.event.*; +import javax.mail.*; +import javax.activation.*; +import java.util.Date; +import java.io.IOException; +import javax.swing.JPanel; + +/** + * @author Christopher Cotton + * @author Bill Shannon + */ + +public class MessageViewer extends JPanel implements CommandObject { + + Message displayed = null; + DataHandler dataHandler = null; + String verb = null; + Component mainbody; + TextArea headers; + + public MessageViewer() { + this(null); + } + + public MessageViewer(Message what) { + // set our layout + super(new GridBagLayout()); + + // add the toolbar + addToolbar(); + + GridBagConstraints gb = new GridBagConstraints(); + gb.gridwidth = GridBagConstraints.REMAINDER; + gb.fill = GridBagConstraints.BOTH; + gb.weightx = 1.0; + gb.weighty = 0.0; + + // add the headers + headers = new TextArea("", 4, 80, TextArea.SCROLLBARS_NONE); + headers.setEditable(false); + add(headers, gb); + + // now display our message + setMessage(what); + } + + /** + * sets the current message to be displayed in the viewer + */ + public void setMessage(Message what) { + displayed = what; + + if (mainbody != null) + remove(mainbody); + + if (what != null) { + loadHeaders(); + mainbody = getBodyComponent(); + } else { + headers.setText(""); + TextArea dummy = new TextArea("", 24, 80, TextArea.SCROLLBARS_NONE); + dummy.setEditable(false); + mainbody = dummy; + } + + // add the main body + GridBagConstraints gb = new GridBagConstraints(); + gb.gridwidth = GridBagConstraints.REMAINDER; + gb.fill = GridBagConstraints.BOTH; + gb.weightx = 1.0; + gb.weighty = 1.0; + add(mainbody, gb); + + invalidate(); + validate(); + } + + protected void addToolbar() { + GridBagConstraints gb = new GridBagConstraints(); + gb.gridheight = 1; + gb.gridwidth = 1; + gb.fill = GridBagConstraints.NONE; + gb.anchor = GridBagConstraints.WEST; + gb.weightx = 0.0; + gb.weighty = 0.0; + gb.insets = new Insets(4,4,4,4); + + // structure button + gb.gridwidth = GridBagConstraints.REMAINDER; // only for the last one + Button b = new Button("Structure"); + b.addActionListener( new StructureAction()); + add(b, gb); + } + + protected void loadHeaders() { + // setup what we want in our viewer + StringBuffer sb = new StringBuffer(); + + // date + sb.append("Date: "); + try { + Date duh = displayed.getSentDate(); + if (duh != null) { + sb.append(duh.toString()); + } else { + sb.append("Unknown"); + } + + sb.append("\n"); + + // from + sb.append("From: "); + Address[] adds = displayed.getFrom(); + if (adds != null && adds.length > 0) { + sb.append(adds[0].toString()); + } + sb.append("\n"); + + // to + sb.append("To: "); + adds = displayed.getRecipients(Message.RecipientType.TO); + if (adds != null && adds.length > 0) { + sb.append(adds[0].toString()); + } + sb.append("\n"); + + // subject + sb.append("Subject: "); + sb.append(displayed.getSubject()); + + headers.setText(sb.toString()); + } catch (MessagingException me) { + headers.setText(""); + } + } + + protected Component getBodyComponent() { + //------------ + // now get a content viewer for the main type... + //------------ + try { + DataHandler dh = displayed.getDataHandler(); + CommandInfo ci = dh.getCommand("view"); + if (ci == null) { + throw new MessagingException("view command failed on: " + + displayed.getContentType()); + } + + Object bean = dh.getBean(ci); + if (bean instanceof Component) { + return (Component)bean; + } else { + throw new MessagingException("bean is not a component " + + bean.getClass().toString()); + } + } catch (MessagingException me) { + return new Label(me.toString()); + } + } + + /** + * the CommandObject method to accept our DataHandler + * @param dh the datahandler used to get the content + */ + public void setCommandContext(String verb, + DataHandler dh) throws IOException { + this.verb = verb; + dataHandler = dh; + + Object o = dh.getContent(); + if (o instanceof Message) { + setMessage((Message)o); + } + else { + System.out.println( + "MessageViewer - content not a Message object, " + o); + if (o != null){ + System.out.println(o.getClass().toString()); + } + } + } + + + class StructureAction implements ActionListener { + StringBuffer sb; + + public void actionPerformed(ActionEvent e) { + System.out.println("\n\nMessage Structure"); + dumpPart("", displayed); + } + + protected void dumpPart(String prefix, Part p) { + try { + System.out.println(prefix + "----------------"); + System.out.println(prefix + + "Content-Type: " + p.getContentType()); + System.out.println(prefix + + "Class: " + p.getClass().toString()); + + Object o = p.getContent(); + if (o == null) { + System.out.println(prefix + "Content: is null"); + } else { + System.out.println(prefix + + "Content: " + o.getClass().toString()); + } + + if (o instanceof Multipart) { + String newpref = prefix + "\t"; + Multipart mp = (Multipart)o; + int count = mp.getCount(); + for (int i = 0; i < count; i++) { + dumpPart(newpref, mp.getBodyPart(i)); + } + } + } catch (MessagingException e) { + e.printStackTrace(); + } catch (IOException ioex) { + System.out.println("Cannot get content" + ioex.getMessage()); + } + } + } +}
diff --git a/client/src/main/java/MultipartViewer.java b/client/src/main/java/MultipartViewer.java new file mode 100644 index 0000000..193b8a8 --- /dev/null +++ b/client/src/main/java/MultipartViewer.java
@@ -0,0 +1,165 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +import java.awt.*; +import java.awt.event.*; +import java.io.*; +import java.beans.*; +import javax.activation.*; +import javax.mail.*; +import javax.swing.JPanel; + + +/** + * A Viewer Bean for the type multipart/mixed + * + * @author Christopher Cotton + */ + +public class MultipartViewer extends JPanel implements CommandObject { + + protected DataHandler dh = null; + protected String verb = null; + + public MultipartViewer() { + super(new GridBagLayout()); + } + + + public void setCommandContext(String verb, DataHandler dh) throws IOException { + this.verb = verb; + this.dh = dh; + + // get the content, and hope it is a Multipart Object + Object content = dh.getContent(); + if (content instanceof Multipart) { + setupDisplay((Multipart)content); + } else { + setupErrorDisplay(content); + } + } + + protected void setupDisplay(Multipart mp) { + // we display the first body part in a main frame on the left, and then + // on the right we display the rest of the parts as attachments + + GridBagConstraints gc = new GridBagConstraints(); + gc.gridheight = GridBagConstraints.REMAINDER; + gc.fill = GridBagConstraints.BOTH; + gc.weightx = 1.0; + gc.weighty = 1.0; + + // get the first part + try { + BodyPart bp = mp.getBodyPart(0); + Component comp = getComponent(bp); + add(comp, gc); + + } catch (MessagingException me) { + add(new Label(me.toString()), gc); + } + + // see if there are more than one parts + try { + int count = mp.getCount(); + + // setup how to display them + gc.gridwidth = GridBagConstraints.REMAINDER; + gc.gridheight = 1; + gc.fill = GridBagConstraints.NONE; + gc.anchor = GridBagConstraints.NORTH; + gc.weightx = 0.0; + gc.weighty = 0.0; + gc.insets = new Insets(4,4,4,4); + + // for each one we create a button with the content type + for(int i = 1; i < count; i++) { // we skip the first one + BodyPart curr = mp.getBodyPart(i); + String label = null; + if (label == null) label = curr.getFileName(); + if (label == null) label = curr.getDescription(); + if (label == null) label = curr.getContentType(); + + Button but = new Button(label); + but.addActionListener( new AttachmentViewer(curr)); + add(but, gc); + } + + + } catch(MessagingException me2) { + me2.printStackTrace(); + } + + } + + protected Component getComponent(BodyPart bp) { + + try { + DataHandler dh = bp.getDataHandler(); + CommandInfo ci = dh.getCommand("view"); + if (ci == null) { + throw new MessagingException( + "view command failed on: " + + bp.getContentType()); + } + + Object bean = dh.getBean(ci); + + if (bean instanceof Component) { + return (Component)bean; + } else { + if (bean == null) + throw new MessagingException( + "bean is null, class " + ci.getCommandClass() + + " , command " + ci.getCommandName()); + else + throw new MessagingException( + "bean is not a awt.Component" + + bean.getClass().toString()); + } + } + catch (MessagingException me) { + return new Label(me.toString()); + } + } + + + + protected void setupErrorDisplay(Object content) { + String error; + + if (content == null) + error = "Content is null"; + else + error = "Object not of type Multipart, content class = " + + content.getClass().toString(); + + System.out.println(error); + Label lab = new Label(error); + add(lab); + } + + class AttachmentViewer implements ActionListener { + + BodyPart bp = null; + + public AttachmentViewer(BodyPart part) { + bp = part; + } + + public void actionPerformed(ActionEvent e) { + ComponentFrame f = new ComponentFrame( + getComponent(bp), "Attachment"); + f.pack(); + f.show(); + } + } + +}
diff --git a/client/src/main/java/README.txt b/client/src/main/java/README.txt new file mode 100644 index 0000000..2b00943 --- /dev/null +++ b/client/src/main/java/README.txt
@@ -0,0 +1,124 @@ +SimpleClient +------------ + +Notes: +====== + +This should not be taken as a demo of how to use the Swing API, but +rather a very simple graphical mail client. It shows how viewers can +be used to display the content from mail messages. It also (like the +other demos) shows how to retrieve Folders from a Store, Messages +from a Folder, and content from Messages. + + +To run the demo: +================ + + 1. If you're using JDK 1.1.x, download the latest version of the JFC + (Swing) APIs from http://java.sun.com/products/jfc/download.html. + The SimpleClient uses at least version 1.1 of Swing. + + If you're using JDK 1.2 (J2SE 1.2) or newer, Swing is included + and no separate download is necessary. + + We *strongly* encourage you to use the latest version of J2SE, + which you can download from http://java.sun.com/j2se/. + + 2. Set your CLASSPATH to include the "mail.jar", "activation.jar", + and (if you're using JDK 1.1.x and downloaded Swing separately) + "swingall.jar", and the current directory. For example: + + For JDK 1.1 on UNIX: + + export CLASSPATH=/u/me/download/mail.jar:/u/me/download/activation.jar:/u/me/download/swingall.jar:. + + For JDK 1.2 and newer on UNIX: + + export CLASSPATH=/u/me/download/mail.jar:/u/me/download/activation.jar:. + + 3. Go to the demo/client directory + + 4. Compile all the files using your Java compiler. For example: + + javac *.java + + 5. Run the demo. For example: + + java SimpleClient -L imap://username:password@hostname/ + + Note that SimpleClient expects to read the "simple.mailcap" + file from the current directory. The simple.mailcap file + contains configuration information about viewers needed by + the SimpleClient demo program. + + + +Overview of the Classes +======================= + +Main Classes: + + SimpleClient = contains main(). + Uses the parameters to the application to + locate the correct Store. e.g. + + SimpleClient -L imap://cotton:secret@snow-goon/ + + It will create the main frame and + creates a tree. The tree uses the + StoreTreeNodes and FolderTreeNodes. + + StoreTreeNode = subclass of Swing's DefaultMutableTreeNode. + This class shows how to get Folders from + the Store. + + FolderTreeNode = subclass of Swing's DefaultMutableTreeNode. + If the folder has messages, it will create + a FolderViewer. Otherwise it will add the + subfolders to the tree. + + SimpleAuthenticator = subclass of javax.mail.Authenticator. If + the Store is missing the username or the + password, this authenticator will be used. + It displays a dialog requesting the + information from the user. + + +Viewing Folders: + + FolderViewer = Uses a Swing Table to display all of the + Message in a Folder. The "model" of the + data for this Table is a FolderModel which + knows how to get displayable information + from a Message. + +JAF Viewers: + + MessageViewer = Uses the content of the DataHandler. The + content will be a javax.mail.Message + object. Displays the headers and then + uses the JAF to find another viewer for + the content type of the Message. (either + multipart/mixed, image/gif, or text/plain) + + MultipartViewer = Uses the content of the DataHandler. The + content will be a javax.mail.Multipart + object. Uses the JAF to find another + viewer for the first BodyPart's content. + Also puts Buttons (as "attachments") for + the rest of the BodyParts. When the + Button are pressed, it uses the JAF to + find a viewer for the BodyPart's content, + and displays it in a separate frame (using + ComponentFrame). + + TextViewer = Uses the content of the DataHandler. The + content will be either a java.lang.String + object, or a java.io.InputStream object. + Creates a TextArea and sets the text using + the String or InputStream. + +Support Classes: + + ComponentFrame = support class which takes a java.awt.Component + and displays it in a Frame.
diff --git a/client/src/main/java/SimpleAuthenticator.java b/client/src/main/java/SimpleAuthenticator.java new file mode 100644 index 0000000..55bacc5 --- /dev/null +++ b/client/src/main/java/SimpleAuthenticator.java
@@ -0,0 +1,127 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +import javax.mail.*; +import java.net.InetAddress; +import java.awt.*; +import javax.swing.*; + +/** + * Simple Authenticator for requesting password information. + * + * @author Christopher Cotton + * @author Bill Shannon + */ + +public class SimpleAuthenticator extends Authenticator { + + Frame frame; + String username; + String password; + + public SimpleAuthenticator(Frame f) { + this.frame = f; + } + + protected PasswordAuthentication getPasswordAuthentication() { + + // given a prompt? + String prompt = getRequestingPrompt(); + if (prompt == null) + prompt = "Please login..."; + + // protocol + String protocol = getRequestingProtocol(); + if (protocol == null) + protocol = "Unknown protocol"; + + // get the host + String host = null; + InetAddress inet = getRequestingSite(); + if (inet != null) + host = inet.getHostName(); + if (host == null) + host = "Unknown host"; + + // port + String port = ""; + int portnum = getRequestingPort(); + if (portnum != -1) + port = ", port " + portnum + " "; + + // Build the info string + String info = "Connecting to " + protocol + " mail service on host " + + host + port; + + //JPanel d = new JPanel(); + // XXX - for some reason using a JPanel here causes JOptionPane + // to display incorrectly, so we workaround the problem using + // an anonymous JComponent. + JComponent d = new JComponent() { }; + + GridBagLayout gb = new GridBagLayout(); + GridBagConstraints c = new GridBagConstraints(); + d.setLayout(gb); + c.insets = new Insets(2, 2, 2, 2); + + c.anchor = GridBagConstraints.WEST; + c.gridwidth = GridBagConstraints.REMAINDER; + c.weightx = 0.0; + d.add(constrain(new JLabel(info), gb, c)); + d.add(constrain(new JLabel(prompt), gb, c)); + + c.gridwidth = 1; + c.anchor = GridBagConstraints.EAST; + c.fill = GridBagConstraints.NONE; + c.weightx = 0.0; + d.add(constrain(new JLabel("Username:"), gb, c)); + + c.anchor = GridBagConstraints.EAST; + c.fill = GridBagConstraints.HORIZONTAL; + c.gridwidth = GridBagConstraints.REMAINDER; + c.weightx = 1.0; + String user = getDefaultUserName(); + JTextField username = new JTextField(user, 20); + d.add(constrain(username, gb, c)); + + c.gridwidth = 1; + c.fill = GridBagConstraints.NONE; + c.anchor = GridBagConstraints.EAST; + c.weightx = 0.0; + d.add(constrain(new JLabel("Password:"), gb, c)); + + c.anchor = GridBagConstraints.EAST; + c.fill = GridBagConstraints.HORIZONTAL; + c.gridwidth = GridBagConstraints.REMAINDER; + c.weightx = 1.0; + JPasswordField password = new JPasswordField("", 20); + d.add(constrain(password, gb, c)); + // XXX - following doesn't work + if (user != null && user.length() > 0) + password.requestFocus(); + else + username.requestFocus(); + + int result = JOptionPane.showConfirmDialog(frame, d, "Login", + JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE); + + if (result == JOptionPane.OK_OPTION) + return new PasswordAuthentication(username.getText(), + password.getText()); + else + return null; + } + + private Component constrain(Component cmp, + GridBagLayout gb, GridBagConstraints c) { + gb.setConstraints(cmp, c); + return (cmp); + } +}
diff --git a/client/src/main/java/SimpleClient.java b/client/src/main/java/SimpleClient.java new file mode 100644 index 0000000..447be55 --- /dev/null +++ b/client/src/main/java/SimpleClient.java
@@ -0,0 +1,148 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +import java.util.*; +import java.io.*; +import javax.mail.*; +import javax.mail.internet.*; +import javax.activation.*; +import java.awt.*; +import java.awt.event.*; +import javax.swing.*; +import javax.swing.table.*; +import javax.swing.tree.*; +import javax.swing.event.*; + + +/** + * Demo app that shows a very simple Mail Client + * + * @author Christopher Cotton + * @author Bill Shannon + */ + +public class SimpleClient { + + static Vector url = new Vector(); + static FolderViewer fv; + static MessageViewer mv; + + public static void main(String argv[]) { + boolean usage = false; + + for (int optind = 0; optind < argv.length; optind++) { + if (argv[optind].equals("-L")) { + url.addElement(argv[++optind]); + } else if (argv[optind].startsWith("-")) { + usage = true; + break; + } else { + usage = true; + break; + } + } + + if (usage || url.size() == 0) { + System.out.println("Usage: SimpleClient -L url"); + System.out.println(" where url is protocol://username:password@hostname/"); + System.exit(1); + } + + try { + // Set up our Mailcap entries. This will allow the JAF + // to locate our viewers. + File capfile = new File("simple.mailcap"); + if (!capfile.isFile()) { + System.out.println( + "Cannot locate the \"simple.mailcap\" file."); + System.exit(1); + } + + CommandMap.setDefaultCommandMap( new MailcapCommandMap( + new FileInputStream(capfile))); + + JFrame frame = new JFrame("Simple JavaMail Client"); + frame.addWindowListener(new WindowAdapter() { + public void windowClosing(WindowEvent e) {System.exit(0);}}); + //frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + + // Get a Store object + SimpleAuthenticator auth = new SimpleAuthenticator(frame); + Session session = + Session.getDefaultInstance(System.getProperties(), auth); + //session.setDebug(true); + + DefaultMutableTreeNode root = new DefaultMutableTreeNode("Root"); + + // create a node for each store we have + for (Enumeration e = url.elements() ; e.hasMoreElements() ;) { + String urlstring = (String) e.nextElement(); + URLName urln = new URLName(urlstring); + Store store = session.getStore(urln); + + StoreTreeNode storenode = new StoreTreeNode(store); + root.add(storenode); + } + + DefaultTreeModel treeModel = new DefaultTreeModel(root); + JTree tree = new JTree(treeModel); + tree.addTreeSelectionListener(new TreePress()); + + /* Put the Tree in a scroller. */ + JScrollPane sp = new JScrollPane(); + sp.setPreferredSize(new Dimension(250, 300)); + sp.getViewport().add(tree); + + /* Create a double buffered JPanel */ + JPanel sv = new JPanel(new BorderLayout()); + sv.add("Center", sp); + + fv = new FolderViewer(null); + + JSplitPane jsp = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, + sv, fv); + jsp.setOneTouchExpandable(true); + mv = new MessageViewer(); + JSplitPane jsp2 = new JSplitPane(JSplitPane.VERTICAL_SPLIT, + jsp, mv); + jsp2.setOneTouchExpandable(true); + + frame.getContentPane().add(jsp2); + frame.pack(); + frame.show(); + + } catch (Exception ex) { + System.out.println("SimpletClient caught exception"); + ex.printStackTrace(); + System.exit(1); + } + } + +} + +class TreePress implements TreeSelectionListener { + + public void valueChanged(TreeSelectionEvent e) { + TreePath path = e.getNewLeadSelectionPath(); + if (path != null) { + Object o = path.getLastPathComponent(); + if (o instanceof FolderTreeNode) { + FolderTreeNode node = (FolderTreeNode)o; + Folder folder = node.getFolder(); + + try { + if ((folder.getType() & Folder.HOLDS_MESSAGES) != 0) { + SimpleClient.fv.setFolder(folder); + } + } catch (MessagingException me) { } + } + } + } +}
diff --git a/client/src/main/java/StoreTreeNode.java b/client/src/main/java/StoreTreeNode.java new file mode 100644 index 0000000..3ca9a5d --- /dev/null +++ b/client/src/main/java/StoreTreeNode.java
@@ -0,0 +1,107 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +import javax.swing.tree.DefaultMutableTreeNode; +import javax.mail.*; + +/** + * Node which represents a Store in the javax.mail apis. + * + * @author Christopher Cotton + */ +public class StoreTreeNode extends DefaultMutableTreeNode { + + protected Store store = null; + protected Folder folder = null; + protected String display = null; + + /** + * creates a tree node that points to the particular Store. + * + * @param what the store for this node + */ + public StoreTreeNode(Store what) { + super(what); + store = what; + } + + + /** + * a Store is never a leaf node. It can always contain stuff + */ + public boolean isLeaf() { + return false; + } + + + /** + * return the number of children for this store node. The first + * time this method is called we load up all of the folders + * under the store's defaultFolder + */ + + public int getChildCount() { + if (folder == null) { + loadChildren(); + } + return super.getChildCount(); + } + + protected void loadChildren() { + try { + // connect to the Store if we need to + if (!store.isConnected()) { + store.connect(); + } + + // get the default folder, and list the + // subscribed folders on it + folder = store.getDefaultFolder(); + // Folder[] sub = folder.listSubscribed(); + Folder[] sub = folder.list(); + + // add a FolderTreeNode for each Folder + int num = sub.length; + for(int i = 0; i < num; i++) { + FolderTreeNode node = new FolderTreeNode(sub[i]); + // we used insert here, since add() would make + // another recursive call to getChildCount(); + insert(node, i); + } + + } catch (MessagingException me) { + me.printStackTrace(); + } + } + + /** + * We override toString() so we can display the store URLName + * without the password. + */ + + public String toString() { + if (display == null) { + URLName url = store.getURLName(); + if (url == null) { + display = store.toString(); + } else { + // don't show the password + URLName too = new URLName( url.getProtocol(), url.getHost(), url.getPort(), + url.getFile(), url.getUsername(), null); + display = too.toString(); + } + } + + return display; + } + + +} +
diff --git a/client/src/main/java/TextViewer.java b/client/src/main/java/TextViewer.java new file mode 100644 index 0000000..1c9e58c --- /dev/null +++ b/client/src/main/java/TextViewer.java
@@ -0,0 +1,87 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +import java.awt.*; +import java.io.*; +import java.beans.*; +import javax.activation.*; +import javax.swing.JPanel; +import javax.swing.JTextArea; +import javax.swing.JScrollPane; + + +/** + * A very simple TextViewer Bean for the MIMEType "text/plain" + * + * @author Christopher Cotton + */ + +public class TextViewer extends JPanel implements CommandObject +{ + + private JTextArea text_area = null; + private DataHandler dh = null; + private String verb = null; + + /** + * Constructor + */ + public TextViewer() { + super(new GridLayout(1,1)); + + // create the text area + text_area = new JTextArea(); + text_area.setEditable(false); + text_area.setLineWrap(true); + + // create a scroll pane for the JTextArea + JScrollPane sp = new JScrollPane(); + sp.setPreferredSize(new Dimension(300, 300)); + sp.getViewport().add(text_area); + + add(sp); + } + + + public void setCommandContext(String verb, DataHandler dh) + throws IOException { + + this.verb = verb; + this.dh = dh; + + this.setInputStream( dh.getInputStream() ); + } + + + /** + * set the data stream, component to assume it is ready to + * be read. + */ + public void setInputStream(InputStream ins) { + + int bytes_read = 0; + // check that we can actually read + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte data[] = new byte[1024]; + + try { + while((bytes_read = ins.read(data)) >0) + baos.write(data, 0, bytes_read); + ins.close(); + } catch(Exception e) { + e.printStackTrace(); + } + + // convert the buffer into a string + // place in the text area + text_area.setText(baos.toString()); + + } +}
diff --git a/client/src/main/java/simple.mailcap b/client/src/main/java/simple.mailcap new file mode 100644 index 0000000..e65e13a --- /dev/null +++ b/client/src/main/java/simple.mailcap
@@ -0,0 +1,9 @@ +# +# Example command map, using the MailcapCommandMap. +# +# for our viewers +# +message/*;; x-java-view=MessageViewer +text/plain;; x-java-view=TextViewer +multipart/*;; x-java-view=MultipartViewer +
diff --git a/copyright-exclude b/copyright-exclude new file mode 100644 index 0000000..040308f --- /dev/null +++ b/copyright-exclude
@@ -0,0 +1,31 @@ +copyright-exclude +/MANIFEST.MF +/META-INF/services/ +/META-INF/javamail +/META-INF/mailcap +/README +doc/release/ +doc/spec/ +/LICENSE.txt +CONTRIBUTING.md +LICENSE.md +.hgtags +.hgignore +client/src/main/java/simple.mailcap +mail/src/oldtest/java/javax/mail/internet/encodedheaders.data +mail/src/oldtest/java/javax/mail/internet/decodetest +mail.sig +webapp/build.bat +webapp/webapp.README.txt +logging/src/main/java/ +mail/src/main/java/com/sun/mail/util/logging/ +mail/src/main/resources/META-INF/hk2-locator/default +mail/src/test/java/com/sun/mail/util/logging/ +mail/src/test/resources/javax/mail/internet/paramdata +mail/src/test/resources/javax/mail/internet/paramdatanostrict +mail/src/test/resources/javax/mail/internet/tokenlist +mail/src/test/resources/javax/mail/internet/addrlist +mail/src/test/resources/javax/mail/internet/MailDateFormat_old.ser +mail/src/test/resources/javax/mail/internet/MailDateFormat_new.ser +mail/src/main/java/doc-files/speclicense.html +android/activation/src/main/resources/META-INF/mimetypes.default
diff --git a/demo/pom.xml b/demo/pom.xml new file mode 100644 index 0000000..c2becf0 --- /dev/null +++ b/demo/pom.xml
@@ -0,0 +1,83 @@ +<?xml version="1.0" encoding="iso-8859-1"?> +<!-- + + Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 + http://maven.apache.org/maven-v4_0_0.xsd"> + <parent> + <groupId>com.sun.mail</groupId> + <artifactId>all</artifactId> + <version>1.6.3</version> + </parent> + <modelVersion>4.0.0</modelVersion> + <groupId>com.sun.mail</groupId> + <artifactId>demo</artifactId> + <packaging>jar</packaging> + <name>JavaMail API demos</name> + + <build> + <plugins> + <plugin> + <artifactId>maven-compiler-plugin</artifactId> + <configuration> + <excludes> + <exclude>internal/**</exclude> + </excludes> + </configuration> + </plugin> + + <!-- + Need to disable the maven-bundle-plugin because the + new version doesn't like classes in the default package. + --> + <plugin> + <groupId>org.apache.felix</groupId> + <artifactId>maven-bundle-plugin</artifactId> + <executions> + <execution> + <id>osgi-manifest</id> + <phase>none</phase> + </execution> + </executions> + </plugin> + + <!-- + Because the maven-jar-plugin depends on the manifest file + created by the maven-bundle-plugin, we need to disable it too. + --> + <plugin> + <artifactId>maven-jar-plugin</artifactId> + <executions> + <execution> + <id>default-jar</id> + <phase>none</phase> + </execution> + </executions> + </plugin> + </plugins> + </build> + + <dependencies> + <dependency> + <groupId>com.sun.mail</groupId> + <artifactId>jakarta.mail</artifactId> + </dependency> + </dependencies> +</project>
diff --git a/demo/src/main/java/CRLFOutputStream.java b/demo/src/main/java/CRLFOutputStream.java new file mode 100644 index 0000000..00cb23d --- /dev/null +++ b/demo/src/main/java/CRLFOutputStream.java
@@ -0,0 +1,72 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +import java.io.*; + +/** + * Convert lines into the canonical MIME format, that is, + * terminate lines with CRLF. <p> + * + * This stream can be used with the Part.writeTo and Message.writeTo + * methods to generate the canonical MIME format of the data for the + * purpose of (e.g.) sending it via SMTP or computing a digital + * signature. + */ +public class CRLFOutputStream extends FilterOutputStream { + protected int lastb = -1; + protected static byte[] newline; + static { + newline = new byte[2]; + newline[0] = (byte)'\r'; + newline[1] = (byte)'\n'; + } + + public CRLFOutputStream(OutputStream os) { + super(os); + } + + public void write(int b) throws IOException { + if (b == '\r') { + out.write(newline); + } else if (b == '\n') { + if (lastb != '\r') + out.write(newline); + } else { + out.write(b); + } + lastb = b; + } + + public void write(byte b[]) throws IOException { + write(b, 0, b.length); + } + + public void write(byte b[], int off, int len) throws IOException { + int start = off; + + len += off; + for (int i = start; i < len ; i++) { + if (b[i] == '\r') { + out.write(b, start, i - start); + out.write(newline); + start = i + 1; + } else if (b[i] == '\n') { + if (lastb != '\r') { + out.write(b, start, i - start); + out.write(newline); + } + start = i + 1; + } + lastb = b[i]; + } + if ((len - start) > 0) + out.write(b, start, len - start); + } +}
diff --git a/demo/src/main/java/NewlineOutputStream.java b/demo/src/main/java/NewlineOutputStream.java new file mode 100644 index 0000000..c620029 --- /dev/null +++ b/demo/src/main/java/NewlineOutputStream.java
@@ -0,0 +1,61 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +import java.io.*; + +/** + * Convert the various newline conventions to the local platform's + * newline convention. <p> + * + * This stream can be used with the Message.writeTo method to + * generate a message that uses the local plaform's line terminator + * for the purpose of (e.g.) saving the message to a local file. + */ +public class NewlineOutputStream extends FilterOutputStream { + private int lastb = -1; + private static byte[] newline; + + public NewlineOutputStream(OutputStream os) { + super(os); + if (newline == null) { + String s = System.lineSeparator(); + if (s == null || s.length() <= 0) + s = "\n"; + try { + newline = s.getBytes("iso-8859-1"); // really us-ascii + } catch (UnsupportedEncodingException ex) { + // should never happen + newline = new byte[] { (byte)'\n' }; + } + } + } + + public void write(int b) throws IOException { + if (b == '\r') { + out.write(newline); + } else if (b == '\n') { + if (lastb != '\r') + out.write(newline); + } else { + out.write(b); + } + lastb = b; + } + + public void write(byte b[]) throws IOException { + write(b, 0, b.length); + } + + public void write(byte b[], int off, int len) throws IOException { + for (int i = 0 ; i < len ; i++) { + write(b[off + i]); + } + } +}
diff --git a/demo/src/main/java/README.txt b/demo/src/main/java/README.txt new file mode 100644 index 0000000..781d064 --- /dev/null +++ b/demo/src/main/java/README.txt
@@ -0,0 +1,384 @@ + README for the demo programs in this directory + ============================================== + +These demo programs illustrate how to use the JavaMail API to +perform a number of common email functions. Note these these +programs are not intended to be examples of good user interfaces, +or good command line interfaces. No one is expected to actually +*use* these programs for anything real. Rather, their value is +in the source code. Don't look at their command line arguments +or user interface to figure out what JavaMail can do, look at +their source code. We strongly recommend that you read the +source code and understand what these programs are doing before +running them. + +All of these programs are simple command line tools with a UNIX +style interface. On Windows you'll need to run them in an MS-DOS +window. We apologize in advance for the inconsistency in how these +programs accept options. There are generally two styles. The very +simple style (e.g., as used by copier.java) requires a fixed number +of arguments in a fixed order. Others (e.g., folderlist.java) take +UNIX-style options, many of which are optional, and which may appear +in any order. The following notes should help you figure it out, +but if in doubt, read the source code. + + + +- copier.java + + This program copies the specified messages from one folder to + another. Both folders must belong to the same store. + + Usage: + java copier <urlname> <src> <dest> <start> <end> + + Arguments (in order): + + <urlname> : URL of the Store. The URL should include + the password as well (if needed). + Example: "imap://john:password@mailstore.com" + + <src> : source folder + <dest> : destination folder + <start> : start message number + <end> : end message number + + +- folderlist.java + + This program lists information about the folders in a Store. + + Usage: + java folderlist -L <url> -T <protocol> -H <host> -U <user> -P <passwd> + [-R <root>] [-r] [-v] [-D] <pattern> + + Options: + + -L <url> : URL of the Store. The URL should include + the password as well (if needed). + Example: "imap://john:password@mailstore.com" + -T <protocol> : store protocol (Ex: "imap") + -H <host> : hostname of store. + -U <user> : username (if needed) + -P <passwd> : password (if needed) + -R <root> : root of the folder hierarchy. This is optional. If + not present, listing starts from the default folder. + -r : list recursively - folder and all subfolders. + -v : verbose - show more info about each folder. + -D : Turn on session debugging + <pattern> : folders that match this pattern are listed. Use "*" + as wildcard to match everything. + + +- monitor.java + + Illustrates how to monitor a folder for interesting events, + like new mail arrival. + + Usage: + java monitor <host> <user> <password> <mbox> <freq> + + Arguments (in order): + + <host> : hostname of store. + <user> : username (if needed) + <passwd> : password (if needed) + <mbox> : folder to monitor + <freq> : frequency of monitoring + + +- mover.java + + Moves messages between folders. The folders must belong to the + same store. + + Usage: + java mover -T <protocol> -H <host> -U <user> -P <passwd> [-v] + -s <src> -d <dest> [-x] <start> <end> + + Options: + + -T <protocol> : store protocol (Ex: "imap") + -H <host> : hostname of store. + -U <user> : username (if needed) + -P <passwd> : password (if needed) + -s <src> : source folder + -d <dest> : destination folder + -v : Optional verbose option + -x : Optional expunge option, to expunge the deleted + messages from src + + Arguments (in order): + + <start> : start message number + <end> : end message number + + +- msgmultisendsample.java + + Demonstrates how to construct and send a multipart message. + + Usage: + java msgmultisendsample <to> <from> <smtphost> true|false + + Arguments (in order): + + <to> : Recipient address + <from> : Sender address + <smtphost> : name of SMTP server + true|false : "true" to turn on session debugging, "false" otherwise + + +- msgsend.java + + Send a simple text message. Optionally saves a copy + of the outgoing message in a folder (record-folder). + + Most parameters to this program are optional. When + the program is run, it interactively asks for + the "To" and "Subject" fields if not already available. + Then the program expects the body of the message. + After you type in the body, hit Ctrl-D on Unix + systems or Ctrl-Z on Windows systems to send + the message. + + Usage: + java msgsend -L <store-url> -T <protocol> -H <host> -U <user> + -P <passwd> -s <subject> -o <from> -c <cc> -b <bcc> + -f <record> -M <smtphost> [-d] <to> + + Options: + + -L <store-url> : URL of the store for the record-folder + -T <protocol> : If <store-url> is not present, this indicates + the store protocol for the record-folder. + -H <host> : If <store-url> is not present, this indicates + the hostname for the record-folder. + -U <user> : If <store-url> is not present, this indicates + the username for the record-folder. + -P <passwd> : If <store-url> is not present, this indicates + the password for the record-folder. + -f <record> : name of record-folder. + -M <smtphost> : Host name of SMTP server. Defaults to "localhost" + which often works on UNIX but rarely on Windows. + -s <subject> : Subject of message to be sent + -o <from> : From address of message to be sent + -c <cc> : Cc address of message to be sent + -b <bcc> : Bcc address of message to be sent + -d : Turn on session debugging. + -a <file> : Include file as an attachment with the message + + Argument: + + <to> : To address of message to be sent + + +- msgsendsample.java + + Demonstrates how to construct and send a simple text message. + + Usage: + java msgsendsample <to> <from> <smtphost> true|false + + Arguments (in order): + + <to> : Recipient address + <from> : Sender address + <smtphost> : name of SMTP server + true|false : "true" to turn on session debugging, "false" otherwise + + +- msgshow.java + + Displays message(s) from a folder or from stdin. + + Usage: + java msgshow -L <url> -T <protocol> -H <host> -p <port> + -U <user> -P <password> -f <mailbox> + [-D] [-s] [-S] [-a] [-v] [msgnum] + java msgshow -m [-D] [-s] [-S] [-v] + + Options: + + -L <url> : URL of the Store. The URL should include + the password as well (if needed). + Example: "imap://john:password@mailstore.com" + -T <protocol> : If <url> is not present, this indicates + the store protocol + -H <host> : If <url> is not present, this indicates + the hostname + -p <port> : If <url> is not present, this indicates + the port number (usually not needed) + -U <user> : If <url> is not present, this indicates + the username + -P <passwd> : If <url> is not present, this indicates + the password + -f <mailbox> : Folder to open + -m : Read message from standard input + -D : Turn on session debugging + -s : Show the structure of the message, but not the contents + -S : Save attachments to appropriately named files + -a : Show ALERTS and NOTIFICATIONS from the Store + -v : Verbose mode - show total messages and number of new messages + + Argument: + + <msgnum> : the message to be displayed. If this + parameter is not present, all messages in the + folder are displayed. + + +- namespace.java + + Displays the namespaces supported by a store. + + Usage: + java namespace -L <url> -T <protocol> -H <host> -p <port> + -U <user> -P <password> [-D] + + Options: + + -L <url> : URL of the Store. The URL should include + the password as well (if needed). + Example: "imap://john:password@mailstore.com" + -T <protocol> : If <url> is not present, this indicates + the store protocol + -H <host> : If <url> is not present, this indicates + the hostname + -p <port> : If <url> is not present, this indicates + the port number (usually not needed) + -U <user> : If <url> is not present, this indicates + the username + -P <passwd> : If <url> is not present, this indicates + the password + -D : Turn on session debugging + + +- populate.java + + Copies an entire folder hierarchy from one message store to + another. + + Usage: + java populate -s <src-url> -d <dest-url> -D -f + + Options: + + -s <src-url> : URL of source folder + -d <dest-url> : URL of destination folder + -D : Turn on session debugging + -f : force the copy to occur even if the destination + folder already exists + -S : skip special folders named "SCCS", "Drafts", "Trash", and + "Shared Folders" + -c : clear out old folders before copying messages + -P : don't preserve flags when copying messages + + +- registry.java + + Demonstrates how to query the JavaMail "registry" for providers, + set default providers, etc. + + Usage: + java registry + + +- search.java + + Search the given folder for messages matching the + given criteria. Illustrates the use of the + javax.mail.search package. + + Usage: + java search -L <url> -T <prot> -H <host> -U <user> -P <passwd> + -f <folder> -subject <subject> -from <from> + -today -or + + Options: + + -L <url> : URL of the store + -T <protocol> : If <url> is not present, this indicates + the store protocol + -H <host> : If <url> is not present, this indicates + the hostname + -U <user> : If <url> is not present, this indicates + the username + -P <passwd> : If <url> is not present, this indicates + the password + -f <folder> : folder to search + + -or : If this flag is present, the search will + return messages that match any one of the + below criteria. Else the search will only + return messages that match all the criteria + + -subject <subject> : search for messages containing this string + as the Subject + -from <from> : search for messages containing this string + as the From address + -today : search for messages received today + + +- sendfile.java + + Send the specified file to the given address. The file + is sent as an attachment. An SMTP server must be available. + + Usage: + java sendfile <to> <from> <smtphost> <file> true|false + + Arguments (in order): + + <to> : Recipient address + <from> : Sender address + <smtphost> : name of SMTP server + <file> : name of file to be sent + true|false : "true" to turn on session debugging, "false" otherwise + + +- sendhtml.java + + The sendhtml program works like the msgsend program, taking + the same options and input, but the text collected from the + user is sent as type "text/html" instead of "text/plain". + + This program is a good example of how to send arbitrary + string data as any arbitrary MIME type. + + +- smtpsend.java + + Takes the same options as the msgsend program, but illustrates + how to handle SMTP-specific error codes. Also accepts the + following options: + + Option: + + -v : verbose output + -A : use SMTP authentication + -S : use SSL + +- transport.java + + Illustrates how to use an explicit Transport object, how to + handle transport exceptions, and how to handle transport events. + + Usage: + java transport <to> <from> <smtphost> <file> true|false + + Arguments (in order): + + <to> : Recipient address + <from> : Sender address + <smtphost> : name of SMTP server + <file> : name of file to be sent + true|false : "true" to turn on session debugging, "false" otherwise + + +- uidmsgshow.java + + The uidmsgshow program works like the msgshow program, taking + the same options, except instead of using message numbers, it + uses message UID's. This will typically only work with IMAP + message stores.
diff --git a/demo/src/main/java/copier.java b/demo/src/main/java/copier.java new file mode 100644 index 0000000..0bce3f7 --- /dev/null +++ b/demo/src/main/java/copier.java
@@ -0,0 +1,90 @@ +/* + * Copyright (c) 1996, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +/** + * + * @author Christopher Cotton + */ + +import javax.mail.*; + +/** + * copier will copy a specified number of messages from one folder + * to another folder. it demonstrates how to use the JavaMail APIs + * to copy messages.<p> + * + * Usage for copier: copier <i>store urlname</i> + * <i>src folder</i> <i>dest folder</i> <i>start msg #</i> <i>end msg #</i><p> + * + */ + +public class copier { + + public static void main(String argv[]) { + boolean debug = false; // change to get more errors + + if (argv.length != 5) { + System.out.println( "usage: copier <urlname> <src folder>" + + "<dest folder> <start msg #> <end msg #>"); + return; + } + + try { + URLName url = new URLName(argv[0]); + String src = argv[1]; // source folder + String dest = argv[2]; // dest folder + int start = Integer.parseInt(argv[3]); // copy from message # + int end = Integer.parseInt(argv[4]); // to message # + + // Get the default Session object + + Session session = Session.getInstance(System.getProperties(), null); + // session.setDebug(debug); + + // Get a Store object that implements the protocol. + Store store = session.getStore(url); + store.connect(); + System.out.println("Connected..."); + + // Open Source Folder + Folder folder = store.getFolder(src); + folder.open(Folder.READ_WRITE); + System.out.println("Opened source..."); + + if (folder.getMessageCount() == 0) { + System.out.println("Source folder has no messages .."); + folder.close(false); + store.close(); + } + + // Open destination folder, create if needed + Folder dfolder = store.getFolder(dest); + if (!dfolder.exists()) // create + dfolder.create(Folder.HOLDS_MESSAGES); + + Message[] msgs = folder.getMessages(start, end); + System.out.println("Got messages..."); + + // Copy messages into destination, + folder.copyMessages(msgs, dfolder); + System.out.println("Copied messages..."); + + // Close the folder and store + folder.close(false); + store.close(); + System.out.println("Closed folder and store..."); + + } catch (Exception e) { + e.printStackTrace(); + } + + System.exit(0); + } +}
diff --git a/demo/src/main/java/folderlist.java b/demo/src/main/java/folderlist.java new file mode 100644 index 0000000..ae2285f --- /dev/null +++ b/demo/src/main/java/folderlist.java
@@ -0,0 +1,164 @@ +/* + * Copyright (c) 1996, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +import java.util.Properties; +import javax.mail.*; + +import com.sun.mail.imap.*; + +/** + * Demo app that exercises the Message interfaces. + * List information about folders. + * + * @author John Mani + * @author Bill Shannon + */ + +public class folderlist { + static String protocol = null; + static String host = null; + static String user = null; + static String password = null; + static String url = null; + static String root = null; + static String pattern = "%"; + static boolean recursive = false; + static boolean verbose = false; + static boolean debug = false; + + public static void main(String argv[]) throws Exception { + int optind; + for (optind = 0; optind < argv.length; optind++) { + if (argv[optind].equals("-T")) { + protocol = argv[++optind]; + } else if (argv[optind].equals("-H")) { + host = argv[++optind]; + } else if (argv[optind].equals("-U")) { + user = argv[++optind]; + } else if (argv[optind].equals("-P")) { + password = argv[++optind]; + } else if (argv[optind].equals("-L")) { + url = argv[++optind]; + } else if (argv[optind].equals("-R")) { + root = argv[++optind]; + } else if (argv[optind].equals("-r")) { + recursive = true; + } else if (argv[optind].equals("-v")) { + verbose = true; + } else if (argv[optind].equals("-D")) { + debug = true; + } else if (argv[optind].equals("--")) { + optind++; + break; + } else if (argv[optind].startsWith("-")) { + System.out.println( +"Usage: folderlist [-T protocol] [-H host] [-U user] [-P password] [-L url]"); + System.out.println( +"\t[-R root] [-r] [-v] [-D] [pattern]"); + System.exit(1); + } else { + break; + } + } + if (optind < argv.length) + pattern = argv[optind]; + + // Get a Properties object + Properties props = System.getProperties(); + + // Get a Session object + Session session = Session.getInstance(props, null); + session.setDebug(debug); + + // Get a Store object + Store store = null; + Folder rf = null; + if (url != null) { + URLName urln = new URLName(url); + store = session.getStore(urln); + store.connect(); + } else { + if (protocol != null) + store = session.getStore(protocol); + else + store = session.getStore(); + + // Connect + if (host != null || user != null || password != null) + store.connect(host, user, password); + else + store.connect(); + } + + // List namespace + if (root != null) + rf = store.getFolder(root); + else + rf = store.getDefaultFolder(); + + dumpFolder(rf, false, ""); + if ((rf.getType() & Folder.HOLDS_FOLDERS) != 0) { + Folder[] f = rf.list(pattern); + for (int i = 0; i < f.length; i++) + dumpFolder(f[i], recursive, " "); + } + + store.close(); + } + + static void dumpFolder(Folder folder, boolean recurse, String tab) + throws Exception { + System.out.println(tab + "Name: " + folder.getName()); + System.out.println(tab + "Full Name: " + folder.getFullName()); + System.out.println(tab + "URL: " + folder.getURLName()); + + if (verbose) { + if (!folder.isSubscribed()) + System.out.println(tab + "Not Subscribed"); + + if ((folder.getType() & Folder.HOLDS_MESSAGES) != 0) { + if (folder.hasNewMessages()) + System.out.println(tab + "Has New Messages"); + System.out.println(tab + "Total Messages: " + + folder.getMessageCount()); + System.out.println(tab + "New Messages: " + + folder.getNewMessageCount()); + System.out.println(tab + "Unread Messages: " + + folder.getUnreadMessageCount()); + } + if ((folder.getType() & Folder.HOLDS_FOLDERS) != 0) + System.out.println(tab + "Is Directory"); + + /* + * Demonstrate use of IMAP folder attributes + * returned by the IMAP LIST response. + */ + if (folder instanceof IMAPFolder) { + IMAPFolder f = (IMAPFolder)folder; + String[] attrs = f.getAttributes(); + if (attrs != null && attrs.length > 0) { + System.out.println(tab + "IMAP Attributes:"); + for (int i = 0; i < attrs.length; i++) + System.out.println(tab + " " + attrs[i]); + } + } + } + + System.out.println(); + + if ((folder.getType() & Folder.HOLDS_FOLDERS) != 0) { + if (recurse) { + Folder[] f = folder.list(); + for (int i = 0; i < f.length; i++) + dumpFolder(f[i], recurse, tab + " "); + } + } + } +}
diff --git a/demo/src/main/java/internal/TtyAuthenticator.java b/demo/src/main/java/internal/TtyAuthenticator.java new file mode 100644 index 0000000..ead5a32 --- /dev/null +++ b/demo/src/main/java/internal/TtyAuthenticator.java
@@ -0,0 +1,112 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +import java.io.*; +import java.net.*; +import javax.mail.*; +import javax.mail.PasswordAuthentication; +import javax.mail.Authenticator; + +/** + * A simple Authenticator that prompts for the user name and password on stdin. + * Puts up a dialog something like: + * <p> <pre> + * Connecting to <protocol> mail service on host <addr>, port <port>. + * <prompt> + * + * User Name: [defaultUserName] + * Password: + * </pre> <p> + * + * @author Bill Shannon + */ + +public class TtyAuthenticator extends Authenticator { + + /** + * @return The PasswordAuthentication collected from the + * user, or null if none is provided. + */ + protected PasswordAuthentication getPasswordAuthentication() { + BufferedReader in = new BufferedReader( + new InputStreamReader((System.in))); + StringBuffer sb = new StringBuffer(); + sb.append("Connecting to "); + sb.append(getRequestingProtocol()); + sb.append(" mail service on host "); + sb.append(getRequestingSite().getHostName()); + int port = getRequestingPort(); + if (port > 0) { + sb.append(", port "); + sb.append(port); + } + sb.append("."); + System.out.println(sb.toString()); + String prompt = getRequestingPrompt(); + if (prompt != null) + System.out.println(prompt); + System.out.println(); + String userName = get(in, "User Name", getDefaultUserName()); + String password = getpw("Password"); + if (userName == null) + return null; + else + return new PasswordAuthentication(userName, password); + } + + private static final String get(BufferedReader in, + String name, String value) { + PrintStream p = System.out; + + p.print(name + ": "); + if (value != null) + p.print("[" + value + "] "); + p.flush(); + + try { + String s = in.readLine(); + if (s.length() == 0) + return value; + else + return s; + } catch (IOException e) { + return value; + } + } + + private static final String getpw(String name) { + Console cons; + char[] passwd; + if ((cons = System.console()) != null && + (passwd = cons.readPassword("[%s] ", name)) != null) + return new String(passwd); + return ""; + } + + // main program, for debugging. + // Usage: java TtyAuthenticator host port protocol prompt defaultUser + public static void main(String argv[]) throws Exception { + Session sess = Session.getInstance(System.getProperties(), + new TtyAuthenticator()); + PasswordAuthentication pw = sess.requestPasswordAuthentication( + InetAddress.getByName(argv[0]), + Integer.parseInt(argv[1]), argv[2], z(argv[3]), z(argv[4])); + System.out.println("User: " + n(pw.getUserName())); + System.out.println("Password: " + n(pw.getPassword())); + } + + private static final String n(String s) { + return s == null ? "<null>" : s; + } + + private static final String z(String s) { + return s.length() > 0 ? s : null; + } +}
diff --git a/demo/src/main/java/internal/answer.java b/demo/src/main/java/internal/answer.java new file mode 100644 index 0000000..e7f8963 --- /dev/null +++ b/demo/src/main/java/internal/answer.java
@@ -0,0 +1,355 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +import java.util.*; +import java.io.*; +import javax.mail.*; +import javax.mail.search.*; +import javax.mail.internet.*; + +/** + * Program to manage the javamail@Sun.COM mailing list by keeping track + * of which messages have been answered. Message that have been answered + * have the ANSWERED flag set. Messages that are replies to other messages + * have the FLAGGED flag set. <p> + * + * Note that this program operates on the log file directly and thus all + * access to the log file using this program must be through the same + * IMAP server so that the flags are handled consistently. <p> + * + * When run with no message number arguments it will list the unanswered + * messages. The -u flag will cause it to first update the mailbox to + * account for any recent replies. <p> + * + * When run with message number arguments it will mark those messages as + * answered. This is useful for marking spam and other random messages + * that will never be replied to. <p> + * + * The -a flag will cause it to mark all messages as answered. Useful for + * flushing the ever accumulating spam. <p> + * + * @author Bill Shannon + */ + +public class answer { + + static String protocol = "imap"; + static String host = "anybodys.sfbay"; + static String user = "javamail"; + static String password = "1javamail1"; + static String mbox = + "/net/anybodys.sfbay/export6/javamail/logs/javamail.log"; + static String url = null; + static boolean verbose = false; + static boolean update = false; + static boolean markAll = false; + + public static void main(String argv[]) { + int optind; + + for (optind = 0; optind < argv.length; optind++) { + if (argv[optind].equals("-T")) { + protocol = argv[++optind]; + } else if (argv[optind].equals("-H")) { + host = argv[++optind]; + } else if (argv[optind].equals("-U")) { + user = argv[++optind]; + } else if (argv[optind].equals("-P")) { + password = argv[++optind]; + } else if (argv[optind].equals("-v")) { + verbose = true; + } else if (argv[optind].equals("-f")) { + mbox = argv[++optind]; + } else if (argv[optind].equals("-L")) { + url = argv[++optind]; + } else if (argv[optind].equals("-u")) { + update = true; + } else if (argv[optind].equals("-a")) { + markAll = true; + update = true; + } else if (argv[optind].equals("--")) { + optind++; + break; + } else if (argv[optind].startsWith("-")) { + System.out.println( +"Usage: answer [-L url] [-T protocol] [-H host] [-U user] [-P password]\n" + +"\t[-f mailbox] [-v] [-u] [-a] [msgno ...]"); + System.exit(1); + } else { + break; + } + } + + try { + // Get a Properties object + Properties props = System.getProperties(); + + // Get a Session object + Session session = Session.getDefaultInstance(props, + new TtyAuthenticator()); + + // Get a Store object + Store store = null; + if (url != null) { + URLName urln = new URLName(url); + store = session.getStore(urln); + store.connect(); + } else { + if (protocol != null) + store = session.getStore(protocol); + else + store = session.getStore(); + + // Connect + if (host != null || user != null || password != null) + store.connect(host, user, password); + else + store.connect(); + } + + + // Open the Folder + + Folder folder = store.getDefaultFolder(); + if (folder == null) { + System.out.println("No default folder"); + System.exit(1); + } + + folder = folder.getFolder(mbox); + if (folder == null) { + System.out.println("Invalid folder"); + System.exit(1); + } + + if (optind < argv.length || update) + folder.open(Folder.READ_WRITE); + else + folder.open(Folder.READ_ONLY); + int totalMessages = folder.getMessageCount(); + + if (totalMessages == 0) { + System.out.println("Empty folder"); + folder.close(false); + store.close(); + System.exit(1); + } + + if (verbose) { + int newMessages = folder.getNewMessageCount(); + pv("Total messages = " + totalMessages); + pv("New messages = " + newMessages); + pv("-------------------------------"); + } + + if (optind >= argv.length) { + // get all messages that aren't answered or flagged + Flags f = new Flags(); + f.add(Flags.Flag.ANSWERED); + f.add(Flags.Flag.FLAGGED); + + // Use a suitable FetchProfile + FetchProfile fp = new FetchProfile(); + fp.add(FetchProfile.Item.ENVELOPE); + fp.add(FetchProfile.Item.FLAGS); + + Message[] msgs = folder.search(new FlagTerm(f, false)); + folder.fetch(msgs, fp); + + if (update) { + doUpdate(msgs); + // re-fetch messages + msgs = folder.search(new FlagTerm(f, false)); + pv(""); + pv(""); + } + + System.out.println("Unanswered messages:"); + for (int i = 0; i < msgs.length; i++) { + Message msg = msgs[i]; + System.out.println("--------------------------"); + System.out.println("MESSAGE #" + + msg.getMessageNumber() + ":"); + Question q = new Question(msg); + System.out.println(q); + if (q.isReply()) + System.out.println( + "A reply to a message we haven't seen"); + if (markAll) { + int msgno = msg.getMessageNumber(); + if (q.isReply()) { + pv("Flagging message #" + msgno); + msg.setFlag(Flags.Flag.FLAGGED, true); + } else { + pv("Answering message #" + msgno); + msg.setFlag(Flags.Flag.ANSWERED, true); + } + } + } + + } else { + for (int i = optind; i < argv.length; i++) { + int msgno = Integer.parseInt(argv[i]); + Message msg = folder.getMessage(msgno); + Question q = new Question(msg); + if (q.isReply()) { + pv("Flagging message #" + msgno); + msg.setFlag(Flags.Flag.FLAGGED, true); + } else { + pv("Answering message #" + msgno); + msg.setFlag(Flags.Flag.ANSWERED, true); + } + } + } + + folder.close(false); + store.close(); + } catch (Exception ex) { + System.out.println("Oops, got exception! " + ex.getMessage()); + ex.printStackTrace(); + System.exit(1); + } + System.exit(0); + } + + /** + * Update the mailbox to account for any replies. + */ + static void doUpdate(Message[] msgs) throws MessagingException { + // a hash table of all the unanswered questions + Hashtable qt = new Hashtable(); + + Address javamailAddress = new InternetAddress("javamail@Sun.COM"); + SearchTerm javamail = new OrTerm( + new RecipientTerm(Message.RecipientType.TO, javamailAddress), + new RecipientTerm(Message.RecipientType.CC, javamailAddress)); + + for (int i = 0; i < msgs.length; i++) { + Message msg = msgs[i]; + pv("--------------------------"); + pv("MESSAGE #" + msg.getMessageNumber() + ":"); + Question q = new Question(msg); + pv(q.toString()); + Question q0; + if ((q0 = (Question)qt.get(q)) != null) { + pv("Found in table"); + if (q.isReply()) { + Message qm = q0.getMessage(); + if (!qm.isSet(Flags.Flag.ANSWERED)) { + pv("Answered:"); + pv(q0.toString()); + qm.setFlag(Flags.Flag.ANSWERED, true); + } + pv("is reply, flagging it"); + msg.setFlag(Flags.Flag.FLAGGED, true); + } else { + // a second copy of a message that's not a reply? + // shouldn't happen. + } + } else { + pv("NOT found in table"); + if (q.isReply()) { + // a reply to a message we haven't seen? + pv("flagging, but no original msg"); + msg.setFlag(Flags.Flag.FLAGGED, true); + } else { + // an original question, put it in the table + qt.put(q, q); + // if the message looks like spam, flag it + if (!javamail.match(msg)) { + pv("SPAM!!!"); + msg.setFlag(Flags.Flag.FLAGGED, true); + } + } + } + } + } + + /** + * Print verbose. + */ + static void pv(String s) { + if (verbose) + System.out.println(s); + } +} + +/** + * This class represents a single "question" sent to javamail@Sun.COM, + * or possibly a reply to such a question. + */ +class Question { + Message msg; + Address sender; + Address[] recipients; + String subject; + Date date; + boolean reply = false; + + Question(Message msg) throws MessagingException { + this.msg = msg; + sender = msg.getReplyTo()[0]; // XXX - assume only one + subject = msg.getSubject(); + if (subject == null) + subject = ""; + else if (subject.regionMatches(true, 0, "Re: ", 0, 4)) { + subject = subject.substring(4); + reply = true; + recipients = msg.getRecipients(Message.RecipientType.TO); + } + subject = subject.trim(); + date = msg.getSentDate(); + if (date == null) + date = msg.getReceivedDate(); + } + + public boolean isReply() { + return reply; + } + + public Message getMessage() { + return msg; + } + + public int hashCode() { + return subject.hashCode(); + } + + public boolean equals(Object obj) { + if (!(obj instanceof Question)) + return false; + Question q = (Question)obj; + if (this.reply == q.reply) + return this.sender.equals(q.sender) && + this.subject.equals(q.subject); + + /* + * A reply is equal to a question if the sender of the question + * is one of the recipients of the reply. + */ + Question qq, qr; + if (this.reply) { + qq = q; + qr = this; + } else { + qq = this; + qr = q; + } + for (int i = 0; i < qr.recipients.length; i++) { + if (qq.sender.equals(qr.recipients[i])) + return true; + } + return false; + } + + public String toString() { + return "Date: " + date + "\nFrom: " + sender + "\nSubject: " + subject; + } +}
diff --git a/demo/src/main/java/internal/foldersplit.java b/demo/src/main/java/internal/foldersplit.java new file mode 100644 index 0000000..c9e4445 --- /dev/null +++ b/demo/src/main/java/internal/foldersplit.java
@@ -0,0 +1,187 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +import java.util.*; +import java.text.*; +import javax.mail.*; + +/** + * + * Split mail folders according to date of messages. + * + * @author Bill Shannon + */ + +public class foldersplit { + + static String protocol; + static String host = null; + static String user = null; + static String password = null; + static String dst = null; + static String url = null; + static int port = -1; + static boolean verbose = false; + static boolean debug = false; + static boolean nop = false; + static SimpleDateFormat df = new SimpleDateFormat("yyyy.MM"); + + public static void main(String argv[]) { + int optind; + + for (optind = 0; optind < argv.length; optind++) { + if (argv[optind].equals("-T")) { + protocol = argv[++optind]; + } else if (argv[optind].equals("-H")) { + host = argv[++optind]; + } else if (argv[optind].equals("-U")) { + user = argv[++optind]; + } else if (argv[optind].equals("-P")) { + password = argv[++optind]; + } else if (argv[optind].equals("-d")) { + dst = argv[++optind]; + } else if (argv[optind].equals("-v")) { + verbose = true; + } else if (argv[optind].equals("-n")) { + nop = true; + } else if (argv[optind].equals("-D")) { + debug = true; + } else if (argv[optind].equals("-L")) { + url = argv[++optind]; + } else if (argv[optind].equals("-p")) { + port = Integer.parseInt(argv[++optind]); + } else if (argv[optind].equals("--")) { + optind++; + break; + } else if (argv[optind].startsWith("-")) { + System.out.println( +"Usage: foldersplit [-L url] [-T protocol] [-H host] [-p port] [-U user]"); + System.out.println( +"\t[-P password] [-v] [-D] folder ..."); + System.exit(1); + } else { + break; + } + } + + try { + // Get a Properties object + Properties props = System.getProperties(); + + // Get a Session object + Session session = Session.getDefaultInstance(props, null); + session.setDebug(debug); + + // Get a Store object + Store store = null; + if (url != null) { + URLName urln = new URLName(url); + store = session.getStore(urln); + store.connect(); + } else { + if (protocol != null) + store = session.getStore(protocol); + else + store = session.getStore(); + + // Connect + if (host != null || user != null || password != null) + store.connect(host, port, user, password); + else + store.connect(); + } + + + // Open the Folder + + Folder deffolder = store.getDefaultFolder(); + if (deffolder == null) { + System.out.println("No default folder"); + System.exit(1); + } + Folder dstfolder; + if (dst == null) { + dstfolder = deffolder; + } else { + dstfolder = deffolder.getFolder(dst); + if (dstfolder == null) { + System.out.println("No destination folder"); + System.exit(1); + } + } + + Folder outf = null; + for (; optind < argv.length; optind++) { + Folder folder = deffolder.getFolder(argv[optind]); + if (folder == null) { + System.out.println("Invalid folder: " + argv[optind]); + continue; + } + System.out.println("Folder: " + folder.getFullName()); + + folder.open(Folder.READ_ONLY); + int totalMessages = folder.getMessageCount(); + + if (totalMessages == 0) { + System.out.println("Empty folder"); + folder.close(false); + continue; + } + + if (verbose) { + int newMessages = folder.getNewMessageCount(); + System.out.println("Total messages = " + totalMessages); + System.out.println("New messages = " + newMessages); + System.out.println("-------------------------------"); + } + + // Attributes & Flags for all messages .. + Message[] msgs = folder.getMessages(); + + // Use a suitable FetchProfile + FetchProfile fp = new FetchProfile(); + fp.add(FetchProfile.Item.ENVELOPE); + fp.add(FetchProfile.Item.FLAGS); + folder.fetch(msgs, fp); + + for (int i = 0; i < msgs.length; i++) { + if (verbose) { + System.out.println("--------------------------"); + System.out.println("MESSAGE #" + (i + 1) + ":"); + } + Date d = msgs[i].getReceivedDate(); + if (d != null) { + String n = df.format(d); + if (verbose) + System.out.println(n); + if (outf == null || !n.equals(outf.getName())) { + outf = dstfolder.getFolder(n); + if (!outf.exists()) { + System.out.println("Creating: " + + outf.getFullName()); + if (!nop) + outf.create(Folder.HOLDS_MESSAGES); + } + } + } + if (!nop) + outf.appendMessages(new Message[] { msgs[i] }); + } + folder.close(false); + } + + store.close(); + } catch (Exception ex) { + System.out.println("Oops, got exception! " + ex.getMessage()); + ex.printStackTrace(); + } + System.exit(1); + } +}
diff --git a/demo/src/main/java/internal/fpopulate.java b/demo/src/main/java/internal/fpopulate.java new file mode 100644 index 0000000..8333213 --- /dev/null +++ b/demo/src/main/java/internal/fpopulate.java
@@ -0,0 +1,207 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +import java.io.*; +import javax.mail.*; +import javax.mail.internet.*; + +/* + * Copy folder hierarchies between files and a Store. This is a useful + * utility to populate new (and possibly empty) mail stores. Specify + * the source as a directory name and the destination folders as a URL. + * + * @author John Mani + * @author Bill Shannon + */ + +public class fpopulate { + + static boolean force = false; + static boolean skipSCCS = false; + static boolean clear = false; + + static Session session; + + public static void main(String argv[]) { + String srcdir = null; + String dstURL = null; + boolean debug = false; + + int optind; + + for (optind = 0; optind < argv.length; optind++) { + if (argv[optind].equals("-s")) { + srcdir = argv[++optind]; + } else if (argv[optind].equals("-d")) { + dstURL = argv[++optind]; + } else if (argv[optind].equals("-D")) { + debug = true; + } else if (argv[optind].equals("-f")) { + force = true; + } else if (argv[optind].equals("-S")) { + skipSCCS = true; + } else if (argv[optind].equals("-c")) { + clear = true; + } else if (argv[optind].equals("--")) { + optind++; + break; + } else if (argv[optind].startsWith("-")) { + printUsage(); + System.exit(1); + } else { + break; + } + } + + try { + + if (srcdir == null || dstURL == null) { + printUsage(); + System.exit(1); + } + + session = Session.getDefaultInstance( + System.getProperties(), null); + session.setDebug(debug); + + // Get source folder + File srcFolder = new File(srcdir); + if (!srcFolder.exists()) { + System.out.println("source folder does not exist"); + System.exit(1); + } + + // Set up destination folder + URLName dstURLName = new URLName(dstURL); + Folder dstFolder; + // Check if the destination URL has a folder specified. If + // not, we use the source folder name + if (dstURLName.getFile() == null) { + Store s = session.getStore(dstURLName); + s.connect(); + dstFolder = s.getFolder(srcFolder.getName()); + } else + dstFolder = session.getFolder(new URLName(dstURL)); + + if (clear && dstFolder.exists()) { + if (!dstFolder.delete(true)) { + System.out.println("couldn't delete " + + dstFolder.getFullName()); + return; + } + } + copy(srcFolder, dstFolder); + + // Close the respective stores. + dstFolder.getStore().close(); + + } catch (MessagingException mex) { + System.out.println(mex.getMessage()); + mex.printStackTrace(); + } catch (IOException ioex) { + System.out.println(ioex.getMessage()); + ioex.printStackTrace(); + } + } + + private static void copy(File src, Folder dst) + throws MessagingException, IOException { + System.out.println("Populating " + dst.getFullName()); + + if (!dst.exists()) { + // Create it. + int type = holdsMessages(src) ? + Folder.HOLDS_MESSAGES : Folder.HOLDS_FOLDERS; + if (!dst.create(type)) { + System.out.println("couldn't create " + dst.getFullName()); + return; + } + + // Copy over any messages from src to dst + if (holdsMessages(src)) + copyMessages(src, dst); + } else { + System.out.println(dst.getFullName() + " already exists"); + // Copy over any messges from src to dst + if (force && holdsMessages(src)) + copyMessages(src, dst); + } + + // Copy over subfolders + if (holdsFolders(src)) { + String[] sf = src.list(); + for (int i = 0; sf != null && i < sf.length; i++) { + // skip SCCS directories? + if (skipSCCS && sf[i].equals("SCCS")) + continue; + File f = new File(src, sf[i]); + if (f.isDirectory()) + copy(f, dst.getFolder(sf[i])); + } + } + } + + /** + * Does this directory hold messages? + * Return true if there's at least one message. + */ + private static boolean holdsMessages(File f) { + File msg = new File(f, "1"); + return msg.exists(); + } + + private static boolean holdsFolders(File f) { + return !holdsMessages(f); // XXX - hack for now + } + + /** + * Copy message files from the source directory to the + * destination folder. Message files must be named "1", + * "2", etc. The first missing number terminates the + * copy. + */ + private static void copyMessages(File src, Folder dst) + throws MessagingException, IOException { + System.out.println(" Copy from " + src + " to " + dst); + int msgnum = 1; + Message[] msgs = new Message[1]; + for (;;) { + File f = new File(src, String.valueOf(msgnum)); + if (!f.exists()) // break when we find a message missing + break; + FileInputStream fis = new FileInputStream(f); + BufferedInputStream is = new BufferedInputStream(fis); + is.mark(1024); + DataInputStream dis = new DataInputStream(is); + String line = dis.readLine(); + /* + * If it's in UNIX mbox format, we skip the first line, + * otherwise we start reading at the beginning. + */ + if (!line.startsWith("From ")) + is.reset(); + MimeMessage msg = new MimeMessage(session, is); + fis.close(); + msgs[0] = msg; + dst.appendMessages(msgs); + msgnum++; + } + System.out.println(" Copied " + (msgnum - 1) + " messages"); + } + + private static void printUsage() { + System.out.println("fpopulate [-D] [-f] [-S] [-c] " + + "-s source_dir -d dest_url"); + System.out.println("URLs are of the form: " + + "protocol://username:password@hostname/foldername"); + System.out.println("The destination URL does not need a foldername," + + " in which case, the source foldername is used"); + } +}
diff --git a/demo/src/main/java/internal/msgsperweek.java b/demo/src/main/java/internal/msgsperweek.java new file mode 100644 index 0000000..7105b97 --- /dev/null +++ b/demo/src/main/java/internal/msgsperweek.java
@@ -0,0 +1,251 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +import java.util.*; +import java.text.*; +import java.io.*; +import javax.mail.*; +import javax.mail.search.*; +import javax.mail.internet.*; + +/** + * Program that generates stats about new messages received per week + * by the javamail@Sun.COM mailing list. It tracks *new* messages only, + * the rule being that the message has a single addressee: + * "javamail@Sun.COM". Also searches each message body for the "pop3" + * string and counts its occurrence. + * + * Note that this program operates on the log file directly and thus all + * access to the log file using this program must be through the same + * IMAP server so that the flags are handled consistently. + * + * @author Max Spivak + */ + +public class msgsperweek { + + static String protocol = "imap"; + static String host = "anybodys.sfbay"; + static String user = "javamail"; + static String password = "1javamail1"; + static String mbox = + "/net/anybodys.sfbay/export6/javamail/logs/javamail.log"; + static String url = null; + static boolean verbose = false; + static boolean doPop3 = false; + static Calendar cal = null; + + public static void main(String argv[]) { + int optind; + + for (optind = 0; optind < argv.length; optind++) { + if (argv[optind].equals("-T")) { + protocol = argv[++optind]; + } else if (argv[optind].equals("-H")) { + host = argv[++optind]; + } else if (argv[optind].equals("-U")) { + user = argv[++optind]; + } else if (argv[optind].equals("-P")) { + password = argv[++optind]; + } else if (argv[optind].equals("-v")) { + verbose = true; + } else if (argv[optind].equals("-f")) { + mbox = argv[++optind]; + } else if (argv[optind].equals("-L")) { + url = argv[++optind]; + } else if (argv[optind].equals("-p")) { + doPop3 = true; + } else if (argv[optind].equals("--")) { + optind++; + break; + } else if (argv[optind].startsWith("-")) { + System.out.println( +"Usage: msgperweek [-L url] [-T protocol] [-H host] [-U user] [-P password]\n"+ +"\t[-f mailbox] [-v] [-p]"); + System.exit(1); + } else { + break; + } + } + + System.out.println("msgsperweek: Generating stats, please wait..."); + + cal = Calendar.getInstance(); + + try { + // Get a Properties object + Properties props = System.getProperties(); + + // Get a Session object + Session session = Session.getDefaultInstance(props, + new TtyAuthenticator()); + + //if (verbose) + //session.setDebug(true); + + // Get a Store object + Store store = null; + if (url != null) { + URLName urln = new URLName(url); + store = session.getStore(urln); + store.connect(); + } else { + if (protocol != null) + store = session.getStore(protocol); + else + store = session.getStore(); + + // Connect + if (host != null || user != null || password != null) + store.connect(host, user, password); + else + store.connect(); + } + + + // Open the Folder + + Folder folder = store.getDefaultFolder(); + if (folder == null) { + System.out.println("No default folder"); + System.exit(1); + } + + folder = folder.getFolder(mbox); + if (folder == null) { + System.out.println("Invalid folder"); + System.exit(1); + } + + folder.open(Folder.READ_ONLY); + int totalMessages = folder.getMessageCount(); + + if (totalMessages == 0) { + System.out.println("Empty folder"); + folder.close(false); + store.close(); + System.exit(1); + } + + if (verbose) { + int newMessages = folder.getNewMessageCount(); + pv("Total messages = " + totalMessages); + pv("New messages = " + newMessages); + pv("-------------------------------"); + } + + // Use a suitable FetchProfile + FetchProfile fp = new FetchProfile(); + fp.add(FetchProfile.Item.ENVELOPE); + + SearchTerm term = new RecipientTerm(Message.RecipientType.TO, + new InternetAddress("javamail@sun.com")); + Message[] msgs = folder.search(term); + folder.fetch(msgs, fp); + + Hashtable wks = new Hashtable(); + int totalCount = 0; + BodyTerm pop3Search = new BodyTerm("pop3"); + int pop3Requests = 0; + + // go through each msg and count it if it's an incoming msg + for (int i = 0; i < msgs.length; i++) { + // make sure we only have a single addressee: + // javamail@sun.com, which means it's a new message + // from outside of Sun + Address[] recs=msgs[i].getRecipients(Message.RecipientType.TO); + if (recs.length > 1) + continue; + + // get msgs date + Date d = msgs[i].getSentDate(); + if (d == null) + d = msgs[i].getReceivedDate(); + cal.setTime(d); + + // figure out what week it is + int week = cal.get(Calendar.WEEK_OF_YEAR); + int yr = cal.get(Calendar.YEAR); + String wkInYr = week + " " + yr; + + // increment that week's count + String num = (String)wks.get(wkInYr); + totalCount++; + if (num == null) + wks.put(wkInYr, "1"); + else { + int count = Integer.parseInt(num); + count++; + Integer str = new Integer(count); + wks.put(wkInYr, str.toString()); + } + + // check for pop3 requests + if (doPop3 && pop3Search.match(msgs[i])) + pop3Requests++; + } + + // print out the statistics + int startWk = 40; + int startYr = 1997; + Calendar now = Calendar.getInstance(); + now.setTime(new Date()); + cal.set(startYr, 1, 1); + cal.set(Calendar.WEEK_OF_YEAR, startWk); + cal.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY); + DateFormat df = new SimpleDateFormat("E MMM dd yyyy"); + int totalWks = 0; + + System.out.println("\nNumber of new messages a week sent to javamail@Sun.COM by external JavaMail\nusers. This does not include our answers and follow-up mail."); + for (;;) { + if (cal.after(now)) + break; + else { + totalWks++; + String wkInYr = (cal.get(Calendar.WEEK_OF_YEAR)) + + " " + + (cal.get(Calendar.YEAR)); + Object o = wks.get(wkInYr); + if (o != null) { + String i = (String)o; + System.out.println(" week of " + + df.format(cal.getTime()) + + ": " + + i + " messages"); + } + + // increment to next week + cal.add(Calendar.WEEK_OF_YEAR, 1); + } + } + System.out.println("------------------"); + System.out.println("Total messages received: " + totalCount); + System.out.println("Average messages/week: " + + totalCount/totalWks); + if (doPop3) + System.out.println("Total POP3 requests: " + pop3Requests); + folder.close(false); + store.close(); + } catch (Exception ex) { + System.out.println("Oops, got exception! " + ex.getMessage()); + ex.printStackTrace(); + } + System.exit(1); + } + + + /** + * Print verbose. + */ + static void pv(String s) { + if (verbose) + System.out.println(s); + } +}
diff --git a/demo/src/main/java/internal/testidle.java b/demo/src/main/java/internal/testidle.java new file mode 100644 index 0000000..6d65f9b --- /dev/null +++ b/demo/src/main/java/internal/testidle.java
@@ -0,0 +1,498 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +import java.util.*; +import java.util.concurrent.atomic.*; +import java.io.*; +import javax.mail.*; +import javax.mail.event.*; +import javax.mail.internet.*; +import javax.activation.*; + +import com.sun.mail.imap.*; + +/** + * Test program for IDLE support in IMAP provider. + * + * Run several threads that access the folder's connection + * while another thread runs the IDLE command. A timer + * thread checks that each thread is making progress, to + * detect deadlock. + * + * XXX - Should have another thread add messages to the folder + * to test the new message notification works properly. + */ +public class testidle { + + public static Store store; + public static Folder folder; + public static boolean showStructure = false; + public static boolean saveAttachments = false; + public static int attnum = 0; + public static int totalTime; + + public static final AtomicBoolean stop = new AtomicBoolean(false); + public static final AtomicInteger folderProgress = new AtomicInteger(); + public static final AtomicInteger msgProgress = new AtomicInteger(); + + public static void main(String argv[]) { + if (argv.length != 5) { + System.out.println( + "Usage: testidle <host> <user> <password> <mbox> <time>"); + System.exit(1); + } + + try { + Properties props = System.getProperties(); + + // Get a Session object + Session session = Session.getInstance(props, null); + // session.setDebug(true); + + // Get a Store object + store = session.getStore("imap"); + + store.addStoreListener(new StoreListener() { + public void notification(StoreEvent e) { + System.out.println("StoreEvent: type " + + e.getMessageType() + ", message " + + e.getMessage()); + } + }); + + // Connect + store.connect(argv[0], argv[1], argv[2]); + + // Open a Folder + folder = store.getFolder(argv[3]); + if (folder == null || !folder.exists()) { + System.out.println("Invalid folder"); + System.exit(1); + } + + folder.open(Folder.READ_WRITE); + + // Add messageCountListener to listen for new messages + folder.addMessageCountListener(new MessageCountAdapter() { + public void messagesAdded(MessageCountEvent ev) { + Message[] msgs = ev.getMessages(); + System.out.println("Got " + msgs.length + " new messages"); + + // Just dump out the new messages + for (int i = 0; i < msgs.length; i++) { + try { + System.out.println("-----"); + System.out.println("Message " + + msgs[i].getMessageNumber() + ":"); + msgs[i].writeTo(System.out); + } catch (IOException ioex) { + ioex.printStackTrace(); + } catch (MessagingException mex) { + mex.printStackTrace(); + } + } + } + }); + + totalTime = Integer.parseInt(argv[4]); + + new Thread("timer") { + public void run() { + timer(); + } + }.start(); + + new Thread("message reader") { + public void run() { + readMessages(); + } + }.start(); + + new Thread("folder reader") { + public void run() { + readFolder(); + } + }.start(); + + new Thread("store idle") { + public void run() { + storeIdle(); + } + }.start(); + + + /* + * make sure two threads running idle works properly + new Thread("idle") { + public void run() { + try { + doIdle(); + } catch (MessagingException mex) { } + } + }.start(); + */ + + doIdle(); + + System.out.println("connected " + store.isConnected()); + + } catch (Exception ex) { + ex.printStackTrace(); + } + } + + /** + * Run the idle command until told to stop. + */ + public static void doIdle() throws MessagingException { + boolean supportsIdle = false; + try { + if (folder instanceof IMAPFolder) { + IMAPFolder f = (IMAPFolder)folder; + f.idle(); + supportsIdle = true; + } + } catch (FolderClosedException fex) { + throw fex; + } catch (MessagingException mex) { + supportsIdle = false; + } + while (!stop.get()) { + if (supportsIdle && folder instanceof IMAPFolder) { + IMAPFolder f = (IMAPFolder)folder; + f.idle(); + /* + System.out.println("IDLE done in " + + Thread.currentThread().getName()); + */ + } else { + try { + Thread.sleep(1000); // sleep for 1000 milliseconds + } catch (InterruptedException ex) { } + + // This is to force the IMAP server to send us + // EXISTS notifications. + folder.getMessageCount(); + } + } + } + + /** + * Monitor progress of threads and tell them to stop + * when their time is up. + */ + public static void timer() { + long tend = System.currentTimeMillis() + totalTime; + int fpcnt = folderProgress.get(); + int mpcnt = msgProgress.get(); + while (System.currentTimeMillis() < tend) { + try { + Thread.sleep(1000); + } catch (InterruptedException ex) { } + int nfpcnt = folderProgress.get(); + int nmpcnt = msgProgress.get(); + if (!(nfpcnt > fpcnt && nmpcnt > mpcnt)) { + System.out.println("THREAD STUCK?"); + System.out.printf("fpcnt %d nfpcnt %d\n", fpcnt, nfpcnt); + System.out.printf("mpcnt %d nmpcnt %d\n", mpcnt, nmpcnt); + } + fpcnt = nfpcnt; + mpcnt = nmpcnt; + } + System.out.println("STOPPING"); + stop.set(true); + try { + folder.getUnreadMessageCount(); // force IDLE to terminate + } catch (MessagingException mex) { } + } + + /** + * Read all the messages in the folder and access all their content. + */ + public static void readMessages() { + try { + while (!stop.get()) { + int cnt = folder.getMessageCount(); + for (int i = 1; i <= cnt; i++) { + //System.out.println("dump " + i); + Message msg = folder.getMessage(i); + dumpEnvelope(msg); + msgProgress.incrementAndGet(); + try { + Thread.sleep(25); + } catch (InterruptedException ex) { } + } + } + System.out.println("messages DONE"); + } catch (Exception ex) { + System.out.println(ex); + } + } + + /** + * Perform folder commands to compete with the message + * access commands. + */ + public static void readFolder() { + try { + while (!stop.get()) { + int cnt = folder.getUnreadMessageCount(); + store.isConnected(); // poke the store too + folderProgress.incrementAndGet(); + try { + Thread.sleep(100); + } catch (InterruptedException ex) { } + } + System.out.println("folder DONE"); + } catch (MessagingException mex) { + System.out.println(mex); + } + } + + /** + * Run the idle command until told to stop. + */ + public static void storeIdle() { + boolean supportsIdle = false; + try { + if (store instanceof IMAPStore) { + IMAPStore s = (IMAPStore)store; + s.idle(); + supportsIdle = true; + } + } catch (MessagingException mex) { + supportsIdle = false; + } + try { + while (!stop.get()) { + if (supportsIdle && store instanceof IMAPStore) { + IMAPStore s = (IMAPStore)store; + s.idle(); + /* + */ + System.out.println("IDLE done in " + + Thread.currentThread().getName()); + /* + */ + } else { + try { + Thread.sleep(1000); // sleep for 1000 milliseconds + } catch (InterruptedException ex) { } + } + } + } catch (MessagingException mex) { + System.out.println(mex); + } + } + + /** + * Dump contents of message part. + * (Copied from msgshow.java) + */ + public static void dumpPart(Part p) throws Exception { + if (p instanceof Message) + dumpEnvelope((Message)p); + + /** Dump input stream .. + + InputStream is = p.getInputStream(); + // If "is" is not already buffered, wrap a BufferedInputStream + // around it. + if (!(is instanceof BufferedInputStream)) + is = new BufferedInputStream(is); + int c; + while ((c = is.read()) != -1) + System.out.write(c); + + **/ + + String ct = p.getContentType(); + try { + pr("CONTENT-TYPE: " + (new ContentType(ct)).toString()); + } catch (ParseException pex) { + pr("BAD CONTENT-TYPE: " + ct); + } + String filename = p.getFileName(); + if (filename != null) + pr("FILENAME: " + filename); + + /* + * Using isMimeType to determine the content type avoids + * fetching the actual content data until we need it. + */ + if (p.isMimeType("text/plain")) { + pr("This is plain text"); + pr("---------------------------"); + if (!showStructure && !saveAttachments) + System.out.println((String)p.getContent()); + } else if (p.isMimeType("multipart/*")) { + pr("This is a Multipart"); + pr("---------------------------"); + Multipart mp = (Multipart)p.getContent(); + level++; + int count = mp.getCount(); + for (int i = 0; i < count; i++) + dumpPart(mp.getBodyPart(i)); + level--; + } else if (p.isMimeType("message/rfc822")) { + pr("This is a Nested Message"); + pr("---------------------------"); + level++; + dumpPart((Part)p.getContent()); + level--; + } else { + if (!showStructure && !saveAttachments) { + /* + * If we actually want to see the data, and it's not a + * MIME type we know, fetch it and check its Java type. + */ + Object o = p.getContent(); + if (o instanceof String) { + pr("This is a string"); + pr("---------------------------"); + System.out.println((String)o); + } else if (o instanceof InputStream) { + pr("This is just an input stream"); + pr("---------------------------"); + InputStream is = (InputStream)o; + int c; + while ((c = is.read()) != -1) + System.out.write(c); + } else { + pr("This is an unknown type"); + pr("---------------------------"); + pr(o.toString()); + } + } else { + // just a separator + pr("---------------------------"); + } + } + + /* + * If we're saving attachments, write out anything that + * looks like an attachment into an appropriately named + * file. Don't overwrite existing files to prevent + * mistakes. + */ + if (saveAttachments && level != 0 && !p.isMimeType("multipart/*")) { + String disp = p.getDisposition(); + // many mailers don't include a Content-Disposition + if (disp == null || disp.equalsIgnoreCase(Part.ATTACHMENT)) { + if (filename == null) + filename = "Attachment" + attnum++; + pr("Saving attachment to file " + filename); + try { + File f = new File(filename); + if (f.exists()) + // XXX - could try a series of names + throw new IOException("file exists"); + ((MimeBodyPart)p).saveFile(f); + } catch (IOException ex) { + pr("Failed to save attachment: " + ex); + } + pr("---------------------------"); + } + } + } + + public static void dumpEnvelope(Message m) throws Exception { + pr("This is the message envelope"); + pr("---------------------------"); + Address[] a; + // FROM + if ((a = m.getFrom()) != null) { + for (int j = 0; j < a.length; j++) + pr("FROM: " + a[j].toString()); + } + + // TO + if ((a = m.getRecipients(Message.RecipientType.TO)) != null) { + for (int j = 0; j < a.length; j++) { + pr("TO: " + a[j].toString()); + InternetAddress ia = (InternetAddress)a[j]; + if (ia.isGroup()) { + InternetAddress[] aa = ia.getGroup(false); + for (int k = 0; k < aa.length; k++) + pr(" GROUP: " + aa[k].toString()); + } + } + } + + // SUBJECT + pr("SUBJECT: " + m.getSubject()); + + // DATE + Date d = m.getSentDate(); + pr("SendDate: " + + (d != null ? d.toString() : "UNKNOWN")); + + // FLAGS + Flags flags = m.getFlags(); + StringBuffer sb = new StringBuffer(); + Flags.Flag[] sf = flags.getSystemFlags(); // get the system flags + + boolean first = true; + for (int i = 0; i < sf.length; i++) { + String s; + Flags.Flag f = sf[i]; + if (f == Flags.Flag.ANSWERED) + s = "\\Answered"; + else if (f == Flags.Flag.DELETED) + s = "\\Deleted"; + else if (f == Flags.Flag.DRAFT) + s = "\\Draft"; + else if (f == Flags.Flag.FLAGGED) + s = "\\Flagged"; + else if (f == Flags.Flag.RECENT) + s = "\\Recent"; + else if (f == Flags.Flag.SEEN) + s = "\\Seen"; + else + continue; // skip it + if (first) + first = false; + else + sb.append(' '); + sb.append(s); + } + + String[] uf = flags.getUserFlags(); // get the user flag strings + for (int i = 0; i < uf.length; i++) { + if (first) + first = false; + else + sb.append(' '); + sb.append(uf[i]); + } + pr("FLAGS: " + sb.toString()); + + // X-MAILER + String[] hdrs = m.getHeader("X-Mailer"); + if (hdrs != null) + pr("X-Mailer: " + hdrs[0]); + else + pr("X-Mailer NOT available"); + } + + static String indentStr = " "; + static int level = 0; + + /** + * Print a, possibly indented, string. + */ + public static void pr(String s) { + /* + if (showStructure) + System.out.print(indentStr.substring(0, level * 2)); + System.out.println(s); + */ + } +}
diff --git a/demo/src/main/java/monitor.java b/demo/src/main/java/monitor.java new file mode 100644 index 0000000..86fb299 --- /dev/null +++ b/demo/src/main/java/monitor.java
@@ -0,0 +1,107 @@ +/* + * Copyright (c) 1996, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +import java.util.*; +import java.io.*; +import javax.mail.*; +import javax.mail.event.*; +import javax.activation.*; + +import com.sun.mail.imap.*; + +/* Monitors given mailbox for new mail */ + +public class monitor { + + public static void main(String argv[]) { + if (argv.length != 5) { + System.out.println( + "Usage: monitor <host> <user> <password> <mbox> <freq>"); + System.exit(1); + } + System.out.println("\nTesting monitor\n"); + + try { + Properties props = System.getProperties(); + + // Get a Session object + Session session = Session.getInstance(props, null); + // session.setDebug(true); + + // Get a Store object + Store store = session.getStore("imap"); + + // Connect + store.connect(argv[0], argv[1], argv[2]); + + // Open a Folder + Folder folder = store.getFolder(argv[3]); + if (folder == null || !folder.exists()) { + System.out.println("Invalid folder"); + System.exit(1); + } + + folder.open(Folder.READ_WRITE); + + // Add messageCountListener to listen for new messages + folder.addMessageCountListener(new MessageCountAdapter() { + public void messagesAdded(MessageCountEvent ev) { + Message[] msgs = ev.getMessages(); + System.out.println("Got " + msgs.length + " new messages"); + + // Just dump out the new messages + for (int i = 0; i < msgs.length; i++) { + try { + System.out.println("-----"); + System.out.println("Message " + + msgs[i].getMessageNumber() + ":"); + msgs[i].writeTo(System.out); + } catch (IOException ioex) { + ioex.printStackTrace(); + } catch (MessagingException mex) { + mex.printStackTrace(); + } + } + } + }); + + // Check mail once in "freq" MILLIseconds + int freq = Integer.parseInt(argv[4]); + boolean supportsIdle = false; + try { + if (folder instanceof IMAPFolder) { + IMAPFolder f = (IMAPFolder)folder; + f.idle(); + supportsIdle = true; + } + } catch (FolderClosedException fex) { + throw fex; + } catch (MessagingException mex) { + supportsIdle = false; + } + for (;;) { + if (supportsIdle && folder instanceof IMAPFolder) { + IMAPFolder f = (IMAPFolder)folder; + f.idle(); + System.out.println("IDLE done"); + } else { + Thread.sleep(freq); // sleep for freq milliseconds + + // This is to force the IMAP server to send us + // EXISTS notifications. + folder.getMessageCount(); + } + } + + } catch (Exception ex) { + ex.printStackTrace(); + } + } +}
diff --git a/demo/src/main/java/mover.java b/demo/src/main/java/mover.java new file mode 100644 index 0000000..acf2e0d --- /dev/null +++ b/demo/src/main/java/mover.java
@@ -0,0 +1,159 @@ +/* + * Copyright (c) 1996, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +import java.io.*; +import java.util.*; +import javax.mail.*; +import javax.mail.internet.*; + +/* MOVE messages between mailboxes */ + +public class mover { + + static String protocol = "imap"; + static String host = null; + static String user = null; + static String password = null; + static String src = null; + static String dest = null; + static boolean expunge = false; + static String url = null; + + public static void main(String argv[]) { + int start = 1; int end = -1; + int optind; + + for (optind = 0; optind < argv.length; optind++) { + if (argv[optind].equals("-T")) { // protocol + protocol = argv[++optind]; + } else if (argv[optind].equals("-H")) { // host + host = argv[++optind]; + } else if (argv[optind].equals("-U")) { // user + user = argv[++optind]; + } else if (argv[optind].equals("-P")) { // password + password = argv[++optind]; + } else if (argv[optind].equals("-L")) { + url = argv[++optind]; + } else if (argv[optind].equals("-s")) { // Source mbox + src = argv[++optind]; + } else if (argv[optind].equals("-d")) { // Destination mbox + dest = argv[++optind]; + } else if (argv[optind].equals("-x")) { // Expunge ? + expunge = true; + } else if (argv[optind].equals("--")) { + optind++; + break; + } else if (argv[optind].startsWith("-")) { + System.out.println( +"Usage: mover [-T protocol] [-H host] [-U user] [-P password] [-L url] [-v]"); + System.out.println( +"\t[-s source mbox] [-d destination mbox] [-x] [msgnum1] [msgnum2]"); + System.out.println( +"\t The -x option => EXPUNGE deleted messages"); + System.out.println( +"\t msgnum1 => start of message-range; msgnum2 => end of message-range"); + System.exit(1); + } else { + break; + } + } + + if (optind < argv.length) + start = Integer.parseInt(argv[optind++]); // start msg + + if (optind < argv.length) + end = Integer.parseInt(argv[optind++]); // end msg + + try { + // Get a Properties object + Properties props = System.getProperties(); + + // Get a Session object + Session session = Session.getInstance(props, null); + + // Get a Store object + Store store = null; + if (url != null) { + URLName urln = new URLName(url); + store = session.getStore(urln); + store.connect(); + } else { + if (protocol != null) + store = session.getStore(protocol); + else + store = session.getStore(); + + // Connect + if (host != null || user != null || password != null) + store.connect(host, user, password); + else + store.connect(); + } + + + // Open source Folder + Folder folder = store.getFolder(src); + if (folder == null || !folder.exists()) { + System.out.println("Invalid folder: " + src); + System.exit(1); + } + + folder.open(Folder.READ_WRITE); + + int count = folder.getMessageCount(); + if (count == 0) { // No messages in the source folder + System.out.println(folder.getName() + " is empty"); + // Close folder, store and return + folder.close(false); + store.close(); + return; + } + + // Open destination folder, create if reqd + Folder dfolder = store.getFolder(dest); + if (!dfolder.exists()) + dfolder.create(Folder.HOLDS_MESSAGES); + + if (end == -1) + end = count; + + // Get the message objects to copy + Message[] msgs = folder.getMessages(start, end); + System.out.println("Moving " + msgs.length + " messages"); + + if (msgs.length != 0) { + folder.copyMessages(msgs, dfolder); + folder.setFlags(msgs, new Flags(Flags.Flag.DELETED), true); + + // Dump out the Flags of the moved messages, to insure that + // all got deleted + for (int i = 0; i < msgs.length; i++) { + if (!msgs[i].isSet(Flags.Flag.DELETED)) + System.out.println("Message # " + msgs[i] + + " not deleted"); + } + } + + // Close folders and store + folder.close(expunge); + store.close(); + + } catch (MessagingException mex) { + Exception ex = mex; + do { + System.out.println(ex.getMessage()); + if (ex instanceof MessagingException) + ex = ((MessagingException)ex).getNextException(); + else + ex = null; + } while (ex != null); + } + } +}
diff --git a/demo/src/main/java/msgmultisendsample.java b/demo/src/main/java/msgmultisendsample.java new file mode 100644 index 0000000..abde0dc --- /dev/null +++ b/demo/src/main/java/msgmultisendsample.java
@@ -0,0 +1,88 @@ +/* + * Copyright (c) 1996, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +import java.util.*; +import java.io.*; +import javax.mail.*; +import javax.mail.internet.*; +import javax.activation.*; + +/** + * msgmultisendsample creates a simple multipart/mixed message and sends it. + * Both body parts are text/plain. + * <p> + * usage: <code>java msgmultisendsample <i>to from smtp true|false</i></code> + * where <i>to</i> and <i>from</i> are the destination and + * origin email addresses, respectively, and <i>smtp</i> + * is the hostname of the machine that has smtp server + * running. The last parameter either turns on or turns off + * debugging during sending. + * + * @author Max Spivak + */ +public class msgmultisendsample { + static String msgText1 = "This is a message body.\nHere's line two."; + static String msgText2 = "This is the text in the message attachment."; + + public static void main(String[] args) { + if (args.length != 4) { + System.out.println("usage: java msgmultisend <to> <from> <smtp> true|false"); + return; + } + + String to = args[0]; + String from = args[1]; + String host = args[2]; + boolean debug = Boolean.valueOf(args[3]).booleanValue(); + + // create some properties and get the default Session + Properties props = new Properties(); + props.put("mail.smtp.host", host); + + Session session = Session.getInstance(props, null); + session.setDebug(debug); + + try { + // create a message + MimeMessage msg = new MimeMessage(session); + msg.setFrom(new InternetAddress(from)); + InternetAddress[] address = {new InternetAddress(to)}; + msg.setRecipients(Message.RecipientType.TO, address); + msg.setSubject("JavaMail APIs Multipart Test"); + msg.setSentDate(new Date()); + + // create and fill the first message part + MimeBodyPart mbp1 = new MimeBodyPart(); + mbp1.setText(msgText1); + + // create and fill the second message part + MimeBodyPart mbp2 = new MimeBodyPart(); + // Use setText(text, charset), to show it off ! + mbp2.setText(msgText2, "us-ascii"); + + // create the Multipart and its parts to it + Multipart mp = new MimeMultipart(); + mp.addBodyPart(mbp1); + mp.addBodyPart(mbp2); + + // add the Multipart to the message + msg.setContent(mp); + + // send the message + Transport.send(msg); + } catch (MessagingException mex) { + mex.printStackTrace(); + Exception ex = null; + if ((ex = mex.getNextException()) != null) { + ex.printStackTrace(); + } + } + } +}
diff --git a/demo/src/main/java/msgsend.java b/demo/src/main/java/msgsend.java new file mode 100644 index 0000000..7a7def9 --- /dev/null +++ b/demo/src/main/java/msgsend.java
@@ -0,0 +1,227 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +import java.io.*; +import java.util.Properties; +import java.util.Date; + +import javax.mail.*; +import javax.mail.internet.*; + +/** + * Demo app that shows how to construct and send an RFC822 + * (singlepart) message. + * + * XXX - allow more than one recipient on the command line + * + * @author Max Spivak + * @author Bill Shannon + */ + +public class msgsend { + + public static void main(String[] argv) { + String to, subject = null, from = null, + cc = null, bcc = null, url = null; + String mailhost = null; + String mailer = "msgsend"; + String file = null; + String protocol = null, host = null, user = null, password = null; + String record = null; // name of folder in which to record mail + boolean debug = false; + BufferedReader in = + new BufferedReader(new InputStreamReader(System.in)); + int optind; + + /* + * Process command line arguments. + */ + for (optind = 0; optind < argv.length; optind++) { + if (argv[optind].equals("-T")) { + protocol = argv[++optind]; + } else if (argv[optind].equals("-H")) { + host = argv[++optind]; + } else if (argv[optind].equals("-U")) { + user = argv[++optind]; + } else if (argv[optind].equals("-P")) { + password = argv[++optind]; + } else if (argv[optind].equals("-M")) { + mailhost = argv[++optind]; + } else if (argv[optind].equals("-f")) { + record = argv[++optind]; + } else if (argv[optind].equals("-a")) { + file = argv[++optind]; + } else if (argv[optind].equals("-s")) { + subject = argv[++optind]; + } else if (argv[optind].equals("-o")) { // originator + from = argv[++optind]; + } else if (argv[optind].equals("-c")) { + cc = argv[++optind]; + } else if (argv[optind].equals("-b")) { + bcc = argv[++optind]; + } else if (argv[optind].equals("-L")) { + url = argv[++optind]; + } else if (argv[optind].equals("-d")) { + debug = true; + } else if (argv[optind].equals("--")) { + optind++; + break; + } else if (argv[optind].startsWith("-")) { + System.out.println( +"Usage: msgsend [[-L store-url] | [-T prot] [-H host] [-U user] [-P passwd]]"); + System.out.println( +"\t[-s subject] [-o from-address] [-c cc-addresses] [-b bcc-addresses]"); + System.out.println( +"\t[-f record-mailbox] [-M transport-host] [-a attach-file] [-d] [address]"); + System.exit(1); + } else { + break; + } + } + + try { + /* + * Prompt for To and Subject, if not specified. + */ + if (optind < argv.length) { + // XXX - concatenate all remaining arguments + to = argv[optind]; + System.out.println("To: " + to); + } else { + System.out.print("To: "); + System.out.flush(); + to = in.readLine(); + } + if (subject == null) { + System.out.print("Subject: "); + System.out.flush(); + subject = in.readLine(); + } else { + System.out.println("Subject: " + subject); + } + + /* + * Initialize the JavaMail Session. + */ + Properties props = System.getProperties(); + // XXX - could use Session.getTransport() and Transport.connect() + // XXX - assume we're using SMTP + if (mailhost != null) + props.put("mail.smtp.host", mailhost); + + // Get a Session object + Session session = Session.getInstance(props, null); + if (debug) + session.setDebug(true); + + /* + * Construct the message and send it. + */ + Message msg = new MimeMessage(session); + if (from != null) + msg.setFrom(new InternetAddress(from)); + else + msg.setFrom(); + + msg.setRecipients(Message.RecipientType.TO, + InternetAddress.parse(to, false)); + if (cc != null) + msg.setRecipients(Message.RecipientType.CC, + InternetAddress.parse(cc, false)); + if (bcc != null) + msg.setRecipients(Message.RecipientType.BCC, + InternetAddress.parse(bcc, false)); + + msg.setSubject(subject); + + String text = collect(in); + + if (file != null) { + // Attach the specified file. + // We need a multipart message to hold the attachment. + MimeBodyPart mbp1 = new MimeBodyPart(); + mbp1.setText(text); + MimeBodyPart mbp2 = new MimeBodyPart(); + mbp2.attachFile(file); + MimeMultipart mp = new MimeMultipart(); + mp.addBodyPart(mbp1); + mp.addBodyPart(mbp2); + msg.setContent(mp); + } else { + // If the desired charset is known, you can use + // setText(text, charset) + msg.setText(text); + } + + msg.setHeader("X-Mailer", mailer); + msg.setSentDate(new Date()); + + // send the thing off + Transport.send(msg); + + System.out.println("\nMail was sent successfully."); + + /* + * Save a copy of the message, if requested. + */ + if (record != null) { + // Get a Store object + Store store = null; + if (url != null) { + URLName urln = new URLName(url); + store = session.getStore(urln); + store.connect(); + } else { + if (protocol != null) + store = session.getStore(protocol); + else + store = session.getStore(); + + // Connect + if (host != null || user != null || password != null) + store.connect(host, user, password); + else + store.connect(); + } + + // Get record Folder. Create if it does not exist. + Folder folder = store.getFolder(record); + if (folder == null) { + System.err.println("Can't get record folder."); + System.exit(1); + } + if (!folder.exists()) + folder.create(Folder.HOLDS_MESSAGES); + + Message[] msgs = new Message[1]; + msgs[0] = msg; + folder.appendMessages(msgs); + + System.out.println("Mail was recorded successfully."); + } + + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * Read the body of the message until EOF. + */ + public static String collect(BufferedReader in) throws IOException { + String line; + StringBuffer sb = new StringBuffer(); + while ((line = in.readLine()) != null) { + sb.append(line); + sb.append("\n"); + } + return sb.toString(); + } +}
diff --git a/demo/src/main/java/msgsendsample.java b/demo/src/main/java/msgsendsample.java new file mode 100644 index 0000000..c810a01 --- /dev/null +++ b/demo/src/main/java/msgsendsample.java
@@ -0,0 +1,106 @@ +/* + * Copyright (c) 1996, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +import java.util.*; +import java.io.*; +import javax.mail.*; +import javax.mail.internet.*; +import javax.activation.*; + +/** + * msgsendsample creates a very simple text/plain message and sends it. + * <p> + * usage: <code>java msgsendsample <i>to from smtphost true|false</i></code> + * where <i>to</i> and <i>from</i> are the destination and + * origin email addresses, respectively, and <i>smtphost</i> + * is the hostname of the machine that has the smtp server + * running. The last parameter either turns on or turns off + * debugging during sending. + * + * @author Max Spivak + */ +public class msgsendsample { + static String msgText = "This is a message body.\nHere's the second line."; + + public static void main(String[] args) { + if (args.length != 4) { + usage(); + System.exit(1); + } + + System.out.println(); + + String to = args[0]; + String from = args[1]; + String host = args[2]; + boolean debug = Boolean.valueOf(args[3]).booleanValue(); + + // create some properties and get the default Session + Properties props = new Properties(); + props.put("mail.smtp.host", host); + if (debug) props.put("mail.debug", args[3]); + + Session session = Session.getInstance(props, null); + session.setDebug(debug); + + try { + // create a message + MimeMessage msg = new MimeMessage(session); + msg.setFrom(new InternetAddress(from)); + InternetAddress[] address = {new InternetAddress(to)}; + msg.setRecipients(Message.RecipientType.TO, address); + msg.setSubject("JavaMail APIs Test"); + msg.setSentDate(new Date()); + // If the desired charset is known, you can use + // setText(text, charset) + msg.setText(msgText); + + Transport.send(msg); + } catch (MessagingException mex) { + System.out.println("\n--Exception handling in msgsendsample.java"); + + mex.printStackTrace(); + System.out.println(); + Exception ex = mex; + do { + if (ex instanceof SendFailedException) { + SendFailedException sfex = (SendFailedException)ex; + Address[] invalid = sfex.getInvalidAddresses(); + if (invalid != null) { + System.out.println(" ** Invalid Addresses"); + for (int i = 0; i < invalid.length; i++) + System.out.println(" " + invalid[i]); + } + Address[] validUnsent = sfex.getValidUnsentAddresses(); + if (validUnsent != null) { + System.out.println(" ** ValidUnsent Addresses"); + for (int i = 0; i < validUnsent.length; i++) + System.out.println(" "+validUnsent[i]); + } + Address[] validSent = sfex.getValidSentAddresses(); + if (validSent != null) { + System.out.println(" ** ValidSent Addresses"); + for (int i = 0; i < validSent.length; i++) + System.out.println(" "+validSent[i]); + } + } + System.out.println(); + if (ex instanceof MessagingException) + ex = ((MessagingException)ex).getNextException(); + else + ex = null; + } while (ex != null); + } + } + + private static void usage() { + System.out.println("usage: java msgsendsample <to> <from> <smtp> true|false"); + } +}
diff --git a/demo/src/main/java/msgshow.java b/demo/src/main/java/msgshow.java new file mode 100644 index 0000000..47ed000 --- /dev/null +++ b/demo/src/main/java/msgshow.java
@@ -0,0 +1,424 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +import java.util.*; +import java.io.*; +import javax.mail.*; +import javax.mail.event.*; +import javax.mail.internet.*; + +/* + * Demo app that exercises the Message interfaces. + * Show information about and contents of messages. + * + * @author John Mani + * @author Bill Shannon + */ + +public class msgshow { + + static String protocol; + static String host = null; + static String user = null; + static String password = null; + static String mbox = null; + static String url = null; + static int port = -1; + static boolean verbose = false; + static boolean debug = false; + static boolean showStructure = false; + static boolean showMessage = false; + static boolean showAlert = false; + static boolean saveAttachments = false; + static int attnum = 1; + + public static void main(String argv[]) { + int optind; + InputStream msgStream = System.in; + + for (optind = 0; optind < argv.length; optind++) { + if (argv[optind].equals("-T")) { + protocol = argv[++optind]; + } else if (argv[optind].equals("-H")) { + host = argv[++optind]; + } else if (argv[optind].equals("-U")) { + user = argv[++optind]; + } else if (argv[optind].equals("-P")) { + password = argv[++optind]; + } else if (argv[optind].equals("-v")) { + verbose = true; + } else if (argv[optind].equals("-D")) { + debug = true; + } else if (argv[optind].equals("-f")) { + mbox = argv[++optind]; + } else if (argv[optind].equals("-L")) { + url = argv[++optind]; + } else if (argv[optind].equals("-p")) { + port = Integer.parseInt(argv[++optind]); + } else if (argv[optind].equals("-s")) { + showStructure = true; + } else if (argv[optind].equals("-S")) { + saveAttachments = true; + } else if (argv[optind].equals("-m")) { + showMessage = true; + } else if (argv[optind].equals("-a")) { + showAlert = true; + } else if (argv[optind].equals("--")) { + optind++; + break; + } else if (argv[optind].startsWith("-")) { + System.out.println( +"Usage: msgshow [-L url] [-T protocol] [-H host] [-p port] [-U user]"); + System.out.println( +"\t[-P password] [-f mailbox] [msgnum ...] [-v] [-D] [-s] [-S] [-a]"); + System.out.println( +"or msgshow -m [-v] [-D] [-s] [-S] [-f msg-file]"); + System.exit(1); + } else { + break; + } + } + + try { + // Get a Properties object + Properties props = System.getProperties(); + + // Get a Session object + Session session = Session.getInstance(props, null); + session.setDebug(debug); + + if (showMessage) { + MimeMessage msg; + if (mbox != null) + msg = new MimeMessage(session, + new BufferedInputStream(new FileInputStream(mbox))); + else + msg = new MimeMessage(session, msgStream); + dumpPart(msg); + System.exit(0); + } + + // Get a Store object + Store store = null; + if (url != null) { + URLName urln = new URLName(url); + store = session.getStore(urln); + if (showAlert) { + store.addStoreListener(new StoreListener() { + public void notification(StoreEvent e) { + String s; + if (e.getMessageType() == StoreEvent.ALERT) + s = "ALERT: "; + else + s = "NOTICE: "; + System.out.println(s + e.getMessage()); + } + }); + } + store.connect(); + } else { + if (protocol != null) + store = session.getStore(protocol); + else + store = session.getStore(); + + // Connect + if (host != null || user != null || password != null) + store.connect(host, port, user, password); + else + store.connect(); + } + + + // Open the Folder + + Folder folder = store.getDefaultFolder(); + if (folder == null) { + System.out.println("No default folder"); + System.exit(1); + } + + if (mbox == null) + mbox = "INBOX"; + folder = folder.getFolder(mbox); + if (folder == null) { + System.out.println("Invalid folder"); + System.exit(1); + } + + // try to open read/write and if that fails try read-only + try { + folder.open(Folder.READ_WRITE); + } catch (MessagingException ex) { + folder.open(Folder.READ_ONLY); + } + int totalMessages = folder.getMessageCount(); + + if (totalMessages == 0) { + System.out.println("Empty folder"); + folder.close(false); + store.close(); + System.exit(1); + } + + if (verbose) { + int newMessages = folder.getNewMessageCount(); + System.out.println("Total messages = " + totalMessages); + System.out.println("New messages = " + newMessages); + System.out.println("-------------------------------"); + } + + if (optind >= argv.length) { + // Attributes & Flags for all messages .. + Message[] msgs = folder.getMessages(); + + // Use a suitable FetchProfile + FetchProfile fp = new FetchProfile(); + fp.add(FetchProfile.Item.ENVELOPE); + fp.add(FetchProfile.Item.FLAGS); + fp.add("X-Mailer"); + folder.fetch(msgs, fp); + + for (int i = 0; i < msgs.length; i++) { + System.out.println("--------------------------"); + System.out.println("MESSAGE #" + (i + 1) + ":"); + dumpEnvelope(msgs[i]); + // dumpPart(msgs[i]); + } + } else { + while (optind < argv.length) { + int msgnum = Integer.parseInt(argv[optind++]); + System.out.println("Getting message number: " + msgnum); + Message m = null; + + try { + m = folder.getMessage(msgnum); + dumpPart(m); + } catch (IndexOutOfBoundsException iex) { + System.out.println("Message number out of range"); + } + } + } + + folder.close(false); + store.close(); + } catch (Exception ex) { + System.out.println("Oops, got exception! " + ex.getMessage()); + ex.printStackTrace(); + System.exit(1); + } + System.exit(0); + } + + public static void dumpPart(Part p) throws Exception { + if (p instanceof Message) + dumpEnvelope((Message)p); + + /** Dump input stream .. + + InputStream is = p.getInputStream(); + // If "is" is not already buffered, wrap a BufferedInputStream + // around it. + if (!(is instanceof BufferedInputStream)) + is = new BufferedInputStream(is); + int c; + while ((c = is.read()) != -1) + System.out.write(c); + + **/ + + String ct = p.getContentType(); + try { + pr("CONTENT-TYPE: " + (new ContentType(ct)).toString()); + } catch (ParseException pex) { + pr("BAD CONTENT-TYPE: " + ct); + } + String filename = p.getFileName(); + if (filename != null) + pr("FILENAME: " + filename); + + /* + * Using isMimeType to determine the content type avoids + * fetching the actual content data until we need it. + */ + if (p.isMimeType("text/plain")) { + pr("This is plain text"); + pr("---------------------------"); + if (!showStructure && !saveAttachments) + System.out.println((String)p.getContent()); + } else if (p.isMimeType("multipart/*")) { + pr("This is a Multipart"); + pr("---------------------------"); + Multipart mp = (Multipart)p.getContent(); + level++; + int count = mp.getCount(); + for (int i = 0; i < count; i++) + dumpPart(mp.getBodyPart(i)); + level--; + } else if (p.isMimeType("message/rfc822")) { + pr("This is a Nested Message"); + pr("---------------------------"); + level++; + dumpPart((Part)p.getContent()); + level--; + } else { + if (!showStructure && !saveAttachments) { + /* + * If we actually want to see the data, and it's not a + * MIME type we know, fetch it and check its Java type. + */ + Object o = p.getContent(); + if (o instanceof String) { + pr("This is a string"); + pr("---------------------------"); + System.out.println((String)o); + } else if (o instanceof InputStream) { + pr("This is just an input stream"); + pr("---------------------------"); + InputStream is = (InputStream)o; + int c; + while ((c = is.read()) != -1) + System.out.write(c); + } else { + pr("This is an unknown type"); + pr("---------------------------"); + pr(o.toString()); + } + } else { + // just a separator + pr("---------------------------"); + } + } + + /* + * If we're saving attachments, write out anything that + * looks like an attachment into an appropriately named + * file. Don't overwrite existing files to prevent + * mistakes. + */ + if (saveAttachments && level != 0 && p instanceof MimeBodyPart && + !p.isMimeType("multipart/*")) { + String disp = p.getDisposition(); + // many mailers don't include a Content-Disposition + if (disp == null || disp.equalsIgnoreCase(Part.ATTACHMENT)) { + if (filename == null) + filename = "Attachment" + attnum++; + pr("Saving attachment to file " + filename); + try { + File f = new File(filename); + if (f.exists()) + // XXX - could try a series of names + throw new IOException("file exists"); + ((MimeBodyPart)p).saveFile(f); + } catch (IOException ex) { + pr("Failed to save attachment: " + ex); + } + pr("---------------------------"); + } + } + } + + public static void dumpEnvelope(Message m) throws Exception { + pr("This is the message envelope"); + pr("---------------------------"); + Address[] a; + // FROM + if ((a = m.getFrom()) != null) { + for (int j = 0; j < a.length; j++) + pr("FROM: " + a[j].toString()); + } + + // REPLY TO + if ((a = m.getReplyTo()) != null) { + for (int j = 0; j < a.length; j++) + pr("REPLY TO: " + a[j].toString()); + } + + // TO + if ((a = m.getRecipients(Message.RecipientType.TO)) != null) { + for (int j = 0; j < a.length; j++) { + pr("TO: " + a[j].toString()); + InternetAddress ia = (InternetAddress)a[j]; + if (ia.isGroup()) { + InternetAddress[] aa = ia.getGroup(false); + for (int k = 0; k < aa.length; k++) + pr(" GROUP: " + aa[k].toString()); + } + } + } + + // SUBJECT + pr("SUBJECT: " + m.getSubject()); + + // DATE + Date d = m.getSentDate(); + pr("SendDate: " + + (d != null ? d.toString() : "UNKNOWN")); + + // FLAGS + Flags flags = m.getFlags(); + StringBuffer sb = new StringBuffer(); + Flags.Flag[] sf = flags.getSystemFlags(); // get the system flags + + boolean first = true; + for (int i = 0; i < sf.length; i++) { + String s; + Flags.Flag f = sf[i]; + if (f == Flags.Flag.ANSWERED) + s = "\\Answered"; + else if (f == Flags.Flag.DELETED) + s = "\\Deleted"; + else if (f == Flags.Flag.DRAFT) + s = "\\Draft"; + else if (f == Flags.Flag.FLAGGED) + s = "\\Flagged"; + else if (f == Flags.Flag.RECENT) + s = "\\Recent"; + else if (f == Flags.Flag.SEEN) + s = "\\Seen"; + else + continue; // skip it + if (first) + first = false; + else + sb.append(' '); + sb.append(s); + } + + String[] uf = flags.getUserFlags(); // get the user flag strings + for (int i = 0; i < uf.length; i++) { + if (first) + first = false; + else + sb.append(' '); + sb.append(uf[i]); + } + pr("FLAGS: " + sb.toString()); + + // X-MAILER + String[] hdrs = m.getHeader("X-Mailer"); + if (hdrs != null) + pr("X-Mailer: " + hdrs[0]); + else + pr("X-Mailer NOT available"); + } + + static String indentStr = " "; + static int level = 0; + + /** + * Print a, possibly indented, string. + */ + public static void pr(String s) { + if (showStructure) + System.out.print(indentStr.substring(0, level * 2)); + System.out.println(s); + } +}
diff --git a/demo/src/main/java/namespace.java b/demo/src/main/java/namespace.java new file mode 100644 index 0000000..0e36247 --- /dev/null +++ b/demo/src/main/java/namespace.java
@@ -0,0 +1,131 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +import java.util.*; +import java.io.*; +import javax.mail.*; + +/* + * Demo app that exercises the namespace interfaces. + * Show the namespaces supported by a store. + * + * @author Bill Shannon + */ + +public class namespace { + + static String protocol; + static String host = null; + static String user = null; + static String password = null; + static String url = null; + static int port = -1; + static boolean debug = false; + static String suser = "other"; + + public static void main(String argv[]) { + int msgnum = -1; + int optind; + + for (optind = 0; optind < argv.length; optind++) { + if (argv[optind].equals("-T")) { + protocol = argv[++optind]; + } else if (argv[optind].equals("-H")) { + host = argv[++optind]; + } else if (argv[optind].equals("-U")) { + user = argv[++optind]; + } else if (argv[optind].equals("-P")) { + password = argv[++optind]; + } else if (argv[optind].equals("-D")) { + debug = true; + } else if (argv[optind].equals("-L")) { + url = argv[++optind]; + } else if (argv[optind].equals("-p")) { + port = Integer.parseInt(argv[++optind]); + } else if (argv[optind].equals("-u")) { + suser = argv[++optind]; + } else if (argv[optind].equals("--")) { + optind++; + break; + } else if (argv[optind].startsWith("-")) { + System.out.println( +"Usage: namespace [-L url] [-T protocol] [-H host] [-p port] [-U user]"); + System.out.println( +"\t[-P password] [-u other-user] [-D]"); + System.exit(1); + } else { + break; + } + } + + try { + // Get a Properties object + Properties props = System.getProperties(); + + // Get a Session object + Session session = Session.getInstance(props, null); + session.setDebug(debug); + + // Get a Store object + Store store = null; + if (url != null) { + URLName urln = new URLName(url); + store = session.getStore(urln); + store.connect(); + } else { + if (protocol != null) + store = session.getStore(protocol); + else + store = session.getStore(); + + // Connect + if (host != null || user != null || password != null) + store.connect(host, port, user, password); + else + store.connect(); + } + + printFolders("Personal", store.getPersonalNamespaces()); + printFolders("User \"" + suser + "\"", + store.getUserNamespaces(suser)); + printFolders("Shared", store.getSharedNamespaces()); + + store.close(); + } catch (Exception ex) { + System.out.println("Oops, got exception! " + ex.getMessage()); + ex.printStackTrace(); + } + System.exit(0); + } + + private static void printFolders(String name, Folder[] folders) + throws MessagingException { + System.out.println(name + " Namespace:"); + if (folders == null || folders.length == 0) { + System.out.println(" <none>"); + return; + } + for (int i = 0; i < folders.length; i++) { + String fn = folders[i].getFullName(); + if (fn.length() == 0) + fn = "<default folder>"; + try { + System.out.println(" " + fn + + ", delimiter \"" + folders[i].getSeparator() + "\""); + Folder[] fl = folders[i].list(); + if (fl.length > 0) { + System.out.println(" Subfolders:"); + for (int j = 0; j < fl.length; j++) + System.out.println(" " + fl[j].getFullName()); + } + } catch (FolderNotFoundException ex) { } + } + } +}
diff --git a/demo/src/main/java/populate.java b/demo/src/main/java/populate.java new file mode 100644 index 0000000..721c16d --- /dev/null +++ b/demo/src/main/java/populate.java
@@ -0,0 +1,231 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +import javax.mail.*; +import javax.mail.internet.*; + +/* + * Copy folder hierarchies between different Stores. This is a useful + * utility to populate new (and possibly empty) mail stores. Specify + * both the source and destination folders as URLs. + * + * @author John Mani + * @author Bill Shannon + */ + +public class populate { + + static boolean force = false; + static boolean skipSpecial = false; + static boolean clear = false; + static boolean dontPreserveFlags = false; + static boolean warn = false; + + public static void main(String argv[]) { + String srcURL = null; + String dstURL = null; + boolean debug = false; + + int optind; + + for (optind = 0; optind < argv.length; optind++) { + if (argv[optind].equals("-s")) { + srcURL = argv[++optind]; + } else if (argv[optind].equals("-d")) { + dstURL = argv[++optind]; + } else if (argv[optind].equals("-D")) { + debug = true; + } else if (argv[optind].equals("-f")) { + force = true; + } else if (argv[optind].equals("-S")) { + skipSpecial = true; + } else if (argv[optind].equals("-c")) { + clear = true; + } else if (argv[optind].equals("-P")) { + dontPreserveFlags = true; + } else if (argv[optind].equals("-W")) { + warn = true; + } else if (argv[optind].equals("--")) { + optind++; + break; + } else if (argv[optind].startsWith("-")) { + printUsage(); + System.exit(1); + } else { + break; + } + } + + try { + + if (srcURL == null || dstURL == null) { + printUsage(); + System.exit(1); + } + + Session session = Session.getInstance( + System.getProperties(), null); + session.setDebug(debug); + + // Get source folder + URLName srcURLName = new URLName(srcURL); + Folder srcFolder; + // Check if the source URL has a folder specified. If + // not, we use the default folder + if (srcURLName.getFile() == null) { + Store s = session.getStore(srcURLName); + s.connect(); + srcFolder = s.getDefaultFolder(); + } else { + srcFolder = session.getFolder(new URLName(srcURL)); + if (!srcFolder.exists()) { + System.out.println("source folder does not exist"); + srcFolder.getStore().close(); + System.exit(1); + } + } + + // Set up destination folder + URLName dstURLName = new URLName(dstURL); + Folder dstFolder; + // Check if the destination URL has a folder specified. If + // not, we use the source folder name + if (dstURLName.getFile() == null) { + Store s = session.getStore(dstURLName); + s.connect(); + dstFolder = s.getFolder(srcFolder.getName()); + } else + dstFolder = session.getFolder(dstURLName); + + if (clear && dstFolder.exists()) { + if (!dstFolder.delete(true)) { + System.out.println("couldn't delete " + + dstFolder.getFullName()); + return; + } + } + copy(srcFolder, dstFolder); + + // Close the respective stores. + srcFolder.getStore().close(); + dstFolder.getStore().close(); + + } catch (MessagingException mex) { + System.out.println(mex.getMessage()); + mex.printStackTrace(); + } + } + + private static void copy(Folder src, Folder dst) + throws MessagingException { + System.out.println("Populating " + dst.getFullName()); + + Folder ddst = dst; + Folder[] srcFolders = null; + if ((src.getType() & Folder.HOLDS_FOLDERS) != 0) + srcFolders = src.list(); + boolean srcHasChildren = srcFolders != null && srcFolders.length > 0; + + if (!dst.exists()) { + // Create it. + boolean dstHoldsOnlyFolders = false; + if (!dst.create(src.getType())) { + // might not be able to create a folder that holds both + if (!dst.create(srcHasChildren ? Folder.HOLDS_FOLDERS : + Folder.HOLDS_MESSAGES)) { + // might only be able to create one type (Gmail) + if (!dst.create(Folder.HOLDS_MESSAGES)) { + System.out.println("couldn't create " + + dst.getFullName()); + return; + } + } + dstHoldsOnlyFolders = srcHasChildren; + } + + // Copy over any messges from src to dst + if ((src.getType() & Folder.HOLDS_MESSAGES) != 0) { + src.open(Folder.READ_ONLY); + if (dstHoldsOnlyFolders) { + if (src.getMessageCount() > 0) { + System.out.println("Unable to copy messages from " + + src.getFullName() + " to " + dst.getFullName() + + " because destination holds only folders"); + } + } else + copyMessages(src, dst); + src.close(false); + } + } else { + System.out.println(dst.getFullName() + " already exists"); + // Copy over any messges from src to dst + if (force && (src.getType() & Folder.HOLDS_MESSAGES) != 0) { + src.open(Folder.READ_ONLY); + copyMessages(src, dst); + src.close(false); + } + } + + // Copy over subfolders + if (srcHasChildren) { + for (int i = 0; i < srcFolders.length; i++) { + // skip special directories? + if (skipSpecial) { + if (srcFolders[i].getName().equals("SCCS") || + srcFolders[i].getName().equals("Drafts") || + srcFolders[i].getName().equals("Trash") || + srcFolders[i].getName().equals("Shared Folders")) + continue; + } + copy(srcFolders[i], dst.getFolder(srcFolders[i].getName())); + } + } + } + + /** + * Copy message from src to dst. If dontPreserveFlags is set + * we first copy the messages to memory, clear all the flags, + * and then copy to the destination. + */ + private static void copyMessages(Folder src, Folder dst) + throws MessagingException { + Message[] msgs = src.getMessages(); + if (dontPreserveFlags) { + for (int i = 0; i < msgs.length; i++) { + MimeMessage m = new MimeMessage((MimeMessage)msgs[i]); + m.setFlags(m.getFlags(), false); + msgs[i] = m; + } + } + if (warn) { + // have to copy messages one at a time + for (int i = 0; i < msgs.length; i++) { + try { + src.copyMessages(new Message[] { msgs[i] }, dst); + } catch (MessagingException mex) { + System.out.println("WARNING: Copy of message " + (i + 1) + + " from " + src.getFullName() + + " to " + dst.getFullName() + + " failed: " + mex.toString()); + } + } + } else + src.copyMessages(msgs, dst); + } + + private static void printUsage() { + System.out.println("populate [-D] [-f] [-S] [-c] " + + "-s source_url -d dest_url"); + System.out.println("URLs are of the form: " + + "protocol://username:password@hostname/foldername"); + System.out.println("The destination URL does not need a foldername," + + " in which case, the source foldername is used"); + } +}
diff --git a/demo/src/main/java/registry.java b/demo/src/main/java/registry.java new file mode 100644 index 0000000..9529490 --- /dev/null +++ b/demo/src/main/java/registry.java
@@ -0,0 +1,151 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +import java.util.*; + +import javax.mail.*; +import javax.mail.internet.*; + +/** + * This class demonstrates how to query the registry for available + * Providers, set default providers, etc. See section 5.2 in the + * JavaMail Spec for details on how to use the registry. + * + * See the comments inline for what's happening. + * + * @author Max Spivak + */ + +public class registry { + // let's remember a few providers + static Provider _aProvider, _bProvider, _sunSMTP, _sunIMAP; + + public static void main(String[] args) { + Properties props = new Properties(); + + // set smtp and imap to be our default + // transport and store protocols, respectively + props.put("mail.transport.protocol", "smtp"); + props.put("mail.store.protocol", "imap"); + + // + props.put("mail.smtp.class", "com.sun.mail.smtp.SMTPTransport"); + props.put("mail.imap.class", "com.sun.mail.imap.IMAPStore"); + + Session session = Session.getInstance(props, null); + //session.setDebug(true); + + // Retrieve all configured providers from the Session + System.out.println("\n------ getProviders()----------"); + Provider[] providers = session.getProviders(); + for (int i = 0; i < providers.length; i++) { + System.out.println("** " + providers[i]); + + // let's remember some providers so that we can use them later + // (I'm explicitly showing multiple ways of testing Providers) + // BTW, no Provider "ACME Corp" will be found in the default + // setup b/c its not in any javamail.providers resource files + String s = null; + if (((s = providers[i].getVendor()) != null) && + s.startsWith("ACME Corp")) { + _aProvider = providers[i]; + } + + // this Provider won't be found by default either + if (providers[i].getClassName().endsWith("application.smtp")) + _bProvider = providers[i]; + + // this Provider will be found since com.sun.mail.imap.IMAPStore + // is configured in javamail.default.providers + if (providers[i].getClassName().equals("com.sun.mail.imap.IMAPStore")){ + _sunIMAP = providers[i]; + } + + // this Provider will be found as well since there is an + // Oracle SMTP transport configured by + // default in javamail.default.providers + if (((s = providers[i].getVendor()) != null) && + s.startsWith("Oracle") && + providers[i].getType() == Provider.Type.TRANSPORT && + providers[i].getProtocol().equalsIgnoreCase("smtp")) { + _sunSMTP = providers[i]; + } + } + + System.out.println("\n------ initial protocol defaults -------"); + try { + System.out.println("imap: " + session.getProvider("imap")); + System.out.println("smtp: " + session.getProvider("smtp")); + // the NNTP provider will fail since we don't have one configured + System.out.println("nntp: " + session.getProvider("nntp")); + } catch (NoSuchProviderException mex) { + System.out.println(">> This exception is OK since there is no NNTP Provider configured by default"); + mex.printStackTrace(); + } + + System.out.println("\n------ set new protocol defaults ------"); + // set some new defaults + try { + // since _aProvider isn't configured, this will fail + session.setProvider(_aProvider); // will fail + } catch (NoSuchProviderException mex) { + System.out.println(">> Exception expected: _aProvider is null"); + mex.printStackTrace(); + } + try { + // _sunIMAP provider should've configured correctly; should work + session.setProvider(_sunIMAP); + } catch (NoSuchProviderException mex) { mex.printStackTrace(); } + try { + System.out.println("imap: " + session.getProvider("imap")); + System.out.println("smtp: " + session.getProvider("smtp")); + } catch (NoSuchProviderException mex) { mex.printStackTrace(); } + + + System.out.println("\n\n----- get some stores ---------"); + // multiple ways to retrieve stores. these will print out the + // string "imap:" since its the URLName for the store + try { + System.out.println("getStore(): " + session.getStore()); + System.out.println("getStore(Provider): " + + session.getStore(_sunIMAP)); + } catch (NoSuchProviderException mex) { + mex.printStackTrace(); + } + + try { + System.out.println("getStore(imap): " + session.getStore("imap")); + // pop3 will fail since it doesn't exist + System.out.println("getStore(pop3): " + session.getStore("pop3")); + } catch (NoSuchProviderException mex) { + System.out.println(">> Exception expected: no pop3 provider"); + mex.printStackTrace(); + } + + + System.out.println("\n\n----- now for transports/addresses ---------"); + // retrieve transports; these will print out "smtp:" (like stores did) + try { + System.out.println("getTransport(): " + session.getTransport()); + System.out.println("getTransport(Provider): " + + session.getTransport(_sunSMTP)); + System.out.println("getTransport(smtp): " + + session.getTransport("smtp")); + System.out.println("getTransport(Address): " + + session.getTransport(new InternetAddress("mspivak@apilon"))); + // News will fail since there's no news provider configured + System.out.println("getTransport(News): " + + session.getTransport(new NewsAddress("rec.humor"))); + } catch (MessagingException mex) { + System.out.println(">> Exception expected: no news provider configured"); + mex.printStackTrace(); + } + } +}
diff --git a/demo/src/main/java/search.java b/demo/src/main/java/search.java new file mode 100644 index 0000000..f9bbe42 --- /dev/null +++ b/demo/src/main/java/search.java
@@ -0,0 +1,301 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +import java.util.*; +import java.io.*; +import javax.mail.*; +import javax.mail.internet.*; +import javax.mail.search.*; +import javax.activation.*; + +/* + * Search the given folder for messages matching the given + * criteria. + * + * @author John Mani + */ + +public class search { + + static String protocol = "imap"; + static String host = null; + static String user = null; + static String password = null; + static String mbox = "INBOX"; + static String url = null; + static boolean debug = false; + + public static void main(String argv[]) { + int optind; + + String subject = null; + String from = null; + boolean or = false; + boolean today = false; + int size = -1; + + for (optind = 0; optind < argv.length; optind++) { + if (argv[optind].equals("-T")) { + protocol = argv[++optind]; + } else if (argv[optind].equals("-H")) { + host = argv[++optind]; + } else if (argv[optind].equals("-U")) { + user = argv[++optind]; + } else if (argv[optind].equals("-P")) { + password = argv[++optind]; + } else if (argv[optind].equals("-or")) { + or = true; + } else if (argv[optind].equals("-D")) { + debug = true; + } else if (argv[optind].equals("-f")) { + mbox = argv[++optind]; + } else if (argv[optind].equals("-L")) { + url = argv[++optind]; + } else if (argv[optind].equals("-subject")) { + subject = argv[++optind]; + } else if (argv[optind].equals("-from")) { + from = argv[++optind]; + } else if (argv[optind].equals("-today")) { + today = true; + } else if (argv[optind].equals("-size")) { + size = Integer.parseInt(argv[++optind]); + } else if (argv[optind].equals("--")) { + optind++; + break; + } else if (argv[optind].startsWith("-")) { + System.out.println( + "Usage: search [-D] [-L url] [-T protocol] [-H host] " + + "[-U user] [-P password] [-f mailbox] " + + "[-subject subject] [-from from] [-or] [-today]"); + System.exit(1); + } else { + break; + } + } + + try { + + if ((subject == null) && (from == null) && !today && size < 0) { + System.out.println( + "Specify either -subject, -from, -today, or -size"); + System.exit(1); + } + + // Get a Properties object + Properties props = System.getProperties(); + + // Get a Session object + Session session = Session.getInstance(props, null); + session.setDebug(debug); + + // Get a Store object + Store store = null; + if (url != null) { + URLName urln = new URLName(url); + store = session.getStore(urln); + store.connect(); + } else { + if (protocol != null) + store = session.getStore(protocol); + else + store = session.getStore(); + + // Connect + if (host != null || user != null || password != null) + store.connect(host, user, password); + else + store.connect(); + } + + + // Open the Folder + + Folder folder = store.getDefaultFolder(); + if (folder == null) { + System.out.println("Cant find default namespace"); + System.exit(1); + } + + folder = folder.getFolder(mbox); + if (folder == null) { + System.out.println("Invalid folder"); + System.exit(1); + } + + folder.open(Folder.READ_ONLY); + SearchTerm term = null; + + if (subject != null) + term = new SubjectTerm(subject); + if (from != null) { + FromStringTerm fromTerm = new FromStringTerm(from); + if (term != null) { + if (or) + term = new OrTerm(term, fromTerm); + else + term = new AndTerm(term, fromTerm); + } + else + term = fromTerm; + } + if (today) { + Calendar c = Calendar.getInstance(); + c.set(Calendar.HOUR, 0); + c.set(Calendar.MINUTE, 0); + c.set(Calendar.SECOND, 0); + c.set(Calendar.MILLISECOND, 0); + c.set(Calendar.AM_PM, Calendar.AM); + ReceivedDateTerm startDateTerm = + new ReceivedDateTerm(ComparisonTerm.GE, c.getTime()); + c.add(Calendar.DATE, 1); // next day + ReceivedDateTerm endDateTerm = + new ReceivedDateTerm(ComparisonTerm.LT, c.getTime()); + SearchTerm dateTerm = new AndTerm(startDateTerm, endDateTerm); + if (term != null) { + if (or) + term = new OrTerm(term, dateTerm); + else + term = new AndTerm(term, dateTerm); + } + else + term = dateTerm; + } + + if (size >= 0) { + SizeTerm sizeTerm = new SizeTerm(ComparisonTerm.GT, size); + if (term != null) { + if (or) + term = new OrTerm(term, sizeTerm); + else + term = new AndTerm(term, sizeTerm); + } + else + term = sizeTerm; + } + + Message[] msgs = folder.search(term); + System.out.println("FOUND " + msgs.length + " MESSAGES"); + if (msgs.length == 0) // no match + System.exit(1); + + // Use a suitable FetchProfile + FetchProfile fp = new FetchProfile(); + fp.add(FetchProfile.Item.ENVELOPE); + folder.fetch(msgs, fp); + + for (int i = 0; i < msgs.length; i++) { + System.out.println("--------------------------"); + System.out.println("MESSAGE #" + (i + 1) + ":"); + dumpPart(msgs[i]); + } + + folder.close(false); + store.close(); + } catch (Exception ex) { + System.out.println("Oops, got exception! " + ex.getMessage()); + ex.printStackTrace(); + } + + System.exit(1); + } + + public static void dumpPart(Part p) throws Exception { + if (p instanceof Message) { + Message m = (Message)p; + Address[] a; + // FROM + if ((a = m.getFrom()) != null) { + for (int j = 0; j < a.length; j++) + System.out.println("FROM: " + a[j].toString()); + } + + // TO + if ((a = m.getRecipients(Message.RecipientType.TO)) != null) { + for (int j = 0; j < a.length; j++) + System.out.println("TO: " + a[j].toString()); + } + + // SUBJECT + System.out.println("SUBJECT: " + m.getSubject()); + + // DATE + Date d = m.getSentDate(); + System.out.println("SendDate: " + + (d != null ? d.toLocaleString() : "UNKNOWN")); + + // FLAGS: + Flags flags = m.getFlags(); + StringBuffer sb = new StringBuffer(); + Flags.Flag[] sf = flags.getSystemFlags(); // get the system flags + + boolean first = true; + for (int i = 0; i < sf.length; i++) { + String s; + Flags.Flag f = sf[i]; + if (f == Flags.Flag.ANSWERED) + s = "\\Answered"; + else if (f == Flags.Flag.DELETED) + s = "\\Deleted"; + else if (f == Flags.Flag.DRAFT) + s = "\\Draft"; + else if (f == Flags.Flag.FLAGGED) + s = "\\Flagged"; + else if (f == Flags.Flag.RECENT) + s = "\\Recent"; + else if (f == Flags.Flag.SEEN) + s = "\\Seen"; + else + continue; // skip it + if (first) + first = false; + else + sb.append(' '); + sb.append(s); + } + + String[] uf = flags.getUserFlags(); // get the user flag strings + for (int i = 0; i < uf.length; i++) { + if (first) + first = false; + else + sb.append(' '); + sb.append(uf[i]); + } + System.out.println("FLAGS = " + sb.toString()); + } + + System.out.println("CONTENT-TYPE: " + p.getContentType()); + + /* Dump input stream + InputStream is = ((MimeMessage)m).getInputStream(); + int c; + while ((c = is.read()) != -1) + System.out.write(c); + */ + + Object o = p.getContent(); + if (o instanceof String) { + System.out.println("This is a String"); + System.out.println((String)o); + } else if (o instanceof Multipart) { + System.out.println("This is a Multipart"); + Multipart mp = (Multipart)o; + int count = mp.getCount(); + for (int i = 0; i < count; i++) + dumpPart(mp.getBodyPart(i)); + } else if (o instanceof InputStream) { + System.out.println("This is just an input stream"); + InputStream is = (InputStream)o; + int c; + while ((c = is.read()) != -1) + System.out.write(c); + } + } +}
diff --git a/demo/src/main/java/sendfile.java b/demo/src/main/java/sendfile.java new file mode 100644 index 0000000..0f44544 --- /dev/null +++ b/demo/src/main/java/sendfile.java
@@ -0,0 +1,121 @@ +/* + * Copyright (c) 1996, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +import java.util.*; +import java.io.*; +import javax.mail.*; +import javax.mail.internet.*; +import javax.activation.*; + +/** + * sendfile will create a multipart message with the second + * block of the message being the given file.<p> + * + * This demonstrates how to use the FileDataSource to send + * a file via mail.<p> + * + * usage: <code>java sendfile <i>to from smtp file true|false</i></code> + * where <i>to</i> and <i>from</i> are the destination and + * origin email addresses, respectively, and <i>smtp</i> + * is the hostname of the machine that has smtp server + * running. <i>file</i> is the file to send. The next parameter + * either turns on or turns off debugging during sending. + * + * @author Christopher Cotton + */ +public class sendfile { + + public static void main(String[] args) { + if (args.length != 5) { + System.out.println("usage: java sendfile <to> <from> <smtp> <file> true|false"); + System.exit(1); + } + + String to = args[0]; + String from = args[1]; + String host = args[2]; + String filename = args[3]; + boolean debug = Boolean.valueOf(args[4]).booleanValue(); + String msgText1 = "Sending a file.\n"; + String subject = "Sending a file"; + + // create some properties and get the default Session + Properties props = System.getProperties(); + props.put("mail.smtp.host", host); + + Session session = Session.getInstance(props, null); + session.setDebug(debug); + + try { + // create a message + MimeMessage msg = new MimeMessage(session); + msg.setFrom(new InternetAddress(from)); + InternetAddress[] address = {new InternetAddress(to)}; + msg.setRecipients(Message.RecipientType.TO, address); + msg.setSubject(subject); + + // create and fill the first message part + MimeBodyPart mbp1 = new MimeBodyPart(); + mbp1.setText(msgText1); + + // create the second message part + MimeBodyPart mbp2 = new MimeBodyPart(); + + // attach the file to the message + mbp2.attachFile(filename); + + /* + * Use the following approach instead of the above line if + * you want to control the MIME type of the attached file. + * Normally you should never need to do this. + * + FileDataSource fds = new FileDataSource(filename) { + public String getContentType() { + return "application/octet-stream"; + } + }; + mbp2.setDataHandler(new DataHandler(fds)); + mbp2.setFileName(fds.getName()); + */ + + // create the Multipart and add its parts to it + Multipart mp = new MimeMultipart(); + mp.addBodyPart(mbp1); + mp.addBodyPart(mbp2); + + // add the Multipart to the message + msg.setContent(mp); + + // set the Date: header + msg.setSentDate(new Date()); + + /* + * If you want to control the Content-Transfer-Encoding + * of the attached file, do the following. Normally you + * should never need to do this. + * + msg.saveChanges(); + mbp2.setHeader("Content-Transfer-Encoding", "base64"); + */ + + // send the message + Transport.send(msg); + + } catch (MessagingException mex) { + mex.printStackTrace(); + Exception ex = null; + if ((ex = mex.getNextException()) != null) { + ex.printStackTrace(); + } + } catch (IOException ioex) { + ioex.printStackTrace(); + } + } +}
diff --git a/demo/src/main/java/sendhtml.java b/demo/src/main/java/sendhtml.java new file mode 100644 index 0000000..5c6410c --- /dev/null +++ b/demo/src/main/java/sendhtml.java
@@ -0,0 +1,216 @@ +/* + * Copyright (c) 1998, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +import java.io.*; +import java.util.Properties; +import java.util.Date; + +import javax.mail.*; +import javax.activation.*; +import javax.mail.internet.*; +import javax.mail.util.*; + +/** + * Demo app that shows how to construct and send a single part html + * message. Note that the same basic technique can be used to send + * data of any type. + * + * @author John Mani + * @author Bill Shannon + * @author Max Spivak + */ + +public class sendhtml { + + public static void main(String[] argv) { + new sendhtml(argv); + } + + public sendhtml(String[] argv) { + + String to, subject = null, from = null, + cc = null, bcc = null, url = null; + String mailhost = null; + String mailer = "sendhtml"; + String protocol = null, host = null, user = null, password = null; + String record = null; // name of folder in which to record mail + boolean debug = false; + BufferedReader in = + new BufferedReader(new InputStreamReader(System.in)); + int optind; + + for (optind = 0; optind < argv.length; optind++) { + if (argv[optind].equals("-T")) { + protocol = argv[++optind]; + } else if (argv[optind].equals("-H")) { + host = argv[++optind]; + } else if (argv[optind].equals("-U")) { + user = argv[++optind]; + } else if (argv[optind].equals("-P")) { + password = argv[++optind]; + } else if (argv[optind].equals("-M")) { + mailhost = argv[++optind]; + } else if (argv[optind].equals("-f")) { + record = argv[++optind]; + } else if (argv[optind].equals("-s")) { + subject = argv[++optind]; + } else if (argv[optind].equals("-o")) { // originator + from = argv[++optind]; + } else if (argv[optind].equals("-c")) { + cc = argv[++optind]; + } else if (argv[optind].equals("-b")) { + bcc = argv[++optind]; + } else if (argv[optind].equals("-L")) { + url = argv[++optind]; + } else if (argv[optind].equals("-d")) { + debug = true; + } else if (argv[optind].equals("--")) { + optind++; + break; + } else if (argv[optind].startsWith("-")) { + System.out.println( +"Usage: sendhtml [[-L store-url] | [-T prot] [-H host] [-U user] [-P passwd]]"); + System.out.println( +"\t[-s subject] [-o from-address] [-c cc-addresses] [-b bcc-addresses]"); + System.out.println( +"\t[-f record-mailbox] [-M transport-host] [-d] [address]"); + System.exit(1); + } else { + break; + } + } + + try { + if (optind < argv.length) { + // XXX - concatenate all remaining arguments + to = argv[optind]; + System.out.println("To: " + to); + } else { + System.out.print("To: "); + System.out.flush(); + to = in.readLine(); + } + if (subject == null) { + System.out.print("Subject: "); + System.out.flush(); + subject = in.readLine(); + } else { + System.out.println("Subject: " + subject); + } + + Properties props = System.getProperties(); + // XXX - could use Session.getTransport() and Transport.connect() + // XXX - assume we're using SMTP + if (mailhost != null) + props.put("mail.smtp.host", mailhost); + + // Get a Session object + Session session = Session.getInstance(props, null); + if (debug) + session.setDebug(true); + + // construct the message + Message msg = new MimeMessage(session); + if (from != null) + msg.setFrom(new InternetAddress(from)); + else + msg.setFrom(); + + msg.setRecipients(Message.RecipientType.TO, + InternetAddress.parse(to, false)); + if (cc != null) + msg.setRecipients(Message.RecipientType.CC, + InternetAddress.parse(cc, false)); + if (bcc != null) + msg.setRecipients(Message.RecipientType.BCC, + InternetAddress.parse(bcc, false)); + + msg.setSubject(subject); + + collect(in, msg); + + msg.setHeader("X-Mailer", mailer); + msg.setSentDate(new Date()); + + // send the thing off + Transport.send(msg); + + System.out.println("\nMail was sent successfully."); + + // Keep a copy, if requested. + + if (record != null) { + // Get a Store object + Store store = null; + if (url != null) { + URLName urln = new URLName(url); + store = session.getStore(urln); + store.connect(); + } else { + if (protocol != null) + store = session.getStore(protocol); + else + store = session.getStore(); + + // Connect + if (host != null || user != null || password != null) + store.connect(host, user, password); + else + store.connect(); + } + + // Get record Folder. Create if it does not exist. + Folder folder = store.getFolder(record); + if (folder == null) { + System.err.println("Can't get record folder."); + System.exit(1); + } + if (!folder.exists()) + folder.create(Folder.HOLDS_MESSAGES); + + Message[] msgs = new Message[1]; + msgs[0] = msg; + folder.appendMessages(msgs); + + System.out.println("Mail was recorded successfully."); + } + + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void collect(BufferedReader in, Message msg) + throws MessagingException, IOException { + String line; + String subject = msg.getSubject(); + StringBuffer sb = new StringBuffer(); + sb.append("<HTML>\n"); + sb.append("<HEAD>\n"); + sb.append("<TITLE>\n"); + sb.append(subject + "\n"); + sb.append("</TITLE>\n"); + sb.append("</HEAD>\n"); + + sb.append("<BODY>\n"); + sb.append("<H1>" + subject + "</H1>" + "\n"); + + while ((line = in.readLine()) != null) { + sb.append(line); + sb.append("\n"); + } + + sb.append("</BODY>\n"); + sb.append("</HTML>\n"); + + msg.setDataHandler(new DataHandler( + new ByteArrayDataSource(sb.toString(), "text/html"))); + } +}
diff --git a/demo/src/main/java/smtpsend.java b/demo/src/main/java/smtpsend.java new file mode 100644 index 0000000..2e26eb5 --- /dev/null +++ b/demo/src/main/java/smtpsend.java
@@ -0,0 +1,350 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +import java.io.*; +import java.net.InetAddress; +import java.util.Properties; +import java.util.Date; + +import javax.mail.*; +import javax.mail.internet.*; + +import com.sun.mail.smtp.*; + +/** + * Demo app that shows how to construct and send an RFC822 + * (singlepart) message. + * + * XXX - allow more than one recipient on the command line + * + * This is just a variant of msgsend.java that demonstrates use of + * some SMTP-specific features. + * + * @author Max Spivak + * @author Bill Shannon + */ + +public class smtpsend { + + /** + * Example of how to extend the SMTPTransport class. + * This example illustrates how to issue the XACT + * command before the SMTPTransport issues the DATA + * command. + * + public static class SMTPExtension extends SMTPTransport { + public SMTPExtension(Session session, URLName url) { + super(session, url); + // to check that we're being used + System.out.println("SMTPExtension: constructed"); + } + + protected synchronized OutputStream data() throws MessagingException { + if (supportsExtension("XACCOUNTING")) + issueCommand("XACT", 250); + return super.data(); + } + } + */ + + public static void main(String[] argv) { + String to, subject = null, from = null, + cc = null, bcc = null, url = null; + String mailhost = null; + String mailer = "smtpsend"; + String file = null; + String protocol = null, host = null, user = null, password = null; + String record = null; // name of folder in which to record mail + boolean debug = false; + boolean verbose = false; + boolean auth = false; + String prot = "smtp"; + BufferedReader in = + new BufferedReader(new InputStreamReader(System.in)); + int optind; + + /* + * Process command line arguments. + */ + for (optind = 0; optind < argv.length; optind++) { + if (argv[optind].equals("-T")) { + protocol = argv[++optind]; + } else if (argv[optind].equals("-H")) { + host = argv[++optind]; + } else if (argv[optind].equals("-U")) { + user = argv[++optind]; + } else if (argv[optind].equals("-P")) { + password = argv[++optind]; + } else if (argv[optind].equals("-M")) { + mailhost = argv[++optind]; + } else if (argv[optind].equals("-f")) { + record = argv[++optind]; + } else if (argv[optind].equals("-a")) { + file = argv[++optind]; + } else if (argv[optind].equals("-s")) { + subject = argv[++optind]; + } else if (argv[optind].equals("-o")) { // originator + from = argv[++optind]; + } else if (argv[optind].equals("-c")) { + cc = argv[++optind]; + } else if (argv[optind].equals("-b")) { + bcc = argv[++optind]; + } else if (argv[optind].equals("-L")) { + url = argv[++optind]; + } else if (argv[optind].equals("-d")) { + debug = true; + } else if (argv[optind].equals("-v")) { + verbose = true; + } else if (argv[optind].equals("-A")) { + auth = true; + } else if (argv[optind].equals("-S")) { + prot = "smtps"; + } else if (argv[optind].equals("--")) { + optind++; + break; + } else if (argv[optind].startsWith("-")) { + System.out.println( +"Usage: smtpsend [[-L store-url] | [-T prot] [-H host] [-U user] [-P passwd]]"); + System.out.println( +"\t[-s subject] [-o from-address] [-c cc-addresses] [-b bcc-addresses]"); + System.out.println( +"\t[-f record-mailbox] [-M transport-host] [-d] [-a attach-file]"); + System.out.println( +"\t[-v] [-A] [-S] [address]"); + System.exit(1); + } else { + break; + } + } + + try { + /* + * Prompt for To and Subject, if not specified. + */ + if (optind < argv.length) { + // XXX - concatenate all remaining arguments + to = argv[optind]; + System.out.println("To: " + to); + } else { + System.out.print("To: "); + System.out.flush(); + to = in.readLine(); + } + if (subject == null) { + System.out.print("Subject: "); + System.out.flush(); + subject = in.readLine(); + } else { + System.out.println("Subject: " + subject); + } + + /* + * Initialize the JavaMail Session. + */ + Properties props = System.getProperties(); + if (mailhost != null) + props.put("mail." + prot + ".host", mailhost); + if (auth) + props.put("mail." + prot + ".auth", "true"); + + /* + * Create a Provider representing our extended SMTP transport + * and set the property to use our provider. + * + Provider p = new Provider(Provider.Type.TRANSPORT, prot, + "smtpsend$SMTPExtension", "JavaMail demo", "no version"); + props.put("mail." + prot + ".class", "smtpsend$SMTPExtension"); + */ + + // Get a Session object + Session session = Session.getInstance(props, null); + if (debug) + session.setDebug(true); + + /* + * Register our extended SMTP transport. + * + session.addProvider(p); + */ + + /* + * Construct the message and send it. + */ + Message msg = new MimeMessage(session); + if (from != null) + msg.setFrom(new InternetAddress(from)); + else + msg.setFrom(); + + msg.setRecipients(Message.RecipientType.TO, + InternetAddress.parse(to, false)); + if (cc != null) + msg.setRecipients(Message.RecipientType.CC, + InternetAddress.parse(cc, false)); + if (bcc != null) + msg.setRecipients(Message.RecipientType.BCC, + InternetAddress.parse(bcc, false)); + + msg.setSubject(subject); + + String text = collect(in); + + if (file != null) { + // Attach the specified file. + // We need a multipart message to hold the attachment. + MimeBodyPart mbp1 = new MimeBodyPart(); + mbp1.setText(text); + MimeBodyPart mbp2 = new MimeBodyPart(); + mbp2.attachFile(file); + MimeMultipart mp = new MimeMultipart(); + mp.addBodyPart(mbp1); + mp.addBodyPart(mbp2); + msg.setContent(mp); + } else { + // If the desired charset is known, you can use + // setText(text, charset) + msg.setText(text); + } + + msg.setHeader("X-Mailer", mailer); + msg.setSentDate(new Date()); + + // send the thing off + /* + * The simple way to send a message is this: + * + Transport.send(msg); + * + * But we're going to use some SMTP-specific features for + * demonstration purposes so we need to manage the Transport + * object explicitly. + */ + SMTPTransport t = + (SMTPTransport)session.getTransport(prot); + try { + if (auth) + t.connect(mailhost, user, password); + else + t.connect(); + t.sendMessage(msg, msg.getAllRecipients()); + } finally { + if (verbose) + System.out.println("Response: " + + t.getLastServerResponse()); + t.close(); + } + + System.out.println("\nMail was sent successfully."); + + /* + * Save a copy of the message, if requested. + */ + if (record != null) { + // Get a Store object + Store store = null; + if (url != null) { + URLName urln = new URLName(url); + store = session.getStore(urln); + store.connect(); + } else { + if (protocol != null) + store = session.getStore(protocol); + else + store = session.getStore(); + + // Connect + if (host != null || user != null || password != null) + store.connect(host, user, password); + else + store.connect(); + } + + // Get record Folder. Create if it does not exist. + Folder folder = store.getFolder(record); + if (folder == null) { + System.err.println("Can't get record folder."); + System.exit(1); + } + if (!folder.exists()) + folder.create(Folder.HOLDS_MESSAGES); + + Message[] msgs = new Message[1]; + msgs[0] = msg; + folder.appendMessages(msgs); + + System.out.println("Mail was recorded successfully."); + } + + } catch (Exception e) { + /* + * Handle SMTP-specific exceptions. + */ + if (e instanceof SendFailedException) { + MessagingException sfe = (MessagingException)e; + if (sfe instanceof SMTPSendFailedException) { + SMTPSendFailedException ssfe = + (SMTPSendFailedException)sfe; + System.out.println("SMTP SEND FAILED:"); + if (verbose) + System.out.println(ssfe.toString()); + System.out.println(" Command: " + ssfe.getCommand()); + System.out.println(" RetCode: " + ssfe.getReturnCode()); + System.out.println(" Response: " + ssfe.getMessage()); + } else { + if (verbose) + System.out.println("Send failed: " + sfe.toString()); + } + Exception ne; + while ((ne = sfe.getNextException()) != null && + ne instanceof MessagingException) { + sfe = (MessagingException)ne; + if (sfe instanceof SMTPAddressFailedException) { + SMTPAddressFailedException ssfe = + (SMTPAddressFailedException)sfe; + System.out.println("ADDRESS FAILED:"); + if (verbose) + System.out.println(ssfe.toString()); + System.out.println(" Address: " + ssfe.getAddress()); + System.out.println(" Command: " + ssfe.getCommand()); + System.out.println(" RetCode: " + ssfe.getReturnCode()); + System.out.println(" Response: " + ssfe.getMessage()); + } else if (sfe instanceof SMTPAddressSucceededException) { + System.out.println("ADDRESS SUCCEEDED:"); + SMTPAddressSucceededException ssfe = + (SMTPAddressSucceededException)sfe; + if (verbose) + System.out.println(ssfe.toString()); + System.out.println(" Address: " + ssfe.getAddress()); + System.out.println(" Command: " + ssfe.getCommand()); + System.out.println(" RetCode: " + ssfe.getReturnCode()); + System.out.println(" Response: " + ssfe.getMessage()); + } + } + } else { + System.out.println("Got Exception: " + e); + if (verbose) + e.printStackTrace(); + } + } + } + + /** + * Read the body of the message until EOF. + */ + public static String collect(BufferedReader in) throws IOException { + String line; + StringBuffer sb = new StringBuffer(); + while ((line = in.readLine()) != null) { + sb.append(line); + sb.append("\n"); + } + return sb.toString(); + } +}
diff --git a/demo/src/main/java/transport.java b/demo/src/main/java/transport.java new file mode 100644 index 0000000..f3cfbd7 --- /dev/null +++ b/demo/src/main/java/transport.java
@@ -0,0 +1,212 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +import java.util.*; +import javax.mail.*; +import javax.mail.internet.*; +import javax.mail.event.*; +import javax.activation.*; + +/** + * transport is a simple program that creates a message, explicitly + * retrieves a Transport from the session based on the type of the + * address (it's InternetAddress, so SMTP will be used) and sends + * the message. + * + * usage: <code>java transport <i>"toaddr1[, toaddr2]*" from smtphost + * true|false</i></code><br> + * where <i>to</i> and <i>from</i> are the destination and + * origin email addresses, respectively, and <i>smtphost</i> + * is the hostname of the machine that has the smtp server + * running. The <i>to</i> addresses can be either a single email + * address or a comma-separated list of email addresses in + * quotes, i.e. "joe@machine, jane, max@server.com" + * The last parameter either turns on or turns off + * debugging during sending. + * + * @author Max Spivak + */ +public class transport implements ConnectionListener, TransportListener { + static String msgText = "This is a message body.\nHere's the second line."; + static String msgText2 = "\nThis was sent by transport.java demo program."; + + public static void main(String[] args) { + Properties props = System.getProperties(); + // parse the arguments + InternetAddress[] addrs = null; + InternetAddress from; + boolean debug = false; + if (args.length != 4) { + usage(); + return; + } else { + props.put("mail.smtp.host", args[2]); + if (args[3].equals("true")) { + debug = true; + } else if (args[3].equals("false")) { + debug = false; + } else { + usage(); + return; + } + + // parse the destination addresses + try { + addrs = InternetAddress.parse(args[0], false); + from = new InternetAddress(args[1]); + } catch (AddressException aex) { + System.out.println("Invalid Address"); + aex.printStackTrace(); + return; + } + } + // create some properties and get a Session + Session session = Session.getInstance(props, null); + session.setDebug(debug); + + transport t = new transport(); + t.go(session, addrs, from); + } + + public transport() {} + + public void go(Session session, InternetAddress[] toAddr, + InternetAddress from) { + Transport trans = null; + + try { + // create a message + Message msg = new MimeMessage(session); + msg.setFrom(from); + msg.setRecipients(Message.RecipientType.TO, toAddr); + msg.setSubject("JavaMail APIs transport.java Test"); + msg.setSentDate(new Date()); // Date: header + msg.setContent(msgText+msgText2, "text/plain"); + msg.saveChanges(); + + // get the smtp transport for the address + trans = session.getTransport(toAddr[0]); + + // register ourselves as listener for ConnectionEvents + // and TransportEvents + trans.addConnectionListener(this); + trans.addTransportListener(this); + + // connect the transport + trans.connect(); + + // send the message + trans.sendMessage(msg, toAddr); + + // give the EventQueue enough time to fire its events + try {Thread.sleep(5);}catch(InterruptedException e) {} + + } catch (MessagingException mex) { + // give the EventQueue enough time to fire its events + try {Thread.sleep(5);}catch(InterruptedException e) {} + + System.out.println("Sending failed with exception:"); + mex.printStackTrace(); + System.out.println(); + Exception ex = mex; + do { + if (ex instanceof SendFailedException) { + SendFailedException sfex = (SendFailedException)ex; + Address[] invalid = sfex.getInvalidAddresses(); + if (invalid != null) { + System.out.println(" ** Invalid Addresses"); + for (int i = 0; i < invalid.length; i++) + System.out.println(" " + invalid[i]); + } + Address[] validUnsent = sfex.getValidUnsentAddresses(); + if (validUnsent != null) { + System.out.println(" ** ValidUnsent Addresses"); + for (int i = 0; i < validUnsent.length; i++) + System.out.println(" "+validUnsent[i]); + } + Address[] validSent = sfex.getValidSentAddresses(); + if (validSent != null) { + System.out.println(" ** ValidSent Addresses"); + for (int i = 0; i < validSent.length; i++) + System.out.println(" "+validSent[i]); + } + } + System.out.println(); + if (ex instanceof MessagingException) + ex = ((MessagingException)ex).getNextException(); + else + ex = null; + } while (ex != null); + } finally { + try { + // close the transport + if (trans != null) + trans.close(); + } catch (MessagingException mex) { /* ignore */ } + } + } + + // implement ConnectionListener interface + public void opened(ConnectionEvent e) { + System.out.println(">>> ConnectionListener.opened()"); + } + public void disconnected(ConnectionEvent e) {} + public void closed(ConnectionEvent e) { + System.out.println(">>> ConnectionListener.closed()"); + } + + // implement TransportListener interface + public void messageDelivered(TransportEvent e) { + System.out.println(">>> TransportListener.messageDelivered()."); + System.out.println(" Valid Addresses:"); + Address[] valid = e.getValidSentAddresses(); + if (valid != null) { + for (int i = 0; i < valid.length; i++) + System.out.println(" " + valid[i]); + } + } + public void messageNotDelivered(TransportEvent e) { + System.out.println(">>> TransportListener.messageNotDelivered()."); + System.out.println(" Invalid Addresses:"); + Address[] invalid = e.getInvalidAddresses(); + if (invalid != null) { + for (int i = 0; i < invalid.length; i++) + System.out.println(" " + invalid[i]); + } + } + public void messagePartiallyDelivered(TransportEvent e) { + System.out.println(">>> TransportListener.messagePartiallyDelivered()."); + System.out.println(" Valid Addresses:"); + Address[] valid = e.getValidSentAddresses(); + if (valid != null) { + for (int i = 0; i < valid.length; i++) + System.out.println(" " + valid[i]); + } + System.out.println(" Valid Unsent Addresses:"); + Address[] unsent = e.getValidUnsentAddresses(); + if (unsent != null) { + for (int i = 0; i < unsent.length; i++) + System.out.println(" " + unsent[i]); + } + System.out.println(" Invalid Addresses:"); + Address[] invalid = e.getInvalidAddresses(); + if (invalid != null) { + for (int i = 0; i < invalid.length; i++) + System.out.println(" " + invalid[i]); + } + } + + private static void usage() { + System.out.println( + "usage: java transport \"<to1>[, <to2>]*\" <from> <smtp> true|false"); + System.out.println( + "example: java transport \"joe@machine, jane\" senderaddr smtphost false"); + } +}
diff --git a/demo/src/main/java/uidmsgshow.java b/demo/src/main/java/uidmsgshow.java new file mode 100644 index 0000000..71e4d14 --- /dev/null +++ b/demo/src/main/java/uidmsgshow.java
@@ -0,0 +1,287 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +import java.util.*; +import java.io.*; +import javax.mail.*; +import javax.mail.internet.*; +import javax.activation.*; + +/* + * Demo app that exercises the Message interfaces. + * Access message given its UID. + * Show information about and contents of messages. + * + * @author John Mani + * @author Bill Shannon + */ + +public class uidmsgshow { + + static String protocol; + static String host = null; + static String user = null; + static String password = null; + static String mbox = "INBOX"; + static String url = null; + static boolean verbose = false; + + public static void main(String argv[]) { + long uid = -1; + int optind; + + for (optind = 0; optind < argv.length; optind++) { + if (argv[optind].equals("-T")) { + protocol = argv[++optind]; + } else if (argv[optind].equals("-H")) { + host = argv[++optind]; + } else if (argv[optind].equals("-U")) { + user = argv[++optind]; + } else if (argv[optind].equals("-P")) { + password = argv[++optind]; + } else if (argv[optind].equals("-v")) { + verbose = true; + } else if (argv[optind].equals("-f")) { + mbox = argv[++optind]; + } else if (argv[optind].equals("-L")) { + url = argv[++optind]; + } else if (argv[optind].equals("--")) { + optind++; + break; + } else if (argv[optind].startsWith("-")) { + System.out.println("Usage: uidmsgshow [-L url] [-T protocol] [-H host] [-U user] [-P password] [-f mailbox] [uid] [-v]"); + System.exit(1); + } else { + break; + } + } + + try { + if (optind < argv.length) + uid = Long.parseLong(argv[optind]); + + // Get a Properties object + Properties props = System.getProperties(); + + // Get a Session object + Session session = Session.getInstance(props, null); + // session.setDebug(true); + + // Get a Store object + Store store = null; + if (url != null) { + URLName urln = new URLName(url); + store = session.getStore(urln); + store.connect(); + } else { + if (protocol != null) + store = session.getStore(protocol); + else + store = session.getStore(); + + // Connect + if (host != null || user != null || password != null) + store.connect(host, user, password); + else + store.connect(); + } + + + // Open the Folder + + Folder folder = store.getDefaultFolder(); + if (folder == null) { + System.out.println("No default folder"); + System.exit(1); + } + + folder = folder.getFolder(mbox); + if (!folder.exists()) { + System.out.println(mbox + " does not exist"); + System.exit(1); + } + + if (!(folder instanceof UIDFolder)) { + System.out.println( + "This Provider or this folder does not support UIDs"); + System.exit(1); + } + + UIDFolder ufolder = (UIDFolder)folder; + + folder.open(Folder.READ_WRITE); + int totalMessages = folder.getMessageCount(); + + if (totalMessages == 0) { + System.out.println("Empty folder"); + folder.close(false); + store.close(); + System.exit(1); + } + + if (verbose) { + int newMessages = folder.getNewMessageCount(); + System.out.println("Total messages = " + totalMessages); + System.out.println("New messages = " + newMessages); + System.out.println("-------------------------------"); + } + + if (uid == -1) { + // Attributes & Flags for ALL messages .. + Message[] msgs = + ufolder.getMessagesByUID(1, UIDFolder.LASTUID); + + // Use a suitable FetchProfile + FetchProfile fp = new FetchProfile(); + fp.add(FetchProfile.Item.ENVELOPE); + fp.add(FetchProfile.Item.FLAGS); + fp.add("X-Mailer"); + folder.fetch(msgs, fp); + + for (int i = 0; i < msgs.length; i++) { + System.out.println("--------------------------"); + System.out.println("MESSAGE UID #" + + ufolder.getUID(msgs[i]) + ":"); + dumpEnvelope(msgs[i]); + // dumpPart(msgs[i]); + } + } else { + System.out.println("Getting message UID: " + uid); + Message m = ufolder.getMessageByUID(uid); + if (m != null) + dumpPart(m); + else + System.out.println( + "This Message does not exist on this folder"); + } + + folder.close(false); + store.close(); + } catch (Exception ex) { + System.out.println("Oops, got exception! " + ex.getMessage()); + ex.printStackTrace(); + } + System.exit(1); + } + + public static void dumpPart(Part p) throws Exception { + if (p instanceof Message) + dumpEnvelope((Message)p); + + /* Dump input stream + InputStream is = new BufferedInputStream(p.getInputStream()); + int c; + while ((c = is.read()) != -1) + System.out.write(c); + */ + + System.out.println("CONTENT-TYPE: " + p.getContentType()); + + Object o = p.getContent(); + if (o instanceof String) { + System.out.println("This is a String"); + System.out.println("---------------------------"); + System.out.println((String)o); + } else if (o instanceof Multipart) { + System.out.println("This is a Multipart"); + System.out.println("---------------------------"); + Multipart mp = (Multipart)o; + int count = mp.getCount(); + for (int i = 0; i < count; i++) + dumpPart(mp.getBodyPart(i)); + } else if (o instanceof Message) { + System.out.println("This is a Nested Message"); + System.out.println("---------------------------"); + dumpPart((Part)o); + } else if (o instanceof InputStream) { + System.out.println("This is just an input stream"); + System.out.println("---------------------------"); + InputStream is = (InputStream)o; + int c; + while ((c = is.read()) != -1) + System.out.write(c); + } + } + + public static void dumpEnvelope(Message m) throws Exception { + System.out.println("This is the message envelope"); + System.out.println("---------------------------"); + Address[] a; + // FROM + if ((a = m.getFrom()) != null) { + for (int j = 0; j < a.length; j++) + System.out.println("FROM: " + a[j].toString()); + } + + // TO + if ((a = m.getRecipients(Message.RecipientType.TO)) != null) { + for (int j = 0; j < a.length; j++) + System.out.println("TO: " + a[j].toString()); + } + + // SUBJECT + System.out.println("SUBJECT: " + m.getSubject()); + + // DATE + Date d = m.getSentDate(); + System.out.println("SendDate: " + + (d != null ? d.toString() : "UNKNOWN")); + + // SIZE + System.out.println("Size: " + m.getSize()); + + // FLAGS: + Flags flags = m.getFlags(); + StringBuffer sb = new StringBuffer(); + Flags.Flag[] sf = flags.getSystemFlags(); // get the system flags + + boolean first = true; + for (int i = 0; i < sf.length; i++) { + String s; + Flags.Flag f = sf[i]; + if (f == Flags.Flag.ANSWERED) + s = "\\Answered"; + else if (f == Flags.Flag.DELETED) + s = "\\Deleted"; + else if (f == Flags.Flag.DRAFT) + s = "\\Draft"; + else if (f == Flags.Flag.FLAGGED) + s = "\\Flagged"; + else if (f == Flags.Flag.RECENT) + s = "\\Recent"; + else if (f == Flags.Flag.SEEN) + s = "\\Seen"; + else + continue; // skip it + if (first) + first = false; + else + sb.append(' '); + sb.append(s); + } + + String[] uf = flags.getUserFlags(); // get the user flag strings + for (int i = 0; i < uf.length; i++) { + if (first) + first = false; + else + sb.append(' '); + sb.append(uf[i]); + } + System.out.println("FLAGS = " + sb.toString()); + + // X-MAILER + String[] hdrs = m.getHeader("X-Mailer"); + if (hdrs != null) + System.out.println("X-Mailer: " + hdrs[0]); + else + System.out.println("X-Mailer NOT available"); + } +}
diff --git a/doc/release/ApacheJServ.html b/doc/release/ApacheJServ.html new file mode 100644 index 0000000..47c85d2 --- /dev/null +++ b/doc/release/ApacheJServ.html
@@ -0,0 +1,98 @@ +<!doctype html public "-//w3c//dtd html 4.0 transitional//en"> +<html> +<head> +<!-- + + Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + + <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1"> + <meta name="Author" content="JavaSoftware"> + <meta name="GENERATOR" content="Mozilla/4.61 [en] (WinNT; U) [Netscape]"> + <title>ApacheJServ</title> +</head> +<body> +If you don't already have Apache web server and Apache JServ +module installed and running, +<br>you'll need to download them from the <a href="http://www.apache.org">Apache +Software Foundation</a> and install them. We +<br>were able to find binary versions of Apache web server for both the +Solaris and Windows NT +<br>operating systems, and Apache JServ 1.1.2 for Windows NT. However, +for Solaris, +<br>Apache JServ needed to be built from the source. +<p>(on Solaris) +<blockquote>Apache Web Server 1.3.9 (<a href="http://httpd.apache.org/dist/binaries/">binary</a>) +<br>Apache JServ 1.1.2 (built from <a href="http://java.apache.org/">source</a>)</blockquote> +(on NT) +<blockquote>Apache Web Server 1.3.14 (<a href="http://httpd.apache.org/dist/binaries/">binary</a>) +<br>Apache JServ 1.1.2 (<a href="http://java.apache.org/">binary</a>)</blockquote> +To run the JavaMailServlet, you must add the JavaMail and JavaBeans Activation +Framework +<br>jar files to the web server's system classpath. Apache JServ also requires +that the +<br>Java Servlet Development Kit 2.0 (<a href="http://java.sun.com/products/servlet/archive.html">JSDK2.0</a>) +is installed and also added to the web server's +<br>system classpath. JSDK2.0 is a reference implementation of the Servlet +API Specification Version 2.0. +<p>When running Apache JServ in automatic mode, add wrapper.classpath properties +for each +<br>of these jar files to the servlet engine's propeties file. The default +engine properties file is +<br>jserv.properties and is usually found in Apache JServ's 'conf' directory. +Wrapper properties +<br>are used to automatically start the servlet engine. +<p>(on Solaris) +<blockquote>wrapper.classpath=/files/java/lib/JSDK2.0/lib/jsdk.jar +<br>wrapper.classpath=/files/java/lib/javamail-1.2/mail.jar +<br>wrapper.classpath=/files/java/lib/jaf-1.0.1/activation.jar</blockquote> +(on NT) +<blockquote>wrapper.classpath=d:\java\lib\jsdk2.0\lib\jsdk.jar +<br>wrapper.classpath=d:\java\lib\javamail-1.2\mail.jar +<br>wrapper.classpath=d:\java\lib\jaf-1.0.1\activation.jar</blockquote> +When running Apache JServ in manual mode, add these jar files to the CLASSPATH +<br>environment variable (this is documented in the JavaMail README file +and additional +<br>Windows NT information is provided <a href="classpath-NT.html">here</a>). +<p>Once this is done, restart the web server. +<br> +<p>Additional Notes +<blockquote>One thing to watch out for is how the web server is referenced +when using the +<br>JavaMailServlet on this web server. It is important to use the +configured server +<br>name (i.e. the name assigned with the ServerName directive) in the +URL. If the +<br>configured server name includes a domain, it must be referenced including +the +<br>domain, even if the JavaMail.html page can be accessed without it. +<p>For example, Apache is configured with the ServerName set to 'shadygrove'. +If the JavaMail demo is referenced by: +<p> http://shadygrove.east/example/JavaMail.html +<p>and the initail reference to the JavaMailServlet being: +<p> http://shadygrove.east/example/JavaMailServlet +<p>the user is successfully logged in. However, when an attempt is made +to access the INBOX, the user is instructed to login because there is no +current session. When this occurs, note that the URL referencing the JavaMailServlet +no longer contains the domain originally used but the configured ServerName. +<p> http://shadygrove/example/JavaMailServlet +<br> +<p>Also note that Apache JServ does not use a SecurityManager or enforce +any Java +<br>security policy.</blockquote> + +</body> +</html>
diff --git a/doc/release/CHANGES.txt b/doc/release/CHANGES.txt new file mode 100644 index 0000000..165141f --- /dev/null +++ b/doc/release/CHANGES.txt
@@ -0,0 +1,804 @@ +You can find more information about each bug number by visiting the +Issue Tracker and looking up each bug you're interested in. + +Bug IDs that start with "E" can be found in the GitHub Issue Tracker +for the Eclipse EE4J JavaMail project (after removing the "E"): + + https://github.com/eclipse-ee4j/javamail/issues/<bug-number> + +Bug IDs that start with "GH" can be found in the GitHub Issue Tracker +(after removing the "GH"): + + https://github.com/javaee/javamail/issues/<bug-number> + +Bug IDs that start with "G" can be found in the GlassFish Issue Tracker +(after removing the "G"): + + https://github.com/javaee/glassfish/issues/<bug-number> + +Seven digit bug numbers are from the old Sun bug database, which is no +longer available. + + + CHANGES IN THE 1.6.3 RELEASE + ---------------------------- +The following bugs have been fixed in the 1.6.3 release. + +E 348 Change Maven coordinates to match EE4J and Jakarta EE conventions + + + CHANGES IN THE 1.6.2 RELEASE + ---------------------------- +The following bugs have been fixed in the 1.6.2 release. + +GH 306 Infinite loop parsing invalid ID response +GH 307 StringIndexOutOfBoundsException when *.proxy.host contains a colon +GH 309 Multipart Content-Transfer-Encoding trailing space +GH 310 Android app fails to build with JavaMail 1.6.1 +GH 314 InternetAddress fails to detect illegal square brackets in local part +GH 315 empty Content-Transfer-Encoding header causes IOException +GH 316 starttls.enable documentation should reference starttls.required prop +GH 317 use System.lineSeparator() instead of System.getProperty(...) +GH 321 URLName.getURL() returns incorrect url. +GH 322 Dots in local part of emails not handled properly +GH 323 Support loading protocol providers using ServiceLoader +GH 326 Apply relaxed Content-Disposition parsing to Content-Disposition params +GH 330 Attachment filename is ignored +GH 332 http proxy support should support authenticating to the proxy server +GH 333 search with Unicode char throws BadCommandException with UTF8=ALLOW +GH 334 gimap set labels error with some non english characters + + + CHANGES IN THE 1.6.1 RELEASE + ---------------------------- +The following bugs have been fixed in the 1.6.1 release. + +GH 262 Some IMAP servers send EXPUNGE responses for unknown messages +GH 278 BODYSTRUCTURE Parser fails on specific IMAP Server response +GH 283 clean up connections when closing IMAPStore +GH 287 Allow relaxed Content-Disposition parsing +GH 289 use a different IMAP tag prefix for each connection +GH 291 define JDK 9 module name for JavaMail +GH 296 HTTP proxy support needs to use HTTP/1.1 + + + CHANGES IN THE 1.6.0 RELEASE + ---------------------------- +The following bugs have been fixed in the 1.6.0 release. + +GH 75 MimeMultipart should throw ParseException for parsing errors +GH 77 MimeMessage.updateHeaders should set the Date header if not already set +GH 93 Support addressing i18n via RFC 6530/6531/6532 +GH 104 The UIDFolder interface should have a getter for UIDNEXT +GH 135 MailHandler should choose a better default subject formatter. +GH 159 Store, Transport, and Folder should implement AutoCloseable +GH 174 MailDateFormat changes for version 1.6 +GH 183 Fix javac warnings +GH 209 fails to parse some fetch response that has space before final ')' +GH 214 IMAP doesn't handle illegal CAPABILITY response after LOGIN/AUTHENTICATE +GH 226 MailSessionDefinition should use the Repeatable annotation for Java EE 8 +GH 227 IdleManager fails on Android +GH 228 Test fails: javax.mail.internet.GetLocalAddressTest +GH 229 Tests fail: com.sun.mail.util.WriteTimeoutSocketTest +GH 230 MboxFolder.expunge can corrupt mailbox file +GH 231 CompactFormatter should handle overridden Throwable.toString methods +GH 232 Update public API to use generics +GH 233 Malformed IMAP FETCH response throws the wrong exception +GH 234 RFC822.SIZE > 2GB isn't handled +GH 237 Protocol#command method call readResponse after IOException is thrown +GH 238 Possible NPE in Status.<init> line 96 +GH 239 MailHandler should support 'login' verify type. +GH 240 MailHandler support for non-multipart messages +GH 241 use of YoungerTerm/OlderTerm on server without WITHIN support fails +GH 244 The UIDFolder interface should have a MAXUID constant +GH 245 java.io.IOException: No content when reading msg with empty attachment +GH 247 look for resource files in <java.home>/conf on JDK 1.9 +GH 248 MimeUtility should treat GB2312 as one of the supersets GBK or GB18030 +GH 249 Flags convenience methods +GH 250 SMTP support for the CHUNKING extension of RFC 3030 +GH 251 MimeUtility.unfold squashes multiple spaces +GH 252 JavaMail PLAIN authentication should implement RFC 4616 +GH 253 Support connecting through web proxy servers +GH 256 support UIDPLUS UIDNOTSTICKY response code +GH 257 SASL authentication should always allow UTF-8 username and password +GH 258 android-activation MANIFEST has empty Bundle-SymbolicName + + + CHANGES IN THE 1.5.6 RELEASE + ---------------------------- +The following bugs have been fixed in the 1.5.6 release. + +GH 199 Support LogRecord.setMillis being deprecated in JDK 9 +GH 200 Logging should support LogRecord.getInstant +GH 202 Create common super class for logging tests +GH 205 NPE by APOP detection when no greeting banner +GH 206 Make IMAPProtocol.handleLoginResult protected +GH 207 InternetAddress.parse fails for valid domain literal address +GH 210 unsolicited FETCH response *must* invalidate X-GM-LABELS in cache +GH 211 MimeBodyPart.isMimeType returns false if type header can't be parsed +GH 213 NPE in Tomcat ClassLoader causes Session.getInstance to fail +GH 215 Deadlock in IMAPFolder.doProtocolCommand() +GH 216 InternetAddress.getLocalAddress should use + InetAddress.getCanonicalHostName +GH 217 Store finalizers should not talk to server +GH 219 MailHandler verify should load additional content handlers +GH 220 NullPointerException if SASL is enabled on Android +GH 221 write timeouts don't work with SSL on Android +GH 222 JavaMail allows injection of unwanted headers +GH 223 Message.setRecipient(type, null) should remove recipients + + + CHANGES IN THE 1.5.5 RELEASE + ---------------------------- +The following bugs have been fixed in the 1.5.5 release. + +GH 168 add support for setting GMail labels on messages +GH 169 Add spam filter for use with MailHandler. +GH 170 Address MailDateFormat issues +GH 172 Typo in "mail.stmp.sendpartial" +GH 173 mail.mime.encodefilename property should override RFC 2231 encoding +GH 176 IMAP should support a mail.imap.auth.mechanisms property like SMTP +GH 177 setting mail.<protocol>.auth.mechanisms should override + mail.<protocol>.auth.<mechanism>.disable +GH 178 add support for OAuth 2.0 without SASL +GH 179 capability() command doesn't properly transform errors +GH 180 MailHandler needs better support for stateful filters. +GH 181 add support for IMAP login referrals (RFC 2221) +GH 182 whitespace line at beginning confuses InternetHeaders +GH 184 Eliminate legacy classes +GH 185 IndexOutOfBoundsException reading IMAP literal when connection fails +GH 186 IdleManager dies with CancelledKeyException +GH 187 IdleManager can deadlock when not busy +GH 188 IMAP Folder methods throw runtime exceptions when connection drops +GH 189 InternetAddress doesn't detect some illegal newlines +GH 190 Status class doesn't decode mailbox name +GH 191 add support for IMAP COMPRESS extension (RFC 4978) +GH 194 Empty Gmail X-GM-LABELS list is misparsed +GH 195 IMAPMessage.getReceivedDate should check if receivedDate is present + before loading envelope +GH 196 CollectorFormatter descending order data race +GH 198 off-by-1 error in Response.readStringList causes early termination of + parsing FETCH response +GH 201 INTERNALDATE FetchProfile Item +GH 203 Exchange returns NIL instead of "" for empty parameter, causing NPE + + + CHANGES IN THE 1.5.4 RELEASE + ---------------------------- +The following bugs have been fixed in the 1.5.4 release. + +GH 149 Include elapsed time, thread id, and sequence for logging formatters. +GH 153 MailHandlerTest does not check field is static or final +GH 157 IdleManager can deadlock with frequent notifications +GH 158 IdleManager can deadlock when connection fails +GH 160 IMAP provider should support the MOVE extension (RFC 6851) +GH 162 MODSEQ should be stored in IMAPMessage if CONDSTORE is enabled +GH 163 Space character lost from end of quoted-printable body parts +GH 164 GmailMessage extensions are not cached after implicit FETCH +GH 165 IMAP message sets should be sorted in cases where order doesn't matter +GH 166 ID command shouldn't escape NIL value +GH 167 Make IMAPProtocol class extendable + + + CHANGES IN THE 1.5.3 RELEASE + ---------------------------- +The following bugs have been fixed in the 1.5.3 release. + +GH 122 Make constructor of POP3Folder protected to allow subclassing +GH 123 calling IdleManager.watch twice on same folder fails +GH 124 NPE in IMAPFolder.copyUIDMessages when COPYUID not returned +GH 127 Message-Id leaks current user/hostname of the Java process (security) +GH 128 IMAP idle breaks interrupt flag +GH 129 Date search terms result in wrong greater-than SEARCH commands for IMAP +GH 131 address similar to (x)<y>(z) will throw StringIndexOutOfBoundsException +GH 132 Update logging demos to use the new 1.5.2 features +GH 133 Use classloader ergonomics in the MailHandler +GH 137 ArrayIndexOutOfBoundsException in IMAPFolder.copyUIDMessages +GH 138 attachment filenames aren't being encoded by default +GH 139 Include javadoc example formats for logging. +GH 141 SharedFileInputStream has problems with 2GB+ files +GH 143 MimeBodyPart with copied DataHandler doesn't always set encoding +GH 144 skip unusable Store and Transport classes +GH 145 long parameter values should be split using RFC 2231 +GH 146 javax.mail.Authenticator thread safety +GH 148 Modify MailHandler to support Google App Engine. +GH 150 EXPUNGE response during UID FETCH breaks UID->seqnum mapping +GH 151 ArrayIndexOutOfBoundsException caused by out-of-range IMAP responses +GH 154 write timeouts don't work with a custom SSL socket factory +GH 155 SMTP SASL DIGEST-MD5 fails on postfix since the last reply sent is "*" + + + CHANGES IN THE 1.5.2 RELEASE + ---------------------------- +The following bugs have been fixed in the 1.5.2 release. + +GH 57 allow IMAP search to throw SearchException when search is too complex +GH 88 NullPointerException at IMAPFolder#getMessagesByUID if msg not found +GH 89 add option to use canonical host name for SASL +GH 90 IMAP astring parsing incorrect +GH 91 SMTP SASL support doesn't handle authentication failure properly +GH 92 SASL authentication failures should not try other methods +GH 94 add OAuth2 support to JavaMail +GH 95 IMAP failures during close can leave connection unusable +GH 96 need way to monitor IMAP responses +GH 97 MimeUtility.encodeText() does not work with Unicode surrogate pairs +GH 105 IMAP alerts and notifications are not sent during authentication +GH 106 add ability to fetch and cache entire IMAP message +GH 107 add ability to return server-specific STATUS responses +GH 108 add ability to set "peek" flag for all IMAP messages +GH 109 add ability to specify scope of event queue +GH 110 add ability to specify an Executor to process events +GH 113 handle multiple IMAP BODY elements in a single FETCH response +GH 115 add more efficient way to monitor multiple folders for new messages +GH 116 Include a subject formatter for the logging package +GH 118 Broken equals in URLName, NewsAddress +GH 119 NullPointerException in ContentType.match +GH 120 hashCode of two equals instances does not match for ModifiedSinceTerm, + YoungerTerm, OlderTerm +GH 121 NullPointerException in InternetAddress + + + CHANGES IN THE 1.5.1 RELEASE + ---------------------------- +The following bugs have been fixed in the 1.5.1 release. + +GH 66 Wrong "source" version in build.properties +GH 67 IMAP provider should support the QRESYNC extension (RFC 5162) +GH 68 IMAP provider should support the WITHIN search extension (RFC 5032) +GH 69 JavaMail does not handle write timeouts +GH 70 method fill() isn't synchronized correctly in SharedFileInputStream +GH 71 NullPointerException in MimeUtility#quote() +GH 72 support RFC 2359 COPYUID response code +GH 73 Empty FROM Field causes Exception +GH 74 Filename isn't parsed correctly if charset is not set +GH 76 copying a DataHandler from a parsed message to a new message fails +GH 78 MimeMessage does not unfold address headers before parsing them +GH 80 When using XGWTRUSTEDAPP mechanism, LOGIN should not be issued if no + authzid is specified +GH 82 support empty IMAP ENVELOPE address list instead of NIL +GH 83 JavaMail should support the IMAP ID extension +GH 84 Typo: "mechansims" +GH 86 Exchange returns out of range message numbers for SEARCH +GH 87 require extra permission to create default Session with SecurityManager + + + CHANGES IN THE 1.5.0 RELEASE + ---------------------------- +The following bugs have been fixed in the 1.5.0 release. + +GH 37 add FetchProfile.Item.SIZE +GH 38 fix protected fields in final classes in javax.mail.search +GH 39 add MimeMultipart(String subtype, BodyPart... bps) constructor +GH 40 exceptions should support exception chaining +GH 41 ParameterList needs to support use by IMAP +GH 42 ContentType.toString & ContentDisposition.toString shouldn't return null +GH 44 add Transport.send(msg, username, password) method +GH 45 add MimeMessage.setFrom(String) method +GH 46 add Message.getSession() method +GH 47 MimeBodyPart.attachFile should set the disposition to ATTACHMENT +GH 48 add MimeMessage.reply(replyToAll, setAnswered) method +GH 49 add "next" methods to HeaderTokenizer to help parsing bad headers +GH 51 add @MailSessionDefinition and @MailSessionDefinitions for Java EE 7 +GH 52 make cachedContent field protected in MimeMessage and MimeBodyPart +GH 53 make MimeMultipart fields protected to allow subclassing +GH 55 need simple way to override MIME type and encoding of attachment +GH 56 enable RFC 2231 support by default +GH 62 Exception when parsing bad address with unclosed quote in mail header +GH 64 when failing to connect to a server, provide more detail in exception + + + CHANGES IN THE 1.4.7 RELEASE + ---------------------------- +The following bugs have been fixed in the 1.4.7 release. + +GH 60 NullPointerException when accessing the content of a message attachment +GH 61 IMAPProtocol.sasllogin uses old constructor for IMAPSaslAuthenticator + + + CHANGES IN THE 1.4.6 RELEASE + ---------------------------- +The following bugs have been fixed in the 1.4.6 release. + +GH 28 IMAP Store socket leak on connect with ProtocolException +GH 29 STARTTLS when already using SSL may cause connection to fail +GH 30 Yahoo: Error reading emails containing "undisclosed recipients" +GH 32 Infinite loop if Quota information is not correctly reported by server +GH 54 Issue in decoding filename with charset iso-2022-jp from Mime header + based on rfc 2231 +<no id> add mail.imap.ignorebodystructuresize to work around server bugs +<no id> add "gimap" EXPERIMENTAL Gmail IMAP provider +<no id> add isSSL() method to all protocol providers +<no id> add support for debug output using java.util.logging +<no id> avoid NullPointerException when encountering a bad Content-Type + + + CHANGES IN THE 1.4.5 RELEASE + ---------------------------- +The following bugs have been fixed in the 1.4.5 release. + +7021190 MimeMessage.setRecipients(type, String) does not accept null address +GH 15 ArrayIndexOutOfBoundsException for some IMAP protocol errors +GH 17 MultipartReport.setReport and setDeliveryStatus are broken +GH 18 Wrong representation of CR/LF are appended to the attachment +GH 19 SSL Re-Negotiation Problem with checkserveridentity=true +GH 22 Thread safety in javax.mail.PasswordAuthentication +GH 25 Add SOCKS5 support +GH 27 wrong message accessed when another client expunges and adds messages +<no id> properly handle timeouts during SSL negotiation +<no id> free MIME headers in IMAPMessage.invalidateHeaders +<no id> fix exception in POP3Message when reading file cached content twice +<no id> suppress auth info in debug output, unless mail.debug.auth=true +<no id> better handle timeouts from POP3 server, throwing FolderClosedException +<no id> add com.sun.mail.util.ReadableMime to allow reading raw MIME data +<no id> work around Gmail IMAP bug with nested messages +<no id> close the SMTP connection if there's an I/O error + + + CHANGES IN THE 1.4.4 RELEASE + ---------------------------- +The following bugs have been fixed in the 1.4.4 release. + +5057742 javax.mail.internet.MimeMessage.addFrom() violates RFC2822 +6778568 lower memory usage of POP3 parsing by buffering to disk +6905730 MimeMessage.parse() is very slow on malformed message content +6910675 IMAP provider can lose track of message sequence numbers +6928566 Header violates RFC 2822 for multiple calls of addRecipient(s) +6995537 work around this JDK bug that causes iso-2022-jp decoding problems +G 11069 update the mail.jar manifest to include DynamicImport-Package +GH 10 make sure socket is closed if SMTP connect fails +GH 12 Multiparts do not parse correctly in presence of legacy Mac line endings +GH 14 InputStreams are not closed in MimeMultipart +<no id> add mail.mime.windowsfilenames System property to handle IE6 breakage +<no id> properly disable TOP if POP3 CAPA response doesn't include it +<no id> add mail.pop3.disablecapa property to disable use of the CAPA command +<no id> fix support for Properties objects with default Properties objects +<no id> integrate NTLM support, no longer needs jcifs.jar +<no id> add mail.pop3.cachewriteto property, default false +<no id> add mail.pop3.filecache.enable property for caching messages in tmp file +<no id> add mail.mime.ignorewhitespacelines property, default false +<no id> add support for IMAP UNSELECT command +<no id> add mail.mime.contentypehandler System property, to clean Content-Type +<no id> add mail.mime.allowencodedmessages System property +<no id> add support for SASL authentication to SMTP provider +<no id> add SMTPSenderFailedException to indicate problems with sender address +<no id> ignore encoding for composite content when writing message +<no id> cache POP3 content using java.lang.ref.SoftReference, + set mail.pop3.keepmessagecontent to true to disable +<no id> add POP3 PIPELINING support +<no id> reduce POP3 memory usage, especially if server supports pipelining +<no id> add support for IMAP SORT extension (RFC 5256) to IMAPFolder +<no id> add demo classes to support non-MIME messages from Outlook +<no id> fix deadlock when using IMAP fetch and IDLE +<no id> add support for mail.smtp.auth.<mechanism>.disable properties +<no id> fix deadlock when accessing IMAP messages while doing a fetch + + + CHANGES IN THE 1.4.3 RELEASE + ---------------------------- +The following bugs have been fixed in the 1.4.3 release. + +6829124 IMAPFolder deadlock using IMAP IDLE +6850882 IMAPMessage returns wrong getMessageNumber() from messageRemovedEvent +6857090 JavaMail is not sending HELO / EHLO according to specs +6872072 QPEncoderStream write method eats up trailing white space of a string +6875367 LineOutputStream wraps IOException instead of throwing it directly +6890265 SMTPTransport does not close socket if STARTTLS is req'd but not sup'd +G 9941 SMTPTransport violates RFC 2821 in HELO command +GH 2 InternetAddress verifies domain per RFC1034 instead of RFC822 in strict +GH 4 added NTLM authentication support for SMTP and IMAP, see NTLMNOTES.txt +<no id> add starttls support to POP3 +<no id> add mail.transport.protocol.<address-type> property +<no id> fail POP3Folder.open if STAT command fails +<no id> fix POP3Folder.isOpen if POP3 server fails and is then reconnected +<no id> better handle modifying messages created from input streams +<no id> include server error message in exception when SMTP authentication fails +<no id> com.sun.mail.util.logging.MailHandler contributed by Jason Mehrens +<no id> add mail.smtp.noop.strict property, default true +<no id> add mail.<protocol>.ssl.trust property to list hosts to be trusted +<no id> work around buggy IMAP servers that don't quote mbox name in STATUS resp + + + CHANGES IN THE 1.4.2 RELEASE + ---------------------------- +The following bugs have been fixed in the 1.4.2 release. + +6621377 unexpected result when uuencode data has any line starting with + "END" characters +6629213 base64 encoder sometimes omits CRLF +6670275 headers may not end up on top where they belong +6671855 list on IMAP folder that can contain both messages and folders + might fail if folder is open +6672359 SMTPTransport notifying both partially delivered and + not delivered methods +6676257 cannot specify two custom ssl socket factories for starttls usage +6679333 missing quotes around zero length parameter values +6720896 add mail.mime.uudecode.ignoreerrors system property +6720896 add mail.mime.uudecode.ignoremissingbeginend system property +6730637 deadlocks in IMAP provider when connections fail +6738454 deadlock when connection is broken +6738468 javadocs use fully qualified names +6797756 StringIndexOutOfBoundsError in InternetAddress.parseHeader() +6799810 getReplyTo() returns zero length array when ReplyTo hdr has no value +G 3929 Inconsistent synchronization in com.sun.mail.iap.Protocol +G 4997 BASE64DecoderStream.skip (etc) skips the wrong number of octets +G 5189 Can't specify SSLSocketFactory for STARTTLS in Javamail 1.4 +G 5861 add mail.<protocol>.starttls.required prop to require use of STARTTLS +<no id> ignore socket timeout while waiting in IMAP IDLE +<no id> fix bug in MailDateFormat parsing in non-lenient mode +<no id> add mail.mime.multipart.allowempty system property to handle (illegal) + empty multiparts (see javax.mail.internet.MimeMultipart) +<no id> add mail.mime.multipart.ignoreexistingboundaryparameter system property + to allow parsing multiparts with incorrect boundary parameters +<no id> handle address of the form "Undisclosed-Recipients:;" +<no id> add com.sun.mail.util.DecodingException to distinguish decoding errors +<no id> add mail.mime.ignoreunknownencoding system property (see MimeUtility) +<no id> ignore errors from SMTP RSET command +<no id> InternetAddress - detect more errors when strict, accept more when not +<no id> add mail.<protocol>.socketFactory and .ssl.socketFactory properties +<no id> add mail.<protocol>.ssl.enable property +<no id> add mail.<protocol>.ssl.checkserveridentity prop for RFC 2595 checks +<no id> add com.sun.mail.util.MailSSLSocketFactory class +<no id> fix possible NPE in MimeMessage if flags is not set in copy constructor +<no id> SMTP I/O failure incorrectly reports valid sent addresses +<no id> avoid creating IMAPMessage objects until they're actually needed +<no id> IMAPStore.isConnected might return true even though not connected +<no id> add support for Message Delivery Notifications (RFC 3798) to dsn.jar +<no id> if mail.mime.parameters.strict=false, param vals can start with specials + + + CHANGES IN THE 1.4.1 RELEASE + ---------------------------- +The following bugs have been fixed in the 1.4.1 release. + +4107594 IMAP implementation should use the IDLE extension if available +4119871 MimeMessage.reply() should set the "References" header +6228377 IMAPFolder's setFlags method handles user flags incorrectly +6423701 Problem with using OrTerm when the protocol is IMAP +6431207 SMTP is adding extra CRLF to message content +6447295 IMAPMessage fails to return Content-Language from bodystructure +6447799 encoded text not decoded even when mail.mime.decodetext.strict is false +6447801 MimeBodyPart.writeTo reencodes data unnecessarily +6456422 NullPointerException in smtptransport when sending MimeMessages + with no encoding +6456444 MimeMessages created from stream are not correctly handled + with allow8bitmime +6478460 java.lang.ArrayIndexOutOfBoundsException: 0 >= 0 in MultipartReport +6506794 ProtocolException not correctly treated in IMAPStore +6517273 encoded parameters not decoded when using IMAP +6538483 JavaMail fails in Turkish locale +6569311 Deadlock in IMAP attachment handling +6604571 Folder.hasNewMessages hangs with some IMAP servers when folder is closed +<no id> fix performance bug in base64 encoder; now even faster! +<no id> throw MessageRemovedException from getContent for IMAP messages +<no id> MimeUtility.decodeText should not discard trailing whitespace +<no id> SSLSocketFactory should be used for imap and smtp STARTTLS commands +<no id> added mail.<prot>.ssl.protocols and mail.<prot>.ssl.ciphersuites props +<no id> fix bug in mapping IMAP UIDs to msgs when some msgs have been expunged +<no id> MimeMultipart failed to parse stream before adding/removing body parts +<no id> if IMAP folder is open, assume it exists, don't ask again +<no id> avoid unnecessary copies of the data in ByteArrayDataSource +<no id> add mail.mime.applefilenames to work around filename encoding bug +<no id> support decoding multi-segment parameter names per RFC 2231 +<no id> make sure Message-ID is really unique (GlassFish Issue 3064) +<no id> do SMTP authentication if connect is called with username and password + even if mail.smtp.auth is not set + + + CHANGES IN THE 1.4 RELEASE + -------------------------- +The following bugs have been fixed in the 1.4 release. + +4107342 parameterList class should support non US-ASCII parameters +4252273 support the IMAP UIDPLUS extension +4377727 allow applications to dynamically register address type mappings +4403733 MimeMessage read from a byte stream loses modifications +4623517 add ByteArrayDataSource class +4820923 JavaMail loads SocketFactories with wrong classloader +4971381 add mail.mime.multipart.ignoremissingendboundary System property +6300765 add MimePart.setText(text, charset, subtype) method +6300768 add mail.mime.encodefilename and decodefilename properties +6300771 add Service.connect(user, password) +6300811 add MimeMultipart.isComplete() method +6300814 add mail.mime.multipart.ignoremissingboundaryparameter property +6300828 add MimeMultipart getPreamble and setPreamble methods +6300831 add MimeMessage.updateMessageID() protected method +6300833 add MimeMessage.createMimeMessage() protected method +6300834 make the "part" field of MimePartDataSource protected +6301381 folder.getSeparator should not require the folder to exist +6301386 add PreencodedMimeBodyPart class +6301390 add MimeBodyPart attachFile and saveFile methods +6302118 add MimeUtility fold and unfold methods +6302832 allow more control over headers in InternetHeaders object +6302835 allow applications to dynamically register new protocol providers +6304051 standard interface for Stores that support quotas +6304189 add SharedByteArrayInputStream class +6304193 add SharedFileInputStream class +6332559 REGRESSION: Bug in JavaMail (1.3.3 !) base64 decoder +6378822 Transport.isConnected() conflicts with Sendmail NOOP check +6401071 Deadlock in IMAP attachment handling +<no id> handle very large IMAP responses more efficiently +<no id> changed default for mail.smtp.quitwait to true +<no id> mailcap multipart entry is a JAF 1.1 fallback entry +<no id> improve MIME multipart parsing performance by 30% - 40% +<no id> add com.sun.mail.dsn package for parsing multipart/report DSN messages + + + CHANGES IN THE 1.3.3 RELEASE + ---------------------------- +The following bugs have been fixed in the 1.3.3 release. + +4239782 add IMAPFolder.getUIDNext +4288393 add IMAPMessage.setPeek to allow reading message without setting SEEN +6214426 POP3Folder.isOpen may return false even though folder is open +6214448 IMAPStore.isConnected may return true even though server is down +6236588 Duplicate Message IDs are generated when mutiple threads create messages +6287582 ArrayIndexOutOfBoundsException when "Sender" field exists with no value +6288399 IMAP Problem parsing bad envelope address format +<no id> improve base64 encoding performance 5X (thanks to John Freeman) +<no id> improve base64 decoding performance 3X +<no id> ignore invalid encodings for composite MIME parts +<no id> add mail.mime.multipart.ignoremissingboundaryparameter +<no id> if IMAP store times out, force folders closed without waiting +<no id> don't check if an IMAP folder exists before subscribing/unsubscribing +<no id> add IMAPMessage.getSender(), getInReplyTo() and getContentLanguage() +<no id> add IMAPFolder.getAttributes to retrieve LIST response attributes +<no id> add IMAPStore.hasCapability to check for IMAP server CAPABILITY strings +<no id> add IMAPMessage.invalidateHeaders for memory management +<no id> when opening IMAP folder, don't do LIST before SELECT +<no id> add mail.pop3.disabletop property to disable use of the TOP command +<no id> add mail.pop3.forgettopheaders property to forget headers from TOP cmd +<no id> add POP3Folder.getSizes() method to return sizes of all messages +<no id> add POP3Folder.listCommand() method to return raw results of LIST cmd +<no id> add SMTPTransport.connect(Socket) to enable ATRN support in server + + + CHANGES IN THE 1.3.2 RELEASE + ---------------------------- +The following bugs have been fixed in the 1.3.2 release. + +4358984 POP3 provider should support APOP, courtesy of "chamness" +4711696 Mapping of nested Exceptions of a SendFailedException +4863399 JavaMail should support specifying the SMTP bind address +4900116 NotifyResponseHandler in Protocol.java throws an ArrayIndexOutOfBoundExc +4924077 folder.getDeletedMessageCount() reports number of undeleted messages +4934814 SASL authentication doesn't default to server specified realm +4945852 Folder exists() function does not handle well folder names that + contains * or % +4945868 Potential infinite loop in com.sun.mail.imap.protocol.BODY +4945901 Folder.copyMessages() throws wrong exception in case of deleted messages +4971383 [RFE] javamail should allow easy access to last smtp response +4971391 BASE64DecoderStream handling error in encoded streams is too strict +4996040 SharedInputStream stream closing policy is inconsistent +4996543 IndexOutOfBoundsException when using SharedInputStream +4996863 in the com.sun.mail.iap.Response bitfield constant "BAD" is set wrongly +6041271 APPEND does not consider DST when computing timezone offset +6067307 Mime-Version capitalization should match MIME spec +6172894 MIME messages with missing end boundary are not reported as an error + (added mail.mime.multipart.ignoremissingendboundary System property) +<no id> accommodate some RFC3501 IMAP protocol changes +<no id> support RFC822 group lists when parsing IMAP address lists +<no id> don't read past end of IMAP part, for buggy servers that don't handle it +<no id> fix IMAP NAMESPACE support +<no id> allow different SMTPTransport objects to have different localhost names +<no id> make sure server is really alive in POP3Folder.isOpen() +<no id> support RFC2554 AUTH= submitter via mail.smtp.submitter and + SMTPMessage.setSubmitter +<no id> added SMTPSendFailedException, SMTPAddressFailedException, and + SMTPAddressSucceededException +<no id> mail.smtp.reportsuccess causes an exception to be thrown even on + successful sends, allowing access to the return codes for each address +<no id> fix IMAP isSubscribed in case where LSUB returns a \Noselect folder +<no id> parse invalid messages with non-ASCII characters in boundary string +<no id> add IMAP AUTH=PLAIN support, courtesy of Sandy McArthur +<no id> add SSL support to all protocols, see SSLNOTES.txt for details +<no id> add STARTTLS support to IMAP and SMTP protocols, see SSLNOTES.txt +<no id> handle IMAP email addresses composed of empty strings +<no id> add SASL support to IMAP provider +<no id> rename mail.stmp.saslrealm to mail.smtp.sasl.realm + + + CHANGES IN THE 1.3.1 FCS RELEASE + -------------------------------- +The following bugs have been fixed in the 1.3.1 release. + +4416417 IMAP alerts and notifications are not sent in all cases - more fixes +4702410 header formatting incorrect for long multibyte +4707106 AuthenticationFailedException not thrown in some cases with POP3 +4708655 IMAPNestedMessage.getContent without partialfetch +4709848 message_rfc822 DataContentHandler can cause NPE +4711606 uudecoder fails when reading more than a byte at a time +4726447 InternetHeaders.getHeader() doc. doesn't document null pointer return +4726629 Java Mail very slow with large attachment +4741812 IMAPFolder can deadlock +4750514 REGRESSION: MimeBodyPart.getContent fails on image/gif if no X11 present +4750519 using SSL, SocketFetcher.getSocket0() throws incorrect Exception +4762643 JavaMail does not support search in all message's parts. +4780255 Message subject not encoded according to 'mail.mime.charset' property +4787814 accessibility 508 non-compliance: api/javax/mail/Session.html +4790700 JavaMail Store.connect() throws wrong exception when already connected +4820025 IMAPStore.getPersonalNamespaces throws a ProtocolException +4874787 InternetAddress.toUnicodeString throws NPE, personal not initialized +4882554 Line breaks in subject text break message format +<no id> don't close connection if open fails, put it back in the pool +<no id> don't always fetch entire envelope in IMAPMessage.getSize +<no id> fixed demo webapp to work with servlet 2.3 and newer +<no id> add DIGEST-MD5 support to SMTP provider, courtesy of Dean Gibson +<no id> added mail.smtp.quitwait property to wait for response to QUIT +<no id> added mail.imap.auth.login.disable prop to disable AUTHENTICATE LOGIN + + + CHANGES IN THE 1.3 FCS RELEASE + ------------------------------ +The following bugs have been fixed in the 1.3 release. + +4112002 IMAP provider hangs if APPEND is prohibited +4201203 I18N: Incorrectly encoded MIME header can't be decoded + (set the *System* property "mail.mime.decodetext.strict" to "false") +4413498 InternetHeaders should add Received headers in front +4416417 IMAP alerts and notifications are not sent in all cases +4483125 Multi-line mail header processing is slow +4483158 null pointer exception for MessageContext.getMessage() +4483206 Please add a public POP3 TOP method in the next release of the POP3 api +4484098 IMAP PREAUTH does not work +4516973 doPrivileged blocks needed for javamail +4517683 new Flags("FOO").contains("FOO") fails +4517686 want JavaMail-specific debug output stream +4638743 JavaMail does not properly parse dates containing folding white space +4638741 JavaMail does not handle in-spec Internet group addresses properly +4650940 InternetAddress parsing should be more tolerant of bad addresses +4650949 wrong encoding chosen for non-text data in rare cases +4650952 should be able to extract group address members +4672308 InternetAddress.toString () throws a NullPointerException after creation +4679516 "NO" Response from IMAP server causes NPE from getSubject() +4684040 Calling Folder.fetch twice may cause to header duplication +<no id> make uudecoder more tolerant of incorrect input +<no id> improve performance of SMTP for small messages +<no id> handle connection failure during open of POP3 folder +<no id> ensure ASCII, not EBCDIC output for POP3 protocol on IBM mainframes +<no id> add POP3Message.invalidate method to invalidate cached message data +<no id> fix thread safety bug in date formatting when appending to IMAP folders +<no id> fix parsing bug in QUOTA support +<no id> add mail.imap.allowreadonlyselect property to support shared mailboxes +<no id> use thread's context class loader for loading classes +<no id> add IMAPFolder.FetchProfileItem.HEADER and SIZE +<no id> don't try to logout store connection twice +<no id> IMAPFolder.close(false) read-only folder doesn't need to EXAMINE first +<no id> add support for group addresses to SMTP transport +<no id> use builtin defaults to allow JavaMail to work in netscape 4 +<no id> tolerate trailing semicolon in Content-Type header (requires JAF 1.0.2) +<no id> add x-uue as another synonym for uuencode Content-Transfer-Encoding +<no id> set default charset for text parts +<no id> properly escape CRLF in MimeUtility.quote +<no id> fix NPE in MessagingException.getMessage + + + CHANGES IN THE 1.2 FCS RELEASE + ------------------------------ +The following bugs have been fixed in the 1.2 release. + +4107752 need MimeMessage(MimeMessage msg) constructor to allow copying message +4112065 Need API to list and set/remove ACLs on folders (IMAP-specific) +4119681 MimeMessage should allow creation of light-weight messages +4124022 Two connections required to IMAP server to open a folder +4124840 A mechanism to get the raw (unencoded) data from a MimePart is needed +4126013 javax.mail.search terms should be serializable +4129743 MimeMessage.parse() and MimeMessage.modified should be protected +4132029 SMTP Submit is limited to 7bit; does not use ESMTP/8BITMIME +4140579 MimeUtility.encode() does not allow for filename when using UUEncode +4163360 Need a suitable MessagingException subclass to indicate read-only folder +4181144 InternetAddress should be Cloneable +4230553 AuthenticationFailedException should include error message from server +4259211 exception constructors inconsistent +4266390 MailDateFormat class should be part of the public API +4281729 AddressStringTerm.match bug +4319895 POP3 provider doesn't enforce read-only mode +4319957 Ambiguous documentation in Javamail 1.1.3 early access edition +4328824 string based methods to add recipients +4328826 getDefaultInstance method with no Authenticator +4330580 MimeMultipart.getBodyPart(String CID) throws exception +4333694 NullPointerException in version 1.1.1 of the POP3 Provider +4336435 quoted right angle bracket not handled in InternetAddress +4339203 writeTo should automatically call saveChanges +4340648 MimeUtility.getEncoding(DataHandler) method should be public +4364827 Support IMAP NAMESPACE extension +4366373 ContentDisposition class should be public +4371862 improve performance of MimeMessage +4372700 ParameterList.toString method should allow for returning folded results +<no id> most control characters must be encoded, not sent as "7bit" +<no id> appending very large message to IMAP folder uses too much memory +<no id> changed multipart boundary generation to not include email address +<no id> support IMAP LITERAL+ extension (RFC 2088) +<no id> allow SMTP multiline reponses with no text (e.g., "250-") +<no id> fix many potential locking bugs in IMAP provider +<no id> add mail.smtp.sendpartial property to send msg with some bad addresses +<no id> add mail.pop3.rsetbeforequit property (see NOTEST.txt) +<no id> throw IllegalStateException instead of MessagingException when folder + is not open (or closed, as appropriate) +<no id> added support for IMAP QUOTA extension +<no id> added support for IMAP PREAUTH greeting response +<no id> added DataContentHandler for text/xml data +<no id> added SMTPMessage class to specify SMTP options on a per-message basis +<no id> added javadocs for Sun protocol providers +<no id> mail.pop3.message.class property allows POP3Message class to be replaced +<no id> mail.{smtp,imap,pop3}.connectiontimeout property for connection timeouts + + + + CHANGES IN THE 1.1.3 FCS RELEASE + -------------------------------- +The following bugs have been fixed in the 1.1.3 release. + +4248755 Problem loading a custom provider +4249046 don't put space after SMTP FROM: and TO: +4249058 IMAP appendMessages() should include the message Flags as well. +4263185 JavaMail and JAF can't find properties when used as std ext +4271714 DEBUG message always printed when providers loaded from <java.home>/lib +4276080 getEncoding method doesn't parse MIME header +4279603 RFC822 and MIME specials does not include period "." +4292793 using Message.reply(true) twice on the same IMAP message causes NPE +4293605 javax.mail.MimeMultipart boundary string contains invalid characters +4296711 JavaMail IMAP provider doesn't set SEEN on messages with 0 length body +4305687 JavaMail speaking SMTP fails to quote dots in some cases +<no id> add support for SMTP Authentication, see NOTES.txt +<no id> add support for SMTP Delivery Status Notification, see NOTES.txt +<no id> SMTP return address is now set in mail.smtp.from +<no id> fix bug in InternetAddress when parsing ``<x@foo.com> (Mr. X)'' +<no id> improve javadocs in many places based on questions to javamail@sun.com +<no id> avoid JDK 1.2 bug 4208960 in SimpleTimeZone.getOffset +<no id> canonicalize the URLName before fetching saved PasswordAuthentication +<no id> convert SimpleClient to swing 1.1 package names (javax.swing.*) +<no id> folder.getURLName() should return native separator, not /, per RFC 2192 +<no id> use JDK 1.2 ClassLoader.getResources() method (if available) to find all + META-INF/javamail.providers and META-INF/javamail.address.map files in + the CLASSPATH, to better support protocol provider jar files +<no id> encode/decode username and password fields of URLName to allow (e.g.) + usernames with "@" +<no id> added DataContentHandler for text/html, to simplify creation of HTML + messages and body parts +<no id> remove escapes from personal name when parsing in InternetAddress +<no id> cache results of IMAP STATUS command for 1 second, to improve + performance of back-to-back calls to getMessageCount, + getNewMessageCount, getUnreadMessageCount +<no id> fix InternetHeaders Enumeration to work even if hasMoreElements isn't + called +<no id> support mail.smtp.timeout property + + + + CHANGES IN THE 1.1.2 FCS RELEASE + -------------------------------- +The following bugs have been fixed in the 1.1.2 release. + +<no id> Fixed bug where IMAP server connection hangs around even though + the connect() method failed. +4199595 force quoted-printable encoding of long text lines +<no id> fix bug in SMTP output that sometimes duplicated "." +<no id> close SMTP transport on I/O error +4230541 don't send SMTP NOOP unnecessarily +4216666 IMAP provider INTERNALDATE formatter error, causing + problems during appendMessages() +4227888 IMAP provider does not honor the UID item in its FetchProfile + + + CHANGES IN THE 1.1.1 FCS RELEASE + -------------------------------- +The following bugs have been fixed in the 1.1.1 release. + +4181143 personal can't be null in constructor +4134273 more careful & picky address parsing in InternetAddress parser +4183700 SMTPTransport fails to close socket under certain situations. +<no id> IMAP provider retains appended message's internal date during + Folder.appendMessages(Message[] m); +<no id> More efficient server-side search for MessageIDTerm in the + IMAP provider +<no id> Fix RFC2047 decoding bug in InternetAddress.getPersonal() +<no id> Be more tolerant of illegally formatted dates in date parsing. +<no id> ignore empty lines in loadMappings +<no id> forgot to use javaCharset() in MimeUtility.decodeWord() +<no id> Allow addresses without hostnames in InternetAddress parser +<no id> unrecognized charsets can cause IllegalArgument runtime + exception when invoking getContent(). +<no id> Authentication failure when connecting to Sun IMAP server. +<no id> Reset SMTP connection after invalid address to allow future + sends to succeed +<no id> Any response to an SMTP NOOP command means we're still connected
diff --git a/doc/release/COMPAT.txt b/doc/release/COMPAT.txt new file mode 100644 index 0000000..05a7ec2 --- /dev/null +++ b/doc/release/COMPAT.txt
@@ -0,0 +1,377 @@ + COMPATIBILITY NOTES + =================== + + JavaMail(TM) API ${mail.version} release + ------------------------------ + +The JavaMail 1.6 specification is fully compatible with the JavaMail +1.5 specification, with the exceptions listed below. + +In addition, changes in the implementation may impact +applications that depend on behavior beyond what is defined by the +JavaMail specification, or that use features specific to the reference +implementation. This note summarizes potential compatibility issues +with this release of the JavaMail API. + + +-- JavaMail 1.6.3 -- + +- Maven coordinates for JavaMail changed. + + JavaMail has moved to the Eclipse Foundation as part of the EE4J + project, and will be included in the Jakarta EE specification. + The Maven coordinates for JavaMail are now: + + - com.sun.mail:jakarta.mail - complete JavaMail jar file + - jakarta.mail:jakarta.mail-api - API jar file for compiling only + + When updating dependencies to use JavaMail 1.6.3 or newer, the + artifactId will need to be changed as above. The APIs are + otherwise compatible. + +- Java module system name changed. + + Experimental support for the Java platform module system was + added in the previous release using the name "java.mail" for the + API module (mailapi.jar). In this release the name of that + module is changed to "jakarta.mail". + + +-- JavaMail 1.6.2 -- + +- Protocol providers now loaded using the java.util.ServiceLoader mechanism. + + All the protocol providers included with JavaMail, as well as + third party providers, can use the Service Loader mechanism to + configure the provider, instead of using the + META-INF/javamail.default.providers or META-INF/javamail.providers + file. These JavaMail provider configuration files are only read + if the ServiceLoader fails to find a provider that supports the + requested protocol. This may have the effect of changing the order + in which providers are loaded, which should only be an issue if + multiple providers support the same protocol name. + + +-- JavaMail 1.6.1 -- + +- com.sun.mail.util.logging no longer included in mailapi.jar. + + If you're using mailapi.jar and depend on the com.sun.mail.util.logging + classes, you'll need to include the logging-mailhandler.jar file in + your application class path. + + + +-- JavaMail 1.6 -- + +- Public API updated to use generics + + Methods in the following classes have been updated to use generics. + This change should be binary compatible but may require source + code changes in applications. + + javax.mail.Multipart + javax.mail.Part + javax.mail.Service + javax.mail.internet.InternetHeaders + javax.mail.internet.MimeBodyPart + javax.mail.internet.MimeMessage + javax.mail.internet.MimePart + javax.mail.internet.ParameterList + +- JDK 1.7 or newer required + + The JavaMail reference implementation now requires JDK 1.7 + or newer. It is expected that the large majority of users + are already using JDK 1.7 or newer. + +- MailDateFormat changes + + The following changes to MailDateFormat may impact applications: + + * parse(String) now throws a ParseException on invalid input instead + of returning null, to conform to the DateFormat contract + * the following methods now throw UnsupportedOperationException + to prevent tampering with MailDateFormat's internals: + + get2DigitYearStart + applyLocalizedPattern + applyPattern + set2DigitYearStart + setDateFormatSymbols + +- method added to UIDFolder interface + + The UIDFolder interface has a new getUIDNext() method. Any + classes implementing the UIDFolder interface will need to add + this method. The IMAPFolder class implements the UIDFolder + interface and has provided this method for some time. + +- YoungerTerm, OlderTerm, and ModifiedSinceTerm fall back to local searching + + In general, SearchTerms that are not understood by the IMAP + provider or not supported by the IMAP server fall back to doing + local searching. The IMAP-specific YoungerTerm, OlderTerm, and + ModifiedSinceTerm were instead throwing an exception if the server + didn't support the extension that the search term depended on. + This has been fixed to fall back to doing the search locally. + This behavior can be changed by setting the property + "mail.imap.throwsearchexception" to "true" to cause an exception + to be thrown if the server can't perform the search. + + + +-- JavaMail 1.5.6 -- + +- finalizers close sockets abruptly + + It's important for finalizers to close an open socket + connection to prevent file descriptor leaks. Previously the + finalizers for the IMAP and POP3 providers would try to close + the connection cleanly, which could result in a timeout waiting + for the server. They now close the connection without + performing any socket I/O, which may result in an unclean + shutdown when seen from the server. Applications should always + close Stores and Folders when done with them to avoid the need + for the finalizer to do this cleanup. The Session property + "mail.<protocol>.finalizecleanclose" can be set to "true" to + force the connection to be closed cleanly in the finalizer. + +- InternetAddress.getLocalAddress uses canonical host name + + The InternetAddress.getLocalAddress method now uses the + java.net.InetAddress.getCanonicalHostName method if neither the + "mail.from" nor "mail.host" properties have been set. The System + property "mail.mime.address.usecanonicalhostname" can be set to + "false" to revert to the previous behavior. + +- SMTPTransport.sasllogin no longer public + + The SMTPTransport.sasllogin method should never have been + declared public. It's used internally when SASL authentication + is requested; applications should not use the method directly. + + + +-- JavaMail 1.5.4 -- + +- Idlemanager.watch no longer throws IOException + + The IdleManager.watch method was declared to throw IOException, + but never actually threw it. The declaraction has been changed, + which will cause a source incompatibility for code expecting to + catch IOException when calling the watch method. + + + +-- JavaMail 1.5.3 -- + +- Date search terms result in wrong greater-than SEARCH commands for IMAP + + The IMAP SentDateTerm and ReceivedDateTerm greater-than comparison + was actually doing a greater-than-or-equal-to comparison. This + has been fixed in the 1.5.3 release, but programs that accidentally + relied on the old behavior may get different results. + +- Authenticator is now synchronized + + The call to the Authenticator's getPasswordAuthentication method + is now synchronized so that the state stored in the Authenticator + is safe if multiple threads try to use the Authenticator + simultaneously. If the application's getPasswordAuthentication + method blocks, it will prevent other threads in the same + Session from using the Authenticator. + + + +-- JavaMail 1.5 -- + +- RFC 2231 parameter encoding/decoding enabled by default + + The System properties "mail.mime.decodeparameters" and + "mail.mime.encodeparameters" now default to true instead of false. + Now that most mailers support RFC 2231, this is expected to + increase interoperability, although in rare cases, and especially + when dealing with older mailers, this may cause problems. + Parameters may appear encoded, and with a different name, + than what the receiver is expecting. + +- ContentType.toString and ContentDisposition.toString never return null + + These methods were previously documented to return null in + error cases when the fields of the class were not valid. + These methods now return an empty string in these cases, to + be consistent with the general contract of Object.toString. + +- additional classes, methods, and fields + + JavaMail 1.5 adds classes to existing packages, methods to + existing classes, and fields to existing classes. All of + these changes have the potential to break source compatibility + for applications using the JavaMail API. + +- JDK 1.5 or newer required + + The JavaMail reference implementation now requires JDK 1.5 + or newer. It is expected that the large majority of users + are already using JDK 1.6 or newer. + +- protected fields in final classes in javax.mail.search made private + + Some of the final classes in the javax.mail.search package + contained protected fields. Since these classes were final + and couldn't be subclassed, the "protected" access qualifier + had no effect. These fields are now private. It's hard to + imagine how this change could impact any applications other + than perhaps those using reflection to access these classes. + +- MailHandler default attachment filters + + The default used for attachment filters has changed from allow + all log records (null) to instead use body filter assigned to + getFilter(). This is a safer choice as it maintains any + existing filter rules when attachments are added. + +- MailHandler default 'TO' address recipient + + If the 'TO' address key is not specified then the local address + is used. The previous behavior was to omit the 'TO' address + header. This can break configurations that are only sending to + a set of 'CC' or 'BCC' addresses. To revert this behavior + simply specify a 'TO' address key with an empty address value. + +- MailHandler intern of error manager, filters, and formatters. + + When MailHandler is created, the error manager, filters, and + formatters are checked for equality. When equal objects are + found they are replaced with a single representation. This + allows objects of the same type to share state for improving + performance, adding functionality, etc. To revert to the + previous behavior the error manager, filters, and formatters + must retain or be wrapped with objects that retain the identity + equals and hash code to prevent interning. + + + +-- JavaMail 1.4.4 -- + +- authorization ID may be null + + The IMAP and SMTP providers support a + "mail.<protocol>.sasl.authorizationid" property that allows you + to specify an authorization ID separately from the authentication + ID that's specified as the user name in properties or in the connect + method. The PLAIN authentication method, and some SASL authentication + methods support use of the separate authorization ID. In previous + releases, if the authorization ID was not specified, it defaulted + to the authentication ID (user name). This can cause problems if + the server doesn't allow an authorization ID even though the SASL + method allows specifying one. In this release, if no authorization + ID is specified, null is passed to the SASL method. If this causes + problems for a SASL method implementation or a server, the + "mail.<protocol>.sasl.authorizationid" property should be set to + the user name used for authentication. + + + +-- JavaMail 1.4.3 -- + +- SMTPTransport.isConnected behavior changed + + The SMTPTransport.isConnected method uses the SMTP NOOP command + to determine if the server is still alive. Because many older + servers were broken in various ways, any response (other than + the 421 "connection timed out" response) was considered a + successful response and the server was considered to be still + alive. Unfortunately, Microsoft Exchange has a bug that causes + it to return a response code of 451 when it times out a connection + instead of the expected 421 response code. SMTPTransport.isConnected + now considers only a 250 response code to indicate success, per + the SMTP spec. The old behavior can be restored by setting the + new mail.smtp.noop.strict property to false. + + + +-- JavaMail 1.4.2 -- + +- mail.smtp.quitwait default changed + + In previous releases, JavaMail would drop the SMTP connection + to the server immediately after sending the QUIT command. + This violates the SMTP spec. The property "mail.stmp.quitwait" + controls this behavior. In this release the default behavior + (if the property isn't specified) has changed so that JavaMail + will wait for the response from the server before dropping the + connection. In some cases, with some servers, this additional + wait time may be noticeable. + + +- MessagingException.getMessage output changed + + The MessagingException class, which is the base class for all + JavaMail exceptions, has been retrofitted to support the + exception chaining feature added to the java.lang.Throwable + class in J2SE 1.4. The visible impact of this change is that + the String returned by the getMessage method will only return + the immediate message for the top level exception, instead of + including messages for all nested exceptions. + + +- connection timeouts no longer use a thread + + To support connection timeouts in older versions of the JDK, + it was necessary for JavaMail to create a thread to make the + connection, so that it could interrupt and abandon that + thread if the connection timeout expired. J2SE 1.4 added + the ability to specify the connection timeout directly, so + JavaMail no longer uses an additional thread for this purpose. + + +- ByteArrayDataSource now part of javax.mail.util + + The ByteArrayDataSource class, which was previously included + in source form in the demo directory, is now a standard part + of the JavaMail API in the new javax.mail.util package. + Applications that are modified to make use of classes in the + new package, and that also included a copy of the demo version + of ByteArrayDataSource, should be careful to avoid potential + name conflicts between these two classes. + + +- mail.SSLSocketFactory.class property no longer supported + + The JavaMail implementation previously used this undocumented + property to locate the SSLSocketFactory from which it would + create SSLSockets when making an SSL or TLS connection. This + property is no longer used. The standard + javax.net.ssl.SSLSocketFactory is used instead, which supports + a standard way of overriding the choice of default SSLSocketFactory. + See the SSLSocketFactory javadocs for details. Most applications + should never need to override the default SSLSocketFactory. + + +- Quota class moved from com.sun.mail.imap to javax.mail + + The new Quota APIs in JavaMail are taken directly from the old + IMAP-specific classes in the com.sun.mail.imap package. If you've + been using these classes, you'll need to update your application + to use the new classes in the javax.mail package. + + +- getProtocol method removed from com.sun.mail.imap.IMAPFolder + + The getProtocol method returns an instance of IMAPProtocol. + This was originally intended to allow applications to + experiment with extending the IMAP protocol support to use IMAP + commands not directly implemented by the IMAP protocol + provider. Unfortunately, to safely use the IMAPProtocol + object, you need to obey the locking requirements of the + IMAPFolder object, and there's no way to do that from outside + the IMAPFolder object. The doCommand method was added to + IMAPFolder to resolve this problem. Now, with the introduction + of IDLE support to the IMAP protocol provider, it's critical to + obey the locking requirements. To prevent mistakes, the old, + unsafe, getProtocol method has been removed. Applications + should use the doCommand method for simple IMAP extensions. + Use of more complex IMAP extensions may require modification + of the IMAP protocol provider.
diff --git a/doc/release/IssueMap.txt b/doc/release/IssueMap.txt new file mode 100644 index 0000000..960627d --- /dev/null +++ b/doc/release/IssueMap.txt
@@ -0,0 +1,261 @@ +# +# This file is the mapping from old java.net issue numbers to +# new github issue numbers. Commit messages from before the migration +# to github will reference these older issue numbers. +# +189 1 +202 2 +965 3 +1207 4 +1343 5 +1345 6 +2728 7 +2986 8 +3155 9 +3442 10 +3444 11 +3539 12 +3565 13 +3566 14 +3815 15 +3983 16 +4002 17 +4065 18 +4296 19 +4355 20 +4448 21 +4511 22 +4579 23 +4581 24 +4583 25 +4743 26 +4753 27 +4906 28 +5086 29 +5090 30 +5218 31 +5233 32 +5257 33 +5285 34 +5300 35 +5681 36 +5682 37 +5683 38 +5684 39 +5685 40 +5686 41 +5687 42 +5688 43 +5689 44 +5690 45 +5691 46 +5692 47 +5693 48 +5694 49 +5713 50 +5743 51 +5769 52 +5770 53 +5816 54 +5818 55 +5819 56 +5820 57 +5821 58 +5822 59 +5829 60 +5830 61 +5847 62 +5853 63 +5861 64 +5896 65 +5901 66 +5924 67 +5925 68 +5933 69 +5934 70 +5978 71 +5987 72 +5989 73 +6004 74 +6069 75 +6072 76 +6074 77 +6102 78 +6104 79 +6108 80 +6121 81 +6125 82 +6137 83 +6141 84 +6143 85 +6160 86 +6161 87 +6181 88 +6201 89 +6203 90 +6207 91 +6208 92 +6216 93 +6238 94 +6260 95 +6261 96 +6274 97 +6275 98 +6276 99 +6277 100 +6278 101 +6279 102 +6280 103 +6281 104 +6283 105 +6324 106 +6325 107 +6326 108 +6327 109 +6328 110 +6329 111 +6330 112 +6336 113 +6348 114 +6352 115 +6353 116 +6361 117 +6365 118 +6366 119 +6367 120 +6368 121 +6379 122 +6407 123 +6430 124 +6452 125 +6467 126 +6496 127 +6498 128 +6526 129 +6528 130 +6535 131 +6551 132 +6552 133 +6565 134 +6568 135 +6572 136 +6585 137 +6638 138 +6644 139 +6650 140 +6657 141 +6662 142 +6667 143 +6668 144 +6687 145 +6703 146 +6712 147 +6718 148 +6719 149 +6755 150 +6762 151 +6765 152 +6767 153 +6772 154 +6778 155 +6787 156 +6804 157 +6817 158 +6823 159 +6824 160 +6827 161 +6840 162 +6844 163 +6850 164 +6852 165 +6860 166 +6863 167 +6886 168 +6905 169 +6907 170 +6913 171 +6938 172 +6943 173 +6949 174 +6954 175 +6964 176 +6965 177 +6966 178 +6973 179 +6989 180 +6997 181 +7009 182 +7010 183 +7011 184 +7014 185 +7019 186 +7026 187 +7027 188 +7028 189 +7030 190 +7035 191 +7042 192 +7051 193 +7052 194 +7075 195 +7083 196 +7088 197 +7090 198 +7091 199 +7092 200 +7094 201 +7097 202 +7104 203 +7118 204 +7140 205 +7150 206 +7151 207 +7156 208 +7182 209 +7238 210 +7332 211 +7355 212 +7356 213 +7371 214 +7378 215 +7471 216 +7472 217 +7483 218 +7506 219 +7512 220 +7513 221 +7529 222 +7536 223 +8387 224 +8389 225 +8398 226 +8399 227 +8403 228 +8404 229 +8405 230 +8408 231 +8415 232 +8420 233 +8422 234 +8428 235 +8435 236 +8486 237 +8487 238 +8492 239 +8540 240 +8550 241 +8558 242 +8562 243 +8568 244 +8583 245 +8621 246 +8748 247 +8810 248 +8822 249 +8833 250 +8846 251 +9169 252 +9418 253 +10909 254 +11057 255 +19563 256
diff --git a/doc/release/JavaWebServer.html b/doc/release/JavaWebServer.html new file mode 100644 index 0000000..9c34bb0 --- /dev/null +++ b/doc/release/JavaWebServer.html
@@ -0,0 +1,58 @@ +<!doctype html public "-//w3c//dtd html 4.0 transitional//en"> +<html> +<head> +<!-- + + Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + + <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1"> + <meta name="Author" content="JavaSoftware"> + <meta name="GENERATOR" content="Mozilla/4.61 [en] (WinNT; U) [Netscape]"> + <title>JavaWebServer</title> +</head> +<body> +If you don't already have Java Web Server installed and running, +you'll need to download it +<br>from the <a href="http://www.sun.com/software/jwebserver/index.html">Java +Web Server</a> product page and install it. We tested the JavaMailServlet +with +<br>these configurations. +<p>(on Solaris) +<blockquote>JavaWebServer 2.0 (binary)</blockquote> +(on NT) +<blockquote>JavaWebServer 2.0 (binary)</blockquote> +To run the JavaMailServlet, you must add the JavaMail and JavaBeans Activation +Framework +<br>jar files to the CLASSPATH environment variable (this is documented +in the JavaMail README file +<br>and additional Windows NT information is provided <a href="classpath-NT.html">here</a>). +<p>Once this is done, restart the web server. +<br> +<p>Additional Notes +<blockquote>If you run JavaWebServer 2.0 with the Java Runtime Environment +(JRE) supplied with +<br>JavaWebServer, no additional security policy is necessary. The security +policy file +<br>(i.e. jserv.policy) supplied with the JRE contains the necessary permissions +for local servlets.</blockquote> + +<blockquote>If you run JavaWebServer 2.0 with another JRE (with an installed +JDK for example), you +<br>must add the necessay security policy for running under a SecurityManager.</blockquote> + +</body> +</html>
diff --git a/doc/release/NOTES.txt b/doc/release/NOTES.txt new file mode 100644 index 0000000..637a2dd --- /dev/null +++ b/doc/release/NOTES.txt
@@ -0,0 +1,206 @@ + NOTES + ===== + + JavaMail(TM) API ${mail.version} release + ------------------------------ + +Welcome to the ${mail.version} release of the JavaMail API implementation. + +Please refer to CHANGES.txt for a list of the changes since the +previous release. + +Please see the FAQ at https://eclipse-ee4j.github.io/javamail/FAQ + +Protocol Providers +------------------ + +The JavaMail API jar file "jakarta.mail.jar" includes the full JavaMail +API implementation and the Sun protocol providers - IMAP, SMTP, and +POP3. The simplest way to use the JavaMail API is to just use the +jakarta.mail.jar file and ignore the other jar files in this package. + +In some cases it may be desirable to minimize the size of the JavaMail +API code used by an application (e.g., in a microservice). +In this case you might want to include the "mailapi.jar" file, which +includes *no* protocol providers, along with just the jar file for the +protocol provider you need. For example, a microservice that only needs +to send mail could use the "mailapi.jar" file and the "smtp.jar" file. + +An important note when using the separate protocol provider jar files: + +- You can't mix and match the Sun protocol providers between different + releases of the JavaMail API. The Sun protocol providers depend on + implementation-specific utility APIs within the mailapi.jar file. + (Third party protocol providers that don't depend on these APIs + should work fine.) + + +NOTE: The Sun protocol provider documentation is included in the javadocs + for the JavaMail API. This documentation describes how to + use features of the Sun protocol providers to directly access + some features of the SMTP, IMAP, and POP3 protocols that are + not otherwise supported by the standard JavaMail API. + + +Gmail IMAP Provider +------------------- + +This release includes an EXPERIMENTAL Gmail IMAP provider. +Normal use of Gmail is handled by the standard "imap" protocol +provider, but the new "gimap" protocol provider supports additional +Gmail-specific non-standard features. See the javadocs for the +com.sun.mail.gimap package for details. Note that the gimap.jar file +needs to be added to your CLASSPATH to use this new provider. + + +SASL Support +------------ + +On systems that support the Java SASL API (javax.security.sasl, JSR-28), +such as J2SE 5.0 and later, the IMAP provider can use the SASL API to +find an appropriate authentication mechanism. The SASL API also allows +you to plug in support for custom authentication mechanisms. See The +Java SASL API Programming and Deployment Guide at +https://docs.oracle.com/javase/8/docs/technotes/guides/security/sasl/sasl-refguide.html +for details on developing custom SASL mechanisms. See the javadocs for +the com.sun.mail.imap package for the properties required to enable and +configure SASL support. + + +DSN Support +----------- + +This release of JavaMail includes EXPERIMENTAL support for creating +and parsing Delivery Status Notifications, as defined by RFC 3462 +and RFC 3464. To make use of this support you need to include dsn.jar +in your CLASSPATH along with jakarta.mail.jar. See the javadocs for the +com.sun.mail.dsn package for more details. + +The DSN package also provides support for creating and parsing Message +Disposition Notifications, as defined by RFC 3798. + +The APIs unique to this package should be considered EXPERIMENTAL. +They may be changed in the future in ways that are incompatible with +applications using the current APIs. + + +NTLM Support +------------ + +This release of JavaMail includes EXPERIMENTAL support for the +Microsoft NTLM authentication mechanism used by Exchange. See the +file NTLMNOTES.txt for details. + + +OSGi Support +------------ + +The JavaMail jar files are now OSGi bundles. Please let us know +of any problems using JavaMail with OSGi. + + +How to submit bug reports +------------------------- + +If you've found a bug, or if you just need help figuring out how to use +the JavaMail API, please try to include the following information in +your message to us: + + - a program or code snippet that shows the problem + - the platform you are using + - the mail server (vendor name, version number) you are using + - your environment variable settings + - a stack trace, if appropriate + - a protocol trace, after turning on session debugging, if appropriate + +Most of the problems reported to us fail to include enough of the above +information to allow us to diagnose your problem. It will save you and +us time if you include this information in your first message to us. + +By far the most common problems we see are: + +Your problem: Something doesn't work right when talking to my mail server. +Our response: Turn on session debugging and send us the protocol trace. + See the demo program documentation for how to turn on + session debugging for the demo programs. In your own + program, call "session.setDebug(true);". + +Your problem: javax.mail or javax.activation classes not found when compiling. +Our response: You didn't set CLASSPATH correctly to find jakarta.mail.jar and + jakarta.activation.jar. See README.txt. + +Your problem: NoSuchProviderException - No such provider for rfc822. +Our response: You unjar'ed jakarta.mail.jar. Don't. + +Your problem: How do I create a message with an attachment? +Our response: Create a message with a MimeMultipart content. See the + sendfile.html and msgmultisendsample.java demo programs. + +Please check the FAQ at https://eclipse-ee4j.github.io/javamail/FAQ +before submitting bug reports. + +Send your bug reports to: + + javamail_ww@oracle.com + + + + +Servers tested with: +-------------------- + + The IMAP implementation works with IMAP4 and IMAP4rev1 servers. + The current release has been tested with: + Oracle Beehive + Oracle Communications Messaging Server 8.0 + UW IMAP4 server version 2003.339 + Cyrus IMAP4 server version 1.6.19 + Microsoft Exchange 2010 + Microsoft Exchange 2013 + Microsoft Exchange 2016 + + Previous releases have been tested with: + Sun Java System Messaging Server version 5.2 + Sun Java System Messaging Server version 6.3 + Sun Java System Messaging Server version 7.0 + Sun Internet Mail Server version 2.0, 3.2, and 4.0 + Netscape Messaging Server version 3.01 and 4.1 + Microsoft MCIS Mail Server + Lotus Notes + Software.com IMAP server + Qualcomm Worldmail + + The current release of the SMTP implementation has been tested with: + Sendmail version 8.13.8 + Oracle Beehive + Oracle Communications Messaging Server 8.0 + Microsoft Exchange 2010 + Microsoft Exchange 2013 + Microsoft Exchange 2016 + + Previous releases have been tested with: + Sendmail version 8.6 and 8.9.1 + Sun Java System Messaging Server version 5.2 + Sun Java System Messaging Server version 6.3 + Sun Java System Messaging Server version 7.0 + Sun Internet Mail Server version 3.2 and 4.0 + Netscape Messaging Server version 3.01 and 4.1 + Microsoft Exchange + Microsoft MCIS Mail Server + Qualcomm Worldmail + +How to give feedback +-------------------- + +Please send your feedback to this email-address: + + javamail_ww@oracle.com + +Check out our website at https://eclipse-ee4j.github.io/javamail/. + +You can also find help on StackOverflow: + + https://stackoverflow.com/questions/tagged/javamail + + +------------------------------------------------------------------
diff --git a/doc/release/NTLMNOTES.txt b/doc/release/NTLMNOTES.txt new file mode 100644 index 0000000..87fce72 --- /dev/null +++ b/doc/release/NTLMNOTES.txt
@@ -0,0 +1,28 @@ + Notes for use of NTLM authentication support with JavaMail + ---------------------------------------------------------- + +Thanks to the efforts of Luis Serralheiro, JavaMail now suports the use +of Microsoft's proprietary NTLM authentication mechanism. This support +within JavaMail is now derived from the NTLM support in the JDK and +included directly in JavaMail, with no external dependencies. + +This release of JavaMail was tested with Microsoft Exchange 5.5 and 2007. + +The SMTP and IMAP providers support the use of NTLM authentication. +The following properties can be used to configure the NTLM support: + +mail.<protocol>.auth.ntlm.domain + The NTLM authentication domain. + +mail.<protocol>.auth.ntlm.flags + NTLM protocol-specific flags. (not currently used) + See http://curl.haxx.se/rfc/ntlm.html#theNtlmFlags for details. + + +NOTE: This capability is very new and has NOT been thoroughly tested. + Please send any feedback or bug reports to us at javamail_ww@oracle.com. + +WARNING: This support, and the APIs and properties used to control it, +should be considered EXPERIMENTAL. They may be changed in the future +in ways that are incompatible with applications using the current APIs +and properties.
diff --git a/doc/release/README.txt b/doc/release/README.txt new file mode 100644 index 0000000..7f20697 --- /dev/null +++ b/doc/release/README.txt
@@ -0,0 +1,175 @@ + README + ====== + + JavaMail(TM) API ${mail.version} release + ------------------------------ + +Welcome to the JavaMail API ${mail.version} release! This release includes +versions of the JavaMail API implementation, IMAP, SMTP, and POP3 +service providers, some examples, and documentation for the JavaMail +API. + +Please see the FAQ at https://eclipse-ee4j.github.io/javamail/FAQ + +JDK Version notes +----------------- + +The JavaMail API supports JDK 1.7 or higher. Note that we have +currently tested this implementation with JDK 1.7 and 1.8. + +While JavaMail will work with JAF 1.0.2, we recommend the use of JAF 1.1 +or newer. JAF 1.2.1 is currently the newest version. Note that JAF 1.1 +is included in JDK 1.6 and JAF 1.1.1 is included in JDK 1.6.0_10 and +later. JAF 1.2.0 is included but hidden in JDK 9 and has been removed +from JDK 11. JAF 1.2.1 is available separately (see below). + + +Protocols supported +------------------- + +This release supports the following Internet standard mail protocols: + + IMAP - a message Store protocol, for reading messages from a server + POP3 - a message Store protocol, for reading messages from a server + SMTP - a message Transport protocol, for sending messages to a server + +The following table lists the names of the supported protocols (as used +in the JavaMail API) and their capabilities: + + Protocol Store or Uses Supports + Name Transport? SSL? STARTTLS? + ------------------------------------------------- + imap Store No Yes + imaps Store Yes N/A + gimap Store Yes N/A + pop3 Store No Yes + pop3s Store Yes N/A + smtp Transport No Yes + smtps Transport Yes N/A + +See our web page at https://eclipse-ee4j.github.io/javamail/ +for the latest information on third party protocol providers. + + +Download +-------- + +See the JavaMail project page to download this release. + + https://eclipse-ee4j.github.io/javamail/ + + +Requirements +------------ + +Note that the JavaMail API requires the JavaBeans(TM) Activation +Framework package to be installed as well if you're using or JDK 11 or later. + +Download the latest version of the JavaBeans Activation Framework from + + https://github.com/eclipse-ee4j/jaf/releases + +and install it in a suitable location. + + +Installation +------------ + + UNIX/Linux + ---------- + + 1. Download the jakarta.mail.jar file from the JavaMail project website. + https://github.com/eclipse-ee4j/javamail/releases + + 2. Set your CLASSPATH to include the "jakarta.mail.jar" file obtained from + the download, as well as the current directory. + + Assuming you have downloaded jakarta.mail.jar to the /u/me/download/ + directory, the following would work: + + export CLASSPATH=$CLASSPATH:/u/me/download/jakarta.mail.jar:. + + (Don't forget the trailing "." for the current directory.) + Also, if you're using JDK 1.5, include the "activation.jar" file that you + obtained from downloading the JavaBeans Activation Framework. For example: + + export CLASSPATH=$CLASSPATH:/u/me/download/activation/activation.jar + + 3. Download the javamail-samples.zip file from the project website. + https://github.com/eclipse-ee4j/javamail/releases + + 4. Compile any sample program using your Java compiler. For example: + + javac msgshow.java + + 5. Run the sample program. The '-' option lists the required and optional + command-line options to successfully run any sample. For example: + + java msgshow - + + lists the available options. And + + java msgshow -T imap -H <mailserver> -U <username> -P <passwd> -f INBOX 5 + + uses the IMAP protocol to display message number 5 from your INBOX. + + (Additional instructions on how to run the simple mail reader sample + and servlet sample are provided in client/README.txt and servlet/README.txt, + respectively.) + + + Windows + ------- + + 1. Download the jakarta.mail.jar file from the JavaMail project website. + https://github.com/eclipse-ee4j/javamail/releases + + 2. Set your CLASSPATH to include the "jakarta.mail.jar" file obtained from + the download, as well as the current directory. + + Assuming you have downloaded jakarta.mail.jar to the /u/me/download/ + directory, the following would work: + + set CLASSPATH=%CLASSPATH%;c:\download\jakarta.mail.jar;. + + (Don't forget the trailing "." for the current directory.) + Also, if you're using JDK 1.5, include the "activation.jar" file that you + obtained from downloading the JavaBeans Activation Framework. For example: + + set CLASSPATH=%CLASSPATH%;c:\download\activation\activation.jar + + 3. Download the javamail-samples.zip file from the project website. + https://github.com/eclipse-ee4j/javamail/releases + + 4. Compile any sample program using your Java compiler. For example: + + javac msgshow.java + + 5. Run the sample program. The '-' option lists the required and optional + command-line options to successfully run any sample. For example: + + java msgshow - + + lists the available options. And + + java msgshow -T imap -H <mailserver> -U <username> -P <passwd> -f INBOX 5 + + uses the IMAP protocol to display message number 5 from your INBOX. + + (Additional instructions on how to run the simple mail reader sample + and servlet sample are provided in client/README.txt and servlet/README.txt, + respectively.) + + +Problems? +--------- + +The JavaMail FAQ at https://eclipse-ee4j.github.io/javamail/FAQ +includes information on protocols supported, installation problems, +debugging tips, etc. + +See the NOTES.txt file for information on how to report bugs. + +Enjoy! + +The JavaMail API Team
diff --git a/doc/release/SSLNOTES.txt b/doc/release/SSLNOTES.txt new file mode 100644 index 0000000..93e2eac --- /dev/null +++ b/doc/release/SSLNOTES.txt
@@ -0,0 +1,311 @@ + Notes for use of SSL with JavaMail + ---------------------------------- + +JavaMail now supports accessing mail servers over connections secured +using SSL or TLS. To simplify such access, there are two alternative +approaches to enable use of SSL. + +First, and perhaps the simplest, is to set a property to enable use +of SSL. For example, to enable use of SSL for SMTP connections, set +the property "mail.smtp.ssl.enable" to "true". + +Alternatively, you can configure JavaMail to use one of the SSL-enabled +protocol names. In addition to the non-SSL JavaMail protocols "imap", +"pop3", and "smtp", the protocols "imaps", "pop3s", and "smtps" can +be used to connect to the corresponding services using an SSL +connection. + +In addition, the "imap" and "smtp" protocols support use of the +STARTTLS command (see RFC 2487 and RFC 3501) to switch the connection +to be secured by TLS. + +Use of the STARTTLS command is preferred in cases where the server +supports both SSL and non-SSL connections. + +This SSL/TLS support in JavaMail works only when JavaMail is used on +a version of J2SE that includes SSL support. We have tested this +support on J2SE 1.4 and newer, which include SSL support. The +SSL support is provided by the JSSE package, which is also available +for earlier versions of J2SE. We have not tested such configurations. + +-- STARTTLS support + +The STARTTLS support is available in the standard "imap" and "smtp" +protocols, but must be enabled by setting the appropriate property, +mail.imap.starttls.enable or mail.smtp.starttls.enable, to "true". +When set, if the server supports the STARTTLS command, it will be +used after making the connection and before sending any login +information. + + +-- Secure protocols + +When using the new protocol names, configuration properties must also use +these protocol names. For instance, set the property "mail.smtps.host" +to specify the host name of the machine to connect to when using the +"smtps" protocol for SMTP over SSL. Similarly, to set the IMAP protocol +timeout when using the "imaps" protocol for IMAP over SSL, set the property +"mail.imaps.timeout". See the package documentation for the different +protocol packages for the list of available properties, which are +always set using property names of the form mail.<protocol>.<property>. + +The Transport.send method will use the default transport protocol, +which remains "smtp". To enable SMTP connections over SSL, set the +"mail.smtp.ssl.enable" property to "true". This is usually the easiest +approach. + +Alternatively, to change the default transport protocol +returned by the Session.getTransport() method to SMTP over SSL, set +the property "mail.transport.protocol" to "smtps". To change the +transport used for internet addresses (as returned by the +Session.getTransport(Address) method, and used by the Transport.send +method), use + + session.setProtocolForAddress("rfc822", "smtps"); + + +-- Trusted Certificates + +To establish an SSL/TLS connection, the JavaMail client must be able +to verify that the security certificate presented by the server +it is connecting to is "trusted" by the client. Trusted certificates +are maintained in a Java keystore file on the client. The J2SE +SDK "keytool" command is used to maintain the keystore file. + +There are two common approaches for verifying server certificates. +The first approach is probably most common for servers accessible to +partners outside a company. The second approach is probably most +common for servers used within a company. + +1. Server certificates may be signed be a well known public + Certificate Authority. The default Java keystore file contains + the public keys of well known Certificate Authorities and can + verify the server's certificate by following the chain of + certificates signing the server's certificate back to one of + these well known CA certificates. + + In this case the client doesn't need to manage certificates + explicitly but can just use the default keystore file. + +2. Server certificates may be "self-signed". In this case there is + no chain of signatures to use in verifying the server's certificate. + Instead, the client will need the server's certificate in the + client's keystore file. The server's certificate is imported into + the keystore file once, using the keytool command, and after that + is used to verify connections to the server. A single keystore file + may contain certificates of many servers. + + In this case the client will need to set the appropriate System + properties to point to the client's keystore file containing the + trusted certificate. These properties can be set when invoking + the "java" command, or can be set programmatically. For example, + + java -Djavax.net.ssl.trustStore=$HOME/.keystore ... + + See the JSSE Reference Guide for details: + http://download.oracle.com/javase/6/docs/technotes/guides/security/jsse/JSSERefGuide.html#CustomizingStores + + +-- Server Identity Check + +RFC 2595 specifies addition checks that must be performed on the +server's certificate to ensure that the server you connected to is +the server you intended to connect to. This reduces the risk of +"man in the middle" attacks. For compatibility with earlier releases +of JavaMail, these additional checks are disabled by default. We +strongly recommend that you enable these checks when using SSL. To +enable these checks, set the "mail.<protocol>.ssl.checkserveridentity" +property to "true". + + +-- Socket Factories + +In earlier releases it was necessary to explicitly set a socket +factory property to enable use of SSL. In almost all cases, this +is no longer necessary. SSL support is built in. However, there +is one case where a special socket factory may be needed. + +JavaMail now includes a special SSL socket factory that can simplify +dealing with servers with self-signed certificates. While the +recommended approach is to include the certificate in your keystore +as described above, the following approach may be simpler in some cases. + +The class com.sun.mail.util.MailSSLSocketFactory can be used as a +simple socket factory that allows trusting all hosts or a specific set +of hosts. For example: + + MailSSLSocketFactory sf = new MailSSLSocketFactory(); + sf.setTrustAllHosts(true); + // or + // sf.setTrustedHosts(new String[] { "my-server" }); + props.put("mail.smtp.ssl.enable", "true"); + // also use following for additional safety + //props.put("mail.smtp.ssl.checkserveridentity", "true"); + props.put("mail.smtp.ssl.socketFactory", sf); + +Use of MailSSLSocketFactory avoids the need to add the certificate to +your keystore as described above, or configure your own TrustManager +as described below. + + +-- Debugging + +Debugging problems with certificates and keystores can be difficult. +The JSSE Reference Guide contains information on debugging utilities +that can help. See: +http://download.oracle.com/javase/6/docs/technotes/guides/security/jsse/JSSERefGuide.html#Debug + +There are some debugging options in the JDK that can help, depending +on the sorts of problems you're having. Setting the following system +properties will produce additional debugging output: + + java.security.debug=certpath + javax.net.debug=trustmanager + +Set these on the command line when you run your program using, for example: + + java -Djava.security.debug=certpath -Djavax.net.debug=trustmanager ... + + +-- keytool Usage + +Given a certificate for the server as used in case #2 above, you can +import this certificate into your Java keystore file using a command +such as: + + keytool -import -alias imap-server -file imap.cer + +The keytool command can also be used to generate a self-signed certificate +that can be used by your mail server, if you're setting up your own server. +Other utilities, such as those included with the OpenSSL package, can also +be used to generate such certificates, and they can be imported into the +Java keystore using keytool. + +For more information on using the keytool command, see the keytool +reference pages at: +http://download.oracle.com/javase/6/docs/technotes/guides/security/index.html + + +-- Configuring Your Own Trust Manager + +When using SSL/TLS, it's important to ensure that the server you connect +to is actually the server you expected to connect to, to prevent "man in +the middle" attacks on your communication. The recommended technique is +to configure the Java keystore using one of the methods described above. +If, for some reason, that approach is not workable, it's also possible +to configure the SSL/TLS implementation to use your own TrustManager +class to evaluate whether to trust the server you've connected to. + +The following "dummy" classes illustrate the framework necessary to create +your own TrustManager implementation. + +First, a replacement for the standard SSLSocketFactory is needed, to allow +you to specify which TrustManager to use: + +==> DummySSLSocketFactory.java <== + +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; + +import javax.net.SocketFactory; +import javax.net.ssl.*; + + +/** + * DummySSLSocketFactory + */ +public class DummySSLSocketFactory extends SSLSocketFactory { + private SSLSocketFactory factory; + + public DummySSLSocketFactory() { + try { + SSLContext sslcontext = SSLContext.getInstance("TLS"); + sslcontext.init(null, + new TrustManager[] { new DummyTrustManager()}, + null); + factory = (SSLSocketFactory)sslcontext.getSocketFactory(); + } catch(Exception ex) { + // ignore + } + } + + public static SocketFactory getDefault() { + return new DummySSLSocketFactory(); + } + + public Socket createSocket() throws IOException { + return factory.createSocket(); + } + + public Socket createSocket(Socket socket, String s, int i, boolean flag) + throws IOException { + return factory.createSocket(socket, s, i, flag); + } + + public Socket createSocket(InetAddress inaddr, int i, + InetAddress inaddr1, int j) throws IOException { + return factory.createSocket(inaddr, i, inaddr1, j); + } + + public Socket createSocket(InetAddress inaddr, int i) + throws IOException { + return factory.createSocket(inaddr, i); + } + + public Socket createSocket(String s, int i, InetAddress inaddr, int j) + throws IOException { + return factory.createSocket(s, i, inaddr, j); + } + + public Socket createSocket(String s, int i) throws IOException { + return factory.createSocket(s, i); + } + + public String[] getDefaultCipherSuites() { + return factory.getDefaultCipherSuites(); + } + + public String[] getSupportedCipherSuites() { + return factory.getSupportedCipherSuites(); + } +} + + +Next you need the actual implementation of the TrustManager. This dummy +trust manager trusts anything. THIS IS NOT SECURE!!! + +==> DummyTrustManager.java <== + +import javax.net.ssl.X509TrustManager; +import java.security.cert.X509Certificate; + + +/** + * DummyTrustManager - NOT SECURE + */ +public class DummyTrustManager implements X509TrustManager { + + public void checkClientTrusted(X509Certificate[] cert, String authType) { + // everything is trusted + } + + public void checkServerTrusted(X509Certificate[] cert, String authType) { + // everything is trusted + } + + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } +} + +Finally, you need to configure JavaMail to use your SSLSocketFactory. +Set the appropriate protocol-specific property, e.g., + + props.setProperty("mail.imap.ssl.enable", "true"); + props.setProperty("mail.imap.ssl.socketFactory.class", + "DummySSLSocketFactory"); + props.setProperty("mail.imap.ssl.socketFactory.fallback", "false"); + Session session = Session.getInstance(props, null); + +Similar properties would need to be set to use other protocols.
diff --git a/doc/release/Tomcat.html b/doc/release/Tomcat.html new file mode 100644 index 0000000..8077875 --- /dev/null +++ b/doc/release/Tomcat.html
@@ -0,0 +1,43 @@ +<!doctype html public "-//w3c//dtd html 4.0 transitional//en"> +<html> +<head> +<!-- + + Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + + <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1"> + <meta name="Author" content="JavaSoftware"> + <title>Tomcat</title> +</head> +<body> +Tomcat is a complete web server written in Java that provides +support for the Servlet and JSP specifications. +Tomcat is developed as a part of the Apache +<a href="http://jakarta.apache.org">Jakarta Project</a>. +Both binaries and source are available from the Jakarta web site. +<p> +To run the JavaMailServlet, you must add the JavaMail and JavaBeans +Activation Framework jar files to the <code>lib</code> directory +under the directory in which you installed Tomcat. This will +cause Tomcat to include these jar files in its classpath automatically. +(Some packages of Tomcat version 4 include JavaMail support.) +Depending on the version of Tomcat used, the <code>mail.jar</code> +and <code>activation.jar</code> files should be copied to the +<code>lib</code> directory or to an appropriate subdirectory of the +<code>lib</code> directory. +</body> +</html>
diff --git a/doc/release/classpath-NT.html b/doc/release/classpath-NT.html new file mode 100644 index 0000000..c0057ae --- /dev/null +++ b/doc/release/classpath-NT.html
@@ -0,0 +1,64 @@ +<!doctype html public "-//w3c//dtd html 4.0 transitional//en"> +<html> +<head> +<!-- + + Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + + <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1"> + <meta name="Author" content="JavaSoftware"> + <meta name="GENERATOR" content="Mozilla/4.61 [en] (WinNT; U) [Netscape]"> + <title>NT Classpath</title> +</head> +<body> +Environment variables are administered on Windows NT from the +<b>System +Properties</b> Control Panel settings (Start->Settings->Control Panel; +it's the <b>System</b> icon in the Control Panel). The <b>Environment</b> +tab contains all of the System and User Variables. +<p>System Variables exist for all users while User Variables only exist +for the current user. Only users with Administrator privileges are able +to define System Variables. For this example, environment variables will +be defined as User Variables. +<p>If this is the first time the CLASSPATH environment variable is being +defined, select the first User Variables entry (its name/value pair will +show up below the User Variables list). Enter 'CLASSPATH' in the 'Variable:' +text edit field. +<p>If the CLASSPATH environment variable has already been previously defined, +simply select it from the User Variables list. +<p>There are a couple of ways of adding something to an environment variable. +It can be added directly, or through the use of a separately defined environment +variable. +<p>When the JavaMail and Java Activation Framework (JAF) jar files are +added directly to the CLASSPATH, the Value field should contain (for example) +".;D:\Java\javamail-1.2\mail.jar;D:\Java\jaf-1.0.1\activation.jar". +<p><img SRC="images/direct-classpath.jpg" height=466 width=410> +<br> +<p>When the JavaMail and Java Activation Framework (JAF) jar files are +added to the CLASSPATH using separate environment variables, the Value +field should contain (for example) ".;%JAVAMAIL_JAR%;%JAF_JAR%" +<br> +<p><img SRC="images/indirect-classpath.jpg" height=466 width=410> +<br> +<p>Once you have finished updating the Value field, you must press the +Set button. Then press OK. It is not necessary to reboot or log out, however +these new settings will only be present in new Command Prompt windows. +Any existing windows must be closed and reopened for the new settings to +take effect. Any running Java applications which inherit the system CLASSPATH +also need to be restarted. +</body> +</html>
diff --git a/doc/release/iPlanet.html b/doc/release/iPlanet.html new file mode 100644 index 0000000..c420863 --- /dev/null +++ b/doc/release/iPlanet.html
@@ -0,0 +1,50 @@ +<!doctype html public "-//w3c//dtd html 4.0 transitional//en"> +<html> +<head> +<!-- + + Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + + <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1"> + <meta name="Author" content="JavaSoftware"> + <meta name="GENERATOR" content="Mozilla/4.61 [en] (WinNT; U) [Netscape]"> + <title>iPlantet</title> +</head> +<body> +If you don't already have iPlanet Web Server installed and running, +you'll need to download it +<br>from the <a href="http://www.iplanet.com/products/infrastructure/web_servers/iws/index.html">iPlanet +Web Server</a> product page and install it. We tested the JavaMailServlet +with +<br>these configurations. +<p>(on Solaris) +<blockquote> iPlanet Web Server 4.1 (binary)</blockquote> +(on NT) +<blockquote> iPlanet Web Server 4.1 (binary)</blockquote> +To run the JavaMailServlet, you must add the JavaMail and JavaBeans Activation +Framework +<br>jar files to the CLASSPATH environment variable (this is documented +in the JavaMail README file +<br>and additional Windows NT information is provided <a href="classpath-NT.html">here</a>). +<p>Once this is done, restart the web server. +<br> +<p>Additional Notes +<blockquote>iPlanet Web Server 4.1 does not use a SecurityManager or enforce +any Java security policy.</blockquote> + +</body> +</html>
diff --git a/doc/release/images/direct-classpath.jpg b/doc/release/images/direct-classpath.jpg new file mode 100644 index 0000000..f78df04 --- /dev/null +++ b/doc/release/images/direct-classpath.jpg Binary files differ
diff --git a/doc/release/images/indirect-classpath.jpg b/doc/release/images/indirect-classpath.jpg new file mode 100644 index 0000000..88813ac --- /dev/null +++ b/doc/release/images/indirect-classpath.jpg Binary files differ
diff --git a/doc/spec/JavaMail-1.1-changes.txt b/doc/spec/JavaMail-1.1-changes.txt new file mode 100644 index 0000000..028dbec --- /dev/null +++ b/doc/spec/JavaMail-1.1-changes.txt
@@ -0,0 +1,656 @@ + + JavaMail 1.1 + ============ + +Following is a description of the new APIs that are being +introduced in JavaMail 1.1 + +Please send comments and feedback to javamail@sun.com + +(1) MessageContext +================== + +In some cases it is desirable for the object representing the content +of a BodyPart to know something about the "context" in which it is +operating, e.g., what other data is contained in the same Multipart +object, who sent the message containing the data, etc. This allows +for more interesting content types that know more about the message +containing them and the mail system in general. + +Some uses of multipart/related might require these capabilities. +For instance, the handler for a text/html body part contained in a +multipart/related object might need to know about the containing +object in order to find the related image data needed to display the +HTML document. + +There is one particular case in the current implementation where +this need arises. (This is a bug in the current release.) When +constructing a MimeMessage object for a nested message contained +in another message, the DataContentHandler needs the Session +object in order to construct the new MimeMessage object. + +To solve these problems we introduce a new class, a new interface, +and a number of new methods. + +The MessageContext class provides the basic information about the +"context" in which a content object is operating: + +/** + * The context in which a piece of Message content is contained. A + * <code>MessageContext</code> object is returned by the + * <code>getMessageContext</code> method of the + * <code>MessageAware</code> interface. <code>MessageAware</code> is + * typically implemented by <code>DataSources</code> to allow a + * <code>DataContentHandler</code> to pass on information about the + * context in which a data content object is operating. + * + * @see javax.mail.MessageAware + * @see javax.activation.DataSource + * @see javax.activation.DataContentHandler + */ +public class MessageContext { + + /** + * Create a MessageContext object describing the context of the + * given Part. + */ + public MessageContext(Part part) + + /** + * Return the Part that contains the content. + * + * @return the containing Part, or null if not known + */ + public Part getPart() + + /** + * Return the Message that contains the content. + * Follows the parent chain up through containing Multipart + * objects until it comes to a Message object, or null. + * + * @return the containing Message, or null if not known + */ + public Message getMessage() + + /** + * Return the Session we're operating in. + * + * @return the Session, or null if not known + */ + public Session getSession() +} + + +A MessageContext object can be obtained by a DataContentHandler from a +DataSource that also implements the MessageAware interface: + +/** + * An interface optionally implemented by <code>DataSources</code> to + * supply information to a <code>DataContentHandler</code> about the + * message context in which the data content object is operating. + * + * @see javax.mail.MessageContext + * @see javax.activation.DataSource + * @see javax.activation.DataContentHandler + */ +public interface MessageAware { + /** + * Return the message context. + */ + public MessageContext getMessageContext(); +} + + +This new interface and class provides the basic information needed by +the DataContentHandler to use in constructing more "interesting" objects +representing a particular type of data. + +To allow navigation up the chain of components contained in a Message, +we add the following methods. + +First, on BodyPart we add: + + /** + * The <code>Multipart</code> object containing this + * <code>BodyPart</code>, if known. + */ + protected Multipart parent; + + /** + * Return the containing <code>Multipart</code> object, + * or <code>null</code> if not known. + */ + public Multipart getParent() + + +On Multipart we add: + + /** + * The <code>Part</code> containing this <code>Multipart</code>, + * if known. + */ + protected Part parent; + + /** + * Return the <code>Part</code> that contains this + * (cod<code>Multipart</c object, or <code>null</code> if not known. + */ + public Part getParent() + + /** + * Set the parent of this <code>Multipart</code> to be the specified + * <code>Part</code>. Normally called by the <code>Message</code> + * or <code>BodyPart</code> <code>setContent(Multipart)</code> + * method. <p> + * + * <code>parent</code> may be <code>null</code> if the + * <code>Multipart</code> is being removed from its containing + * <code>Part</code>. + */ + public void setParent(Part parent) + + +To enable this new functionality we change the implementations of +MimeBodyPart, MimeMessage, and MimeMultipart to maintain the "parent" +links where possible. We also change MimePartDataSource to implement +the MessageAware interface. + +=================================================================== + +(2) MessageID +============= +The MimeMessage class requires a getMessageID() method. + +The client can now fetch it as a header, but since this is a "standard" +RFC822 header, we want to provide an easier access method. + +An added benefit is that protocols like IMAP, which provide this info +as part of the ENVELOPE structure, can implement this method much more +efficiently. + + /** + * Returns the value of the "Message-ID" header field. Returns + * null if this field is unavailable or its value is absent. <p> + * + * The default implementation provided here uses the + * <code>getHeader()</code> method to return the value of the + * "Message-ID" field. + * + * @return Message-ID + * @exception MessagingException, if the retrieval of this field + * causes any exception. + * @see javax.mail.search.MessageIDTerm + */ + public String getMessageID() throws MessagingException; + +=================================================================== + +(3) UIDFolder +============= + +The following methods in the UIDFolder interface have certain problems. + +We are proposing changes that will fix these problems. These are +binary incompatible changes, but we think these changes are necessary for +this interface to work in a useful manner. We are also not aware of +anyone that has shipped a Store Provider that implements this interface, +so we are hoping that the effect of this change is minimal (non-existent). + +(1) Message getMessageByUID(long uid) + +Description: + Get the Message corresponding to the given UID. If no such +message exists, the java.util.NoSuchElementException is thrown. + +Problem: + A disconnected client can have a stash of UIDs from a +previous session. Some of these UIDs may have been expunged from the +server, but the disconnected client does not know this. It is quite +reasonable (and expected) for a disconnected client to use this +method when reconnecting - to check whether a UID is valid or not at +the server. + Since failure is an expected result here, using a +RunTimeException to indicate it, seems wrong and counter-intuitive for +the client. + +Solution: + Our solution is to allow this method to return null to indicate +that the requested UID is not valid anymore. Thus, this method will no +longer throw the java.util.NoSuchElementException. + +(2) Message[] getMessagesByUID(long start, long end) + +Description: + Get the Messages corresponding to the given UID range. If any +UID is invalid, the java.util.NoSuchElementException is thrown. + +Problem: + Similar, but worse than (1). We think that disconnected clients +(or clients that prefer to use UIDs) may issue + getMessagesByUID(1, LASTUID) +to get all the Messages at the server, especially when the client does +not know the exact UIDs for this folder. In this case, we certainly +do not want this method to fail if some id in the given range is +not a valid UID; rather we want it to return all available Messages from +the server. + +Solution: + Our solution is to remove the NoSuchElementException exception +and allow this method to return Message objects in the given range. + +(3) Message[] getMessagesByUID(long[] uids) + +Description: + Get the Messages corresponding to the given UID range. If any +UID is invalid, the java.util.NoSuchElementException is thrown. + +Problem: + Identical to (1). + A disconnected client can have a stash of UIDs from a +previous session. Some of these UIDs may have been expunged from the +server, but the disconnected client does not know this. It is quite +reasonable (and expected) for a disconnected client to use this +method when reconnecting - to check whether a set of UIDs are valid +or not at the server. + Since failure can be an expected result, using a +RunTimeException to indicate it, seems wrong and counter-intuitive for +the client. + +Solution: + Our solution is to allow this method to return null entries +to indicate that a requested UID is not valid anymore. Thus, the +message array returned by this method can have null entries for the +invalid UIDs; and this method will no longer throw the +java.util.NoSuchElementException. + Note that the size of the returned Message array is the same +as the size of the request array of UIDs, and that each entry in the +Message array corresponds to the same index entry in the UID array. +=================================================================== + +(4) InternetAddress +=================== + +The InternetAddress class needs the following protected field to +properly support encoding of the "personal name". + + protected String encodedPersonal; // the encoded personal name + +No other API changes are associated with this. + +This is a binary compatible change to JavaMail 1.0. However, there is +a potential problem for any existing InternetAddress subclasses, which +can cause them to break. This typically will affect only providers, +not clients. + +Details: +------- + +The InternetAddress implementation will use this field to store the +encoded personal name. + + The getPersonal() method will return the protected +'personal' field; if this field is null, it will check the 'encodedPersonal' +field and decode its value and return it. + The toString() method will use the protected 'encodedPersonal' +field to create the RFC2047 compliant address string. If this field is +null, it will check the 'personal' field, encode that if necessary and +use it. + +This implies that, if an InternetAddress subclass changes either +the 'personal' or 'encodedPersonal' fields, it should set the other +to null to force its recomputation. Unfortunately, this also implies +that existing subclasses of InternetAddress that directly set the +'personal' field can break in certain situations. + We feel that the risk of this happening is minimal, since we +don't expect that our users have subclassed InternetAddress. Also, this +is necessary to properly support encoded personal names, so we feel that +this has to be done. +================================================================ + +(5) MimeCharset +================ + +A utility method to convert java charsets into MIME charsets is +needed. + +The JDK supports a variety of charsets. The JDK has names for these +charsets, unfortunately they dont always match to their MIME or IANA +equivalents. + +It is necessary in some cases, (especially for providers) to map the +JDK charset names into their MIME or IANA equivalents. This method does +that. + +The API: +------- + +This is a new static method to the javax.mail.internet.MimeUtility class + + /** + * Convert a java charset into its MIME charset name. <p> + * + * Note that a future version of JDK (post 1.2) might provide + * this functionality, in which case, we might deprecate this + * method then. + * + * @param charset the JDK charset + * @return the MIME/IANA equivalent. If a mapping + * is not possible, the passed in charset itself + * is returned. + */ + public static String mimeCharset(String charset); + +==================================================================== + +(6) getDefaultJavaCharset() +============================ + +This method returns the default charset for the platform's locale, as +a Java charset. This is a new static method to the MimeUtility class. + + /** + * Get the default charset for this locale. <p> + * + * @return the default charset of the platform's locale, + * as a Java charset. (NOT a MIME charset) + */ + public static String getDefaultJavaCharset() + +==================================================================== + +(7) Method to print out the nested Exceptions +============================================= + +The MessagingException class currently allows nested exceptions. It +would be nice to have one single method to dump out all the +nested exceptions' messages into System.out + +Proposal +-------- + +Override the getMessage() method from the superclass (Throwable), to +append the messages from all nested Exceptions. This is similar to +java.rmi.RemoteException. + +================================================================== + +(8) New SearchTerms +==================== + +The current address related search terms - AddressTerm, FromTerm and +RecipientTerm are limited in that they operate on Address objects, not +Strings. These terms use the equals() method to compare the addresses - +which is not useful for the common case of substring comparisons. + +Hence we introduce three new SearchTerms: + +public AddressStringTerm extends StringTerm { + /** + * Constructor. + * @param pattern the address pattern to be compared + */ + protected AddressStringTerm(String pattern); + + /** + * Check whether the address pattern specified in the + * constructor is a substring of the string representation of + * the given Address object. + * + * @param address the address to match against + */ + protected boolean match(Address address); +} + +public FromStringTerm extends AddressStringTerm { + /** + * Constructor. + * @param address the address to be compared. + */ + public FromStringTerm(String string); + + /** + * Check whether the address specified in the constructor is + * a substring of the "From" attribute of this message. + * + * @param msg the address comparison is applied to this Message + */ + public boolean match(Message msg); +} + +public RecipientStringTerm extends AddressStringTerm { + + /** + * Constructor. + * + * @param type the recipient type + * @param address the address to be compared. + */ + public RecipientStringTerm(Message.RecipientType type, String address); + + /** + * Return the type of recipient to match with. + */ + public Message.RecipientType getRecipientType(); + + /** + * Check whether the address specified in the constructor is + * a substring of the given Recipient attribute of this message. + * + * @param msg the address comparison is applied to this Message + */ + public boolean match(Message msg); +} + +================================================================== + +(9) InternetAddress.toString(Address[] addresses, int used) +=========================================================== + +As per RFC2047 (MIME), the length of a header field that contains +encoded-words is limited to 76 characters. + +There are two methods in the InternetAddress class that generate +RFC822 style address strings: + + - The toString() method on InternetAddress, which generates + the string for one InternetAddress object + - The toString() static method, which generates a comma separated + string for the given array of InternetAddress objects. + +Both these methods currently do not honor the 76 character limit. +Actually, the former does to an extent, since the encodedWord +generator (i.e the MimeUtility.encodeWord() method) does break +encoded words into multiples, if they stretch beyond 76 characters. + +Solution +-------- + +For the 1.1 release, we are planning to fix the the toString() +static method as follows: + +Add a new static method + static String toString(Address[] address, int used) + +This method takes an array of Address objects and an integer representing +the number of "used" character positions for the first line of this +field. The typical use of this method is when setting RFC822 headers, +like the "From" header. 'used' can be set to sizeof("From: ") in +this case. + +When generating the string, this method starts a new line if the +addition of the next address.toString() causes the current line's +line-length to go over 76. + +Note that this algorithm does not work right if the length of a single +InternetAddress is itself more than 76 characters. Also, it does not +optimally break an address field, so that the maximum characters are +accommodated in a single line. + +So, essentially, this is an initial attempt to solve this problem. We +will add more APIs in the next version to further refine this. +================================================================== + +(10) Folder.getMode() +===================== + +It is currently not possible to tell whether a folder is open READ_ONLY +or READ_WRITE without attempting to write it and catching the exception. + +We propose to add a protected field to Folder to store the open mode and +a new method to Folder to return the open mode. Because existing +subclasses will not use this new field, we can't guarantee that the +method will always return the correct value (although all Folder subclasses +in the JavaMail package will be updated to return the correct value). + + /** + * The open mode (<code>Folder.READ_ONLY</code>, + * <code>Folder.READ_WRITE</code>, or -1 if not known). + */ + protected int mode = -1; + + /** + * Return the open mode of this folder. Returns + * <code>Folder.READ_ONLY</code>, <code>Folder.READ_WRITE</code>, + * or -1 if the open mode is not known (usually only because an older + * <code>Folder</code> provider has not been updated to use this new + * method). + * + * @exception IllegalStateException if this folder is not opened + * @return the open mode of this folder + */ + public int getMode() { + if (!isOpen()) + throw new IllegalStateException("Folder not open"); + return mode; + } + +==================================================================== + +(11) Folder.getURLName() +======================== + +The URLName support in the JavaMail 1.0 API's is incomplete and +inadequately specified. While you can get a Folder from a Session +given a URLName for the folder, you can't find out the URLName +for a given Folder object. We propose adding the following method +to Folder: + + /** + * Return a URLName representing this folder. The returned URLName + * does <em>not</em> include the password used to access the store. + * + * @return the URLName representing this folder + * @see URLName + */ + public URLName getURLName() throws MessagingException + +Previously it was not specified whether the URLName returned from +the Store.getURLName method would include the password field or +not, but sometimes it did. We propose to tighen the specification +and fix the implementation so that the password field is not returned: + + /** + * Return a URLName representing this store. The returned URLName + * does <em>not</em> include the password field. <p> + * + * Subclasses should only override this method if their + * URLName does not follow the standard format. + * + * @return the URLName representing this store + * @see URLName + */ + public URLName getURLName() + +Similarly for Transport. + +Previously it was unspecified how the Store and Transport connect +methods interacted with the url field. In some cases it would be +updated and in other cases it would not. We propose to tighen the +specification as follows: + + /** + * Connect to the specified address. This method provides a simple + * authentication scheme that requires a username and password. <p> + * + * If the connection is successful, an "open" ConnectionEvent is + * delivered to any ConnectionListeners on this Store. <p> + * + * It is an error to connect to an already connected Store. <p> + * + * The implementation in the Store class will collect defaults + * for the host, user, and password from the session, prompting the + * user if necessary, and will then call the protocolConnect method, + * which the subclass must override. The subclass should also + * implement the <code>getURLName</code> method, or use the + * implementation in this class. <p> + * + * On a successful connection, the <code>setURLName</code> method is + * called with a URLName that includes the information used to make + * the connection, including the password. <p> + * + * If the password passed in is null and this is the first successful + * connection to this store, the user name and the password + * collected from the user will be saved as defaults for subsequent + * connection attempts to this same store. If the password passed + * in is not null, it is not saved, on the assumption that the + * application is managing passwords explicitly. + * + * @param host the host to connect to + * @param user the user name + * @param password this user's password + * @exception AuthenticationFailedException for authentication failures + * @exception MessagingException for other failures + * @exception IllegalStateException if the store is already connected + * @see javax.mail.event.ConnectionEvent + */ + public void connect(String host, String user, String password) + throws MessagingException + +And add this method to Store and Transport: + + /** + * Set the URLName representing this store. + * Normally used to update the <code>url</code> field + * after a store has successfully connected. <p> + * + * Subclasses should only override this method if their + * URLName does not follow the standard format. <p> + * + * The implementation in the Store class simply sets the + * <code>url</code> field. + * + * @see URLName + */ + protected void setURLName(URLName url) + +And finally, to simplify the implementation of Store and Transport, +and make the common design patterns between them more clear, we're +considering introducing a new Service class as a superclass of +Store and Transport, and moving all common methods (the various +connnect methods, URLName methods, and some listener methods) to +the superclass. Note that this is a binary compatible change. + +===================================================================== + +12) New Service class +====================== + +To emphasize the commonality in behavior between the Store and +Transport classes, and to simplify maintenance of these classes, we +propose moving many of the common methods to a new superclass called +javax.mail.Service. Store and Transport would then extend Service. +These existing methods currently have identical implementations in the +Store and Transport classes so moving them to a common superclass will +not change the behavior of either Store or Transport. + +The Service class will contain all the methods and fields having to do +with connecting, connection listeners, and naming via URLNames. The +Store class retains the methods for getting Folders and managing Store +and Folder listeners. The Transport class retains the methods for +sending messages and managing Transport listeners. + +Note that this is a binary compatible change both for existing users of +the Store and Transport classes, as well as for existing subclasses of +these classes. + +======================================================================
diff --git a/doc/spec/JavaMail-1.2-changes.txt b/doc/spec/JavaMail-1.2-changes.txt new file mode 100644 index 0000000..8c64f08 --- /dev/null +++ b/doc/spec/JavaMail-1.2-changes.txt
@@ -0,0 +1,1022 @@ + + JavaMail 1.2 + ============ + +Following is a description of the new APIs that are being +introduced in JavaMail 1.2. The numbers in parentheses +are bug numbers; you can find more information about the +bug reports at: + + http://developer.java.sun.com/developer/bugParade/index.html + +Please send comments and feedback to javamail@sun.com. + + +=================================================================== + +1. New protected field and methods (4129743) +-------------------------------------------- + +To simplify creating subclasses of MimeMessage, the following +field and method that were previously private are now protected: + + /** + * A flag indicating whether the message has been modified. + * If the message has not been modified, any data in the + * <code>content</code> array is assumed to be valid and is used + * directly in the <code>writeTo</code> method. This flag is + * set to true when an empty message is created or when the + * <code>saveChanges</code> method is called. + */ + protected boolean modified = false; + + /** + * Parse the InputStream setting the <code>headers</code> and + * <code>content</code> fields appropriately. Also resets the + * <code>modified</code> flag. <p> + * + * This method is intended for use by subclasses that need to + * control when the InputStream is parsed. + * + * @param is The message input stream + * @exception MessagingException + */ + protected void parse(InputStream is) throws MessagingException + + +The javadocs for the following exsting methods have been updated to +describe the use of the modified flag: + + /** + * Default constructor. An empty message object is created. + * The <code>headers</code> field is set to an empty InternetHeaders + * object. The <code>flags</code> field is set to an empty Flags + * object. The <code>modified</code> flag is set to true. + */ + public MimeMessage(Session session) + + /** + * Output the message as an RFC 822 format stream, without + * specified headers. If the <code>modified</code> flag is not + * set and the <code>content</code> array is not null, the + * <code>content</code> array is written directly, after + * writing the appropriate message headers. + * + * @exception javax.mail.MessagingException + * @exception IOException if an error occurs writing to the stream + * or if an error is generated by the + * javax.activation layer. + * @see javax.activation.DataHandler#writeTo + */ + public void writeTo(OutputStream os, String[] ignoreList) + throws IOException, MessagingException + + /** + * Updates the appropriate header fields of this message to be + * consistent with the message's contents. If this message is + * contained in a Folder, any changes made to this message are + * committed to the containing folder. <p> + * + * If any part of a message's headers or contents are changed, + * <code>saveChanges</code> must be called to ensure that those + * changes are permanent. Otherwise, any such modifications may or + * may not be saved, depending on the folder implementation. <p> + * + * Messages obtained from folders opened READ_ONLY should not be + * modified and saveChanges should not be called on such messages. <p> + * + * This method sets the <code>modified</code> flag to true and then + * calls the <code>updateHeaders<code> method. + * + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException + */ + public void saveChanges() throws MessagingException + +The following method is added to MimeMessage: + + /** + * Create and return an InternetHeaders object that loads the + * headers from the given InputStream. Subclasses can override + * this method to return a subclass of InternetHeaders, if + * necessary. This implementation simply constructs and returns + * an InternetHeaders object. + * + * @param is the InputStream to read the headers from + * @exception MessagingException + */ + protected InternetHeaders createInternetHeaders(InputStream is) + throws MessagingException + +Likewise, to simplify creating subclasses of MimeMultipart, the parse +method is made protected: + + /** + * Parse the InputStream from our DataSource, constructing the + * appropriate MimeBodyParts. The <code>parsed</code> flag is + * set to true, and if true on entry nothing is done. This + * method is called by all other methods that need data for + * the body parts, to make sure the data has been parsed. + */ + protected synchronized void parse() throws MessagingException + +and the following protected methods are added: + + /** + * Create and return an InternetHeaders object that loads the + * headers from the given InputStream. Subclasses can override + * this method to return a subclass of InternetHeaders, if + * necessary. This implementation simply constructs and returns + * an InternetHeaders object. + * + * @param is the InputStream to read the headers from + * @exception MessagingException + */ + protected InternetHeaders createInternetHeaders(InputStream is) + throws MessagingException + + /** + * Create and return a MimeBodyPart object to represent a + * body part parsed from the InputStream. Subclasses can override + * this method to return a subclass of MimeBodyPart, if + * necessary. This implementation simply constructs and returns + * a MimeBodyPart object. + * + * @param headers the headers for the body part + * @param content the content ofthe body part + * @exception MessagingException + */ + protected MimeBodyPart createMimeBodyPart(InternetHeaders headers, + byte[] content) throws MessagingException + + /** + * Create and return a MimeBodyPart object to represent a + * body part parsed from the InputStream. Subclasses can override + * this method to return a subclass of MimeBodyPart, if + * necessary. This implementation simply constructs and returns + * a MimeBodyPart object. + * + * @param is InputStream containing the body part + * @exception MessagingException + */ + protected MimeBodyPart createMimeBodyPart(InputStream is) + throws MessagingException + +=================================================================== + +2. MimeMessage and MimeBodyPart getRawInputStream method (4124840) +------------------------------------------------------------------ + +In some cases it is desirable to get the data for a body part +before JavaMail attempts to decode it. This is particularly important +if the Content-Transfer-Encoding header is incorrect. (For example, +some mail software is known to use "7-bit" instead of the MIME +defined "7bit".) Access to this data is currently provided +through the protected getContentStream method. Since simply +making this method public has the potential to cause a source +incompatibility for any subclasses that declare this method as +protected, we instead add a new public method that calls this +protected method: + + /** + * Return an InputStream to the raw data with any Content-Transfer-Encoding + * intact. This method is useful if the "Content-Transfer-Encoding" + * header is incorrect or corrupt, which would prevent the + * <code>getInputStream</code> method or <code>getContent</code> method + * from returning the correct data. In such a case the application may + * use this method and attempt to decode the raw data itself. <p> + * + * This implementation simply calls the <code>getContentStream</code> + * method. + * + * @see #getInputStream + * @see #getContentStream + */ + public InputStream getRawInputStream() throws MessagingException + + +=================================================================== + +3. ReadOnlyFolderException (4163360) +------------------------------------ + +A new MessagingException subclass to indicate a read-only folder +is needed. + +Currently, if a client attempts to open a folder in read-write mode +and the folder is read-only, a MessagingException is thrown. This +exception type does not indicate that the anomaly was caused due to +the lack of write-permissions. + +/** + * This exception is thrown when an attempt is made to open a folder + * with read-write access when the folder is marked read-only. <p> + */ + +public class ReadOnlyFolderException extends MessagingException { + + /** + * Constructor. + * @param folder the Folder + */ + public ReadOnlyFolderException(Folder folder) + + /** + * Constructor. + * @param folder the Folder + * @param message the detailed error message + */ + public ReadOnlyFolderException(Folder folder, String message) + + /** + * Returns the Folder object. + */ + public Folder getFolder() +} + +=================================================================== + +4. InternetAddress implements Cloneable (4181144) +------------------------------------------------- + +To simplify copying of InternetAddress objects, the InternetAddress +class will implement the Cloneable interface and will provide a +public clone method. + +public class InternetAddress extends Address implements Cloneable { + + /** + * Return a copy of this InternetAddress object. + */ + public Object clone() +} + +=================================================================== + +5. MimeMessage copy constructor (4107752) +----------------------------------------- + +When forwarding or saving a message retrieved from a Store, it is +sometimes desirable to be able to modify the message first. Since +most Stores do not allow their Message objects to be modified, the +message must first be copied. To simplify copying a MimeMessage, +we introduce a copy constructor that allows a new MimeMessage to +be created and initialized with a copy of another MimeMessage. + + /** + * Constructs a new MimeMessage with content initialized form the + * <code>source</code> MimeMessage. The new message is independent + * of the original. <p> + * + * @param source the message to copy content from + * @exception MessagingException + */ + public MimeMessage(MimeMessage source) throws MessagingException + +=================================================================== + +6. AuthenticationFailedException includes server message (4230553) +------------------------------------------------------------------ + +When authentication with a server fails, the server often supplies +some information in its protocol message that indicates the reason +for the failure. To allow a service provider to return this information +to the user, we now allow the Service.protocolConnect() method to +throw an AuthenticationFailedException in this case. The exception +may contain a string message that includes the additional information +from the server. + + /** + * The service implementation should override this method to + * perform the actual protocol-specific connection attempt. + * The default implementation of the <code>connect</code> method + * calls this method as needed. <p> + * + * The <code>protocolConnect</code> method should return + * <code>false</code> if a user name or password is required + * for authentication but the corresponding parameter is null; + * the <code>connect</code> method will prompt the user when + * needed to supply missing information. This method may + * also return <code>false</code> if authentication fails for + * the supplied user name or password. Alternatively, this method + * may throw an AuthenticationFailedException when authentication + * fails. This exception may include a String message with more + * detail about the failure. <p> + * + * The <code>protocolConnect</code> method should throw an + * exception to report failures not related to authentication, + * such as an invalid host name or port number, loss of a + * connection during the authentication process, unavailability + * of the server, etc. + * + * @param host the name of the host to connect to + * @param port the port to use (-1 means use default port) + * @param user the name of the user to login as + * @param password the user's password + * @return true if connection successful, false if authentication failed + * @exception AuthenticationFailedException for authentication failures + * @exception MessagingException for non-authentication failures + */ + protected boolean protocolConnect(String host, int port, String user, + String password) throws MessagingException + +=================================================================== + +7. Support ESMTP 8BITMIME extension (4132029) +--------------------------------------------- + +JavaMail chooses the encoding for body parts when the Message.saveChanges() +method is called. At the time this method is called, JavaMail has no +information about the Transport that might be used to send the message. +Thus, communicating the Transport's capabilities to the part of JavaMail +that chooses body part encodings is problematic. + +To provide some minimal support for the 8BITMIME extension, the SMTP +protocol provider is extended in the following way. Note that this +capability is part of Sun's SMTP protocol provider, and is *not* a +part of the JavaMail API spec. We include it here for convenience only. + +If the property "mail.smtp.allow8bitmime" is set to "true", and the +SMTP server supports the 8BITMIME extension, the SMTP Transport will +traverse the message and adjust the Content-Transfer-Encoding of text +body parts from "quoted-printable" or "base64" to "8bit" as appropriate. + +Note that if the same message is subsequently sent over a Transport +or to a server that does not support 8bit MIME, the message will *not* +be converted back to a non-8bit encoding. Normally this will not be a +problem. Note also that a message using "8bit" encoding can be safely +appended to an IMAP folder. + +=================================================================== + +8. Make MailDateFormat class public (4266390) +--------------------------------------------- + +It would be useful to have the MailDateFormat class available as part +of the javax.mail.internet package to allow parsing and formatting +dates in other MIME headers. + +/** + * Formats and parses date specification based on the + * draft-ietf-drums-msg-fmt-08 dated January 26, 2000. This is a followup + * spec to RFC822.<p> + * + * This class does not take pattern strings. It always formats the + * date based on the specification below.<p> + * + * 3.3 Date and Time Specification<p> + * + * Date and time occur in several header fields of a message. This section + * specifies the syntax for a full date and time specification. Though folding + * whitespace is permitted throughout the date-time specification, it is + * recommended that only a single space be used where FWS is required and no + * space be used where FWS is optional in the date-time specification; some + * older implementations may not interpret other occurrences of folding + * whitespace correctly.<p> + * + * date-time = [ day-of-week "," ] date FWS time [CFWS]<p> + * + * day-of-week = ([FWS] day-name) / obs-day-of-week<p> + * + * day-name = "Mon" / "Tue" / "Wed" / "Thu" / "Fri" / "Sat" / "Sun"<p> + * + * date = day month year<p> + * + * year = 4*DIGIT / obs-year<p> + * + * month = (FWS month-name FWS) / obs-month<p> + * + *<pre>month-name = "Jan" / "Feb" / "Mar" / "Apr" / + * "May" / "Jun" / "Jul" / "Aug" / + * "Sep" / "Oct" / "Nov" / "Dec" + * </pre><p> + * day = ([FWS] 1*2DIGIT) / obs-day<p> + * + * time = time-of-day FWS zone<p> + * + * time-of-day = hour ":" minute [ ":" second ]<p> + * + * hour = 2DIGIT / obs-hour<p> + * + * minute = 2DIGIT / obs-minute<p> + * + * second = 2DIGIT / obs-second<p> + * + * zone = (( "+" / "-" ) 4DIGIT) / obs-zone<p> + * + * + * The day is the numeric day of the month. The year is any numeric year in + * the common era.<p> + * + * The time-of-day specifies the number of hours, minutes, and optionally + * seconds since midnight of the date indicated.<p> + * + * The date and time-of-day SHOULD express local time.<p> + * + * The zone specifies the offset from Coordinated Universal Time (UTC, + * formerly referred to as "Greenwich Mean Time") that the date and + * time-of-day represent. The "+" or "-" indicates whether the time-of-day is + * ahead of or behind Universal Time. The first two digits indicate the number + * of hours difference from Universal Time, and the last two digits indicate + * the number of minutes difference from Universal Time. (Hence, +hhmm means + * +(hh * 60 + mm) minutes, and -hhmm means -(hh * 60 + mm) minutes). The form + * "+0000" SHOULD be used to indicate a time zone at Universal Time. Though + * "-0000" also indicates Universal Time, it is used to indicate that the time + * was generated on a system that may be in a local time zone other than + * Universal Time.<p> + * + * A date-time specification MUST be semantically valid. That is, the + * day-of-the week (if included) MUST be the day implied by the date, the + * numeric day-of-month MUST be between 1 and the number of days allowed for + * the specified month (in the specified year), the time-of-day MUST be in the + * range 00:00:00 through 23:59:60 (the number of seconds allowing for a leap + * second; see [STD-12]), and the zone MUST be within the range -9959 through + * +9959.<p> + * + */ +public class MailDateFormat extends SimpleDateFormat { + + public MailDateFormat() + + /** + * Formats the given date in the format specified by + * draft-ietf-drums-msg-fmt-08 in the current TimeZone + * + * @param date the Date object + * @param dateStrBuf the formatted string + * @param fieldPosition the current field position + * @returns StringBuffer the formatted String + */ + public StringBuffer format(Date date, StringBuffer dateStrBuf, + FieldPosition fieldPosition) + + /** + * Parses the given date in the format specified by + * draft-ietf-drums-msg-fmt-08 in the current TimeZone + * + * @param text the formatted date to be parsed + * @param pos the current parse position + * @returns Date the parsed date in a Date object + */ + public Date parse(String text, ParsePosition pos) +} + +=================================================================== + +9. String-based MimeMessage setRecipients and addRecipients methods (4328824) +----------------------------------------------------------------------------- +The following convenience methods will be added to MimeMessage. They take a +String for setting/adding a recipient (instead of javax.mail.Address objects). + + /** + * Set the specified recipient type to the given addresses. + * If the address parameter is <code>null</code>, the corresponding + * recipient field is removed. + * + * @param type Recipient type + * @param addresses Addresses + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException + * @see #getRecipients + */ + public void setRecipients(Message.RecipientType type, String addresses) + throws MessagingException + + /** + * Add the given addresses to the specified recipient type. + * + * @param type Recipient type + * @param addresses Addresses + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values. + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException + */ + public void addRecipients(Message.RecipientType type, String addresses) + throws MessagingException + +=================================================================== + +10. Add Session.getDefaultInstance(Properties props) and + Session.getInstance(Properties props) methods (4328826) +--------------------------------------------------------------- + +These are convenience methods for retrieving the default Session or a new +Session object which does not require an Authenticator parameter +(it is assumed to be null). + + /** + * Get the default Session object. If a default has not yet been + * setup, a new Session object is created and installed as the + * default.<p> + * + * Note that a default session created with no Authenticator is + * available to all code executing in the same Java virtual + * machine, and the session can contain security sensitive + * information such as user names and passwords. + * + * @param props Properties object. Used only if a new Session + * object is created.<br> + * It is expected that the client supplies values + * for the properties listed in Appendix A of the + * JavaMail spec (particularly mail.store.protocol, + * mail.transport.protocol, mail.host, mail.user, + * and mail.from) as the defaults are unlikely to + * work in all cases. + * @return the default Session object + */ + public static Session getDefaultInstance(Properties props) + + /** + * Get a new Session object. + * + * @param props Properties object that hold relevant properties.<br> + * It is expected that the client supplies values + * for the properties listed in Appendix A of the + * JavaMail spec (particularly mail.store.protocol, + * mail.transport.protocol, mail.host, mail.user, + * and mail.from) as the defaults are unlikely to + * work in all cases. + * @return a new Session object + * @see javax.mail.Authenticator + */ + public static Session getInstance(Properties props) + +=================================================================== + +11. Allow for providing a filename when using MimeUtility.encode() (4140579) +---------------------------------------------------------------------------- + +The UUEncode encoder requires the filename to be inserted into the encoded +stream. The public access point to the encoder is thru the MimeUtility.encode() +method, which does not have any parameter that can provide the filename. +Hence the uuencoded stream always has "encode.buf" as filename. This new +method allows the setting of the filename. + + /** + * Wrap an encoder around the given output stream. + * All the encodings defined in RFC 2045 are supported here. + * They include "base64", "quoted-printable", "7bit", "8bit" and + * "binary". In addition, "uuencode" is also supported. + * The <code>filename</code> parameter is used with the "uuencode" + * encoding and is included in the encoded output. + * + * @param os output stream + * @param encoding the encoding of the stream. + * @param filename name for the file being encoded + * @exception MessagingException + * @return output stream that applies the + * specified encoding. + */ + public static OutputStream encode(OutputStream os, String encoding, + String filename) + throws MessagingException + +=================================================================== + +12. New exception constructors (4259211) +---------------------------------------- + +The FolderNotFoundException constructors are not consistant with other +exceptions defined in the API. New constructors are needed to eliminate +these inconsistencies. + + /** + * Constructs a MessagingException with the specified folder + * @param folder the Folder + */ + public FolderNotFoundException(Folder folder) + + /** + * Constructs a MessagingException with the specified detail message + * and the specified folder. + * @param folder the Folder + * @param s the detail message + */ + public FolderNotFoundException(Folder folder, String s) + +=================================================================== + +13. InternetAddress.toUnicodeString() method (4281729) +------------------------------------------------------- + +Problem: AddressStringTerm.match does not return the correct results +in some situations. + +AddressStringTerm wants to do the match against the formatted address +string in Unicode, not the ASCII version that might include charset +encoding information. To do this it attempts to format the address +itself, but it's not smart enough to know all the rules about +formatting an address (e.g., when to quote the personal name) so it +does this formatting differently than InternetAddress.toString does. +When the address contains only ASCII characters, the formatting should +be identical. + + /** + * Returns a properly formatted address (RFC 822 syntax) of + * Unicode characters. + * + * @return Unicode address string + */ + public String toUnicodeString() + +=================================================================== + +14. Call saveChanges automatically on newly constructed message (4339203) +------------------------------------------------------------------------- + +One of the most common errors when constructing new messages is forgetting +to call the saveChanges() method before writing out the message or calling +the Transport.sendMessage() method. To solve this problem we add a flag +to MimeMessage and change the writeTo() method accordingly: + + /** + * Does the <code>saveChanges</code> method need to be called on + * this message? This flag is set to false by the public constructor + * and set to true by the <code>saveChanges</code> method. The + * <code>writeTo</code> method checks this flag and calls the + * <code>saveChanges</code> method as necessary. This avoids the + * common mistake of forgetting to call the <code>saveChanges</code> + * method on a newly constructed message. + */ + protected boolean saved = false; + + /** + * Updates the appropriate header fields of this message to be + * consistent with the message's contents. If this message is + * contained in a Folder, any changes made to this message are + * committed to the containing folder. <p> + * + * If any part of a message's headers or contents are changed, + * <code>saveChanges</code> must be called to ensure that those + * changes are permanent. Otherwise, any such modifications may or + * may not be saved, depending on the folder implementation. <p> + * + * Messages obtained from folders opened READ_ONLY should not be + * modified and saveChanges should not be called on such messages. <p> + * + * This method sets the <code>modified</code> flag to true, the + * <code>save</code> flag to true, and then calls the + * <code>updateHeaders<code> method. + * + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException + */ + public void saveChanges() throws MessagingException + + /** + * Output the message as an RFC 822 format stream, without + * specified headers. If the <code>saved</code> flag is not set, + * the <code>saveChanges</code> method is called. + * If the <code>modified</code> flag is not + * set and the <code>content</code> array is not null, the + * <code>content</code> array is written directly, after + * writing the appropriate message headers. + * + * @exception javax.mail.MessagingException + * @exception IOException if an error occurs writing to the stream + * or if an error is generated by the + * javax.activation layer. + * @see javax.activation.DataHandler#writeTo + */ + public void writeTo(OutputStream os, String[] ignoreList) + throws IOException, MessagingException + +=================================================================== + +15. New MimeUtility.getEncoding(DataHandler) method (4340648) +------------------------------------------------------------- + +To improve the performance of JavaMail, we previously added a (package +private) getEncoding() method to MimeUtility. This method is now public: + + /** + * Same as <code>getEncoding(DataSource)</code> except that instead + * of reading the data from an <code>InputStream</code> it uses the + * <code>writeTo</code> method to examine the data. This is more + * efficient in the common case of a <code>DataHandler</code> + * created with an object and a MIME type (for example, a + * "text/plain" String) because all the I/O is done in this + * thread. In the case requiring an <code>InputStream</code> the + * <code>DataHandler</code> uses a thread, a pair of pipe streams, + * and the <code>writeTo</code> method to produce the data. <p> + */ + public static String getEncoding(DataHandler dh) + +=================================================================== + +16. New TransportEvent.getMessage() method (4331674) +----------------------------------------------------- + +The TransportEvent class saves the message that caused the error, +but provides no getMessage method for the listener to retrieve the +Message object. The following method will be added: + + /** + * Get the Message object associated with this Transport Event. + * + * @return the Message object + */ + public Message getMessage() + + +=================================================================== + +17. javax.mail.search terms should be serializable (4126013) +------------------------------------------------------------ + +The javax.mail.search package allows you to programmatically construct +a search term. It would be convenient if those terms could be saved +in persistent storage and restored in a later session. Using +serialization to store these expressions is the simplest approach. + +Many of the search terms reference other objects that must also be +serializable. The most problematic such objects are of the class +Message.RecipientType. This class uses the java "type-safe enum" +idiom, which involves a number of static final instances of the class. +Applications are allowed to test for equivalence with these "constants" +by using the "==" equality operator. Thus, it's critical that only a +single instance of each constant exist in the Java virtual machine. +To ensure that this constraint is met when deserializing an object +of this class, we must take advantage of the JDK 1.2 readReplace() +method. Since this method is not available on JDK 1.1, objects of +this class, and thus search terms that reference them, can not be +correctly deserialized on JDK 1.1. This is a limitation of this +new capability. + +To provide this support, the following classes and all their subclasses +now implement the Serializable interface: + + javax.mail.search.SearchTerm + javax.mail.Address + javax.mail.Flags + javax.mail.Message.RecipientType + +In addition, to allow comparison between search terms, the equals +and hashCode methods on SearchTerm (and all subclasses) now implement +"value" equivalence rather than identity equivalence. + + +=================================================================== + +18. Support IMAP NAMESPACE extension (4364827) +------------------------------------------------------------ + +We propose the following new APIs to be added to javax.mail.Store to +provide namespace information. If the IMAP server supports the +NAMESPACE extension, it will be used to return this information. + + /** + * Return a set of folders representing the <i>personal</i> namespaces + * for the current user. A personal namespace is a set of names that + * is considered within the personal scope of the authenticated user. + * Typically, only the authenticated user has access to mail folders + * in their personal namespace. If an INBOX exists for a user, it + * must appear within the user's personal namespace. In the + * typical case, there should be only one personal namespace for each + * user in each Store. <p> + * + * This implementation returns an array with a single entry containing + * the return value of the <code>getDefaultFolder</code> method. + * Subclasses should override this method to return appropriate information. + * + * @exception IllegalStateException if this Store is not connected. + * @return array of Folder objects + */ + public Folder[] getPersonalNamespaces() throws MessagingException + + /** + * Return a set of folders representing the namespaces for + * <code>user</code>. The namespaces returned represent the + * personal namespaces for the user. To access mail folders in the + * other user's namespace, the currently authenticated user must be + * explicitly granted access rights. For example, it is common for + * a manager to grant to their secretary access rights to their + * mail folders. <p> + * + * This implementation returns an empty array. Subclasses should + * override this method to return appropriate information. + * + * @exception IllegalStateException if this Store is not connected. + * @return array of Folder objects + */ + public Folder[] getUserNamespaces(String user) throws MessagingException + + /** + * Return a set of folders representing the <i>shared</i> namespaces. + * A shared namespace is a namespace that consists of mail folders + * that are intended to be shared amongst users and do not exist + * within a user's personal namespace. <p> + * + * This implementation returns an empty array. Subclasses should + * override this method to return appropriate information. + * + * @exception IllegalStateException if this Store is not connected. + * @return array of Folder objects + */ + public Folder[] getSharedNamespaces() throws MessagingException + + +=================================================================== + +19. Make ContentDisposition class public (4366373) +-------------------------------------------------- + +The javax.mail.internet.ContentDisposition class is package private +and should be made public. The API is: + +/** + * This class represents a MIME ContentDisposition value. It provides + * methods to parse a ContentDisposition string into individual components + * and to generate a MIME style ContentDisposition string. + */ + +public class ContentDisposition + + /** + * No-arg Constructor. + */ + public ContentDisposition() + + /** + * Constructor. + * + * @param disposition disposition + * @param list ParameterList + */ + public ContentDisposition(String disposition, ParameterList list) + + /** + * Constructor that takes a ContentDisposition string. The String + * is parsed into its constituents: dispostion and parameters. + * A ParseException is thrown if the parse fails. + * + * @param s the ContentDisposition string. + * @exception ParseException if the parse fails. + */ + public ContentDisposition(String s) throws ParseException + + /** + * Return the disposition value. + * @return the disposition + */ + public String getDisposition() + + /** + * Return the specified parameter value. Returns <code>null</code> + * if this parameter is absent. + * @return parameter value + */ + public String getParameter(String name) + + /** + * Return a ParameterList object that holds all the available + * parameters. Returns null if no parameters are available. + * + * @return ParameterList + */ + public ParameterList getParameterList() + + /** + * Set the primary type. Overrides existing primary type. + * @param primaryType primary type + */ + public void setDisposition(String disposition) + + /** + * Set the specified parameter. If this parameter already exists, + * it is replaced by this new value. + * + * @param name parameter name + * @param value parameter value + */ + public void setParameter(String name, String value) + + /** + * Set a new ParameterList. + * @param list ParameterList + */ + public void setParameterList(ParameterList list) + + /** + * Retrieve a RFC2045 style string representation of + * this ContentDisposition. Returns <code>null</code> if + * the conversion failed. + * + * @return RFC2045 style string + */ + public String toString() +} + +=================================================================== + +20. Improve performance of MimeMessage (4371862) +-------------------------------------------------- + +To allow us to improve the performance of the MimeMessage and MimeMultipart +classes when parsing data from an InputStream, we introduce a new +interface that allows the data in the InputStream to be shared instead +of copied, and we use this new interface in key parts of the implementation. + +The following field is added to MimeMessage: + + /** + * If the data for this message was supplied by an + * InputStream that implements the SharedInputStream interface, + * <code>contentStream</code> is another such stream representing + * the content of this message. In this case, <code>content</code> + * will be null. + */ + protected InputStream contentStream; + +The following field is added to MimeBodyPart: + + /** + * If the data for this body part was supplied by an + * InputStream that implements the SharedInputStream interface, + * <code>contentStream</code> is another such stream representing + * the content of this body part. In this case, <code>content</code> + * will be null. + */ + protected InputStream contentStream; + +The following interface is added: + +package javax.mail.internet; + +/** + * An InputStream that is backed by data that can be shared by multiple + * readers may implement this interface. This allows users of such an + * InputStream to determine the current positionin the InputStream, and + * to create new InputStreams representing a subset of the data in the + * original InputStream. The new InputStream will access the same + * underlying data as the original, without copying the data. + */ + +public interface SharedInputStream { + /** + * Return the current position in the InputStream, as an + * offset from the beginning of the InputStream. + * + * @return the current position + */ + public long getPosition(); + + /** + * Return a new InputStream representing a subset of the data + * from this InputStream, starting at <code>start</code> (inclusive) + * up to <code>end</code> (exclusive). <code>start</code> must be + * non-negative. If <code>end</code> is -1, the new stream ends + * at the same place as this stream. The returned InputStream + * will also implement the SharedInputStream interface. + * + * @param start the starting position + * @param end the ending position + 1 + * @return the new stream + */ + public InputStream newStream(long start, long end); +} + +=================================================================== + +21. New ParameterList.toString(int used) method. +-------------------------------------------------- + +The ParameterList.toString() method returns its results "unfolded". It +would be useful to have the results "folded" in certain situations. A +new method will be added to the ParamterList class which will return +"folded" results. Folding is defined by RFC 822 as the process of splitting +a header field into multiple lines. "The general rule is that wherever there +may be linear-white-space (NOT simply LWSP-chars), a CRLF immediately +followed by AT LEAST one LWSP-char may instead be inserted." Unfolding is +the process of returning to a single line representation. "Unfolding is +accomplished by regarding CRLF immediately followed by a LWSP-char as +equivalent to the LWSP-char." + + /** + * Convert this ParameterList into a MIME String. If this is + * an empty list, an empty string is returned. + * + * The 'used' parameter specifies the number of character positions + * already taken up in the field into which the resulting address + * sequence string is to be inserted. It's used to determine where + * to "fold" the resulting parameter list. + * + * @param used number of character positions already used, in + * the field into which the parameter list is to + * be inserted. + * @return String + */ + public String toString(int used)
diff --git a/doc/spec/JavaMail-1.3-changes.txt b/doc/spec/JavaMail-1.3-changes.txt new file mode 100644 index 0000000..78cb952 --- /dev/null +++ b/doc/spec/JavaMail-1.3-changes.txt
@@ -0,0 +1,303 @@ + + JavaMail 1.3 + ============ + + (Updated April 1, 2002) + +Following is a description of the new APIs that are being +introduced in JavaMail 1.3. The numbers in parentheses +are bug numbers; you can find more information about the +bug reports at: + + http://developer.java.sun.com/developer/bugParade/index.html + +Please send comments and feedback to javamail@sun.com. + +Many of these changes expand JavaMail's conformance with Internet +standards, or make JavaMail more tolerant of messages that don't +quite conform to the standards. "Be liberal in what you receive +and conservative in what you send." + + +=================================================================== + +1. Add setSender and getSender methods to MimeMessage (4405115) +--------------------------------------------------------------- + +These convenience methods support setting and reading the RFC 822 +Sender header. + + /** + * Returns the value of the RFC 822 "Sender" header field. + * If the "Sender" header field is absent, <code>null</code> + * is returned.<p> + * + * This implementation uses the <code>getHeader</code> method + * to obtain the requisite header field. + * + * @return Address object + * @exception MessagingException + * @see #headers + * @since JavaMail 1.3 + */ + public Address getSender() throws MessagingException + + /** + * Set the RFC 822 "Sender" header field. Any existing values are + * replaced with the given address. If address is <code>null</code>, + * this header is removed. + * + * @param address the sender of this message + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException + * @since JavaMail 1.3 + */ + public void setSender(Address address) throws MessagingException + +=================================================================== + +2. Add setContentID method to MimeBodyPart (4377720) +---------------------------------------------------- + +This convenience method supports setting the Content-ID header. + + /** + * Set the "Content-ID" header field of this body part. + * If the <code>cid</code> parameter is null, any existing + * "Content-ID" is removed. + * + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this body part is + * obtained from a READ_ONLY folder. + * @exception MessagingException + * @since JavaMail 1.3 + */ + public void setContentID(String cid) throws MessagingException + +=================================================================== + +3. Add mail.mime.charset property (4377731) +------------------------------------------- + +The "mail.mime.charset" System property (NOTE: *not* Session property) +names the default charset to be used by JavaMail. If not set, the +standard J2SE "file.encoding" System property is used. This allows +applications to specify a default character set for sending messages +that's different than the character set used for files stored on the +system. This is common on Japanese systems. + +=================================================================== + +4. Add getDeletedMesageCount method to Folder (4388730) +------------------------------------------------------- + +This convenience method would return a count of the number of deleted +messages in a folder. + + /** + * Get the number of deleted messages in this Folder. <p> + * + * This method can be invoked on a closed folder. However, note + * that for some folder implementations, getting the deleted message + * count can be an expensive operation involving actually opening + * the folder. In such cases, a provider can choose not to support + * this functionality in the closed state, in which case this method + * must return -1. <p> + * + * Clients invoking this method on a closed folder must be aware + * that this is a potentially expensive operation. Clients must + * also be prepared to handle a return value of -1 in this case. <p> + * + * This implementation returns -1 if this folder is closed. Else + * this implementation gets each Message in the folder using + * <code>getMessage(int)</code> and checks whether its + * <code>DELETED</code> flag is set. The total number of messages + * that have this flag set is returned. + * + * @return number of deleted messages. -1 may be returned + * by certain implementations if this method is + * invoked on a closed folder. + * @exception FolderNotFoundException if this folder does + * not exist. + * @exception MessagingException + * @since JavaMail 1.3 + */ + public int getDeletedMessageCount() throws MessagingException + +=================================================================== + +5. Support parsing "illegal" Internet addresses (4650940) +--------------------------------------------------------- + +The parse method on the InternetAddress class takes a flag that tells +whether or not to strictly enforce the RFC822 syntax rules. Currently, +when the flag is false most rules are still checked while a few are not. +To better support the range of "invalid" addresses seen in real messages, +and in combination with the following two changes, the parseHeader +method would enforce fewer syntax rules when the strict flag is false +and would enforce more rules when the strict flag is true. If the +strict flag is false and the parse is successful in separating out an +email address or addresses, the syntax of the addresses themselves +would not be checked. (Introducing a new method preserves +compatibility with users of the existing parse method.) + + /** + * Parse the given sequence of addresses into InternetAddress + * objects. If <code>strict</code> is false, the full syntax rules for + * individual addresses are not enforced. If <code>strict</code> is + * true, many (but not all) of the RFC822 syntax rules are enforced. + * + * Non-strict parsing is typically used when parsing a list of + * mail addresses entered by a human. Strict parsing is typically + * used when parsing address headers in mail messages. + * + * @param addresslist comma separated address strings + * @param strict enforce RFC822 syntax + * @return array of InternetAddress objects + * @exception AddressException if the parse failed + * @since JavaMail 1.3 + */ + public static InternetAddress[] parseHeader(String s, boolean strict) + throws AddressException + + +To allow applications to check the syntax of addresses that might've +been parsed with the strict flag set to false, we add a validate +method. + + /** + * Validate that this address conforms to the syntax rules + * of RFC 822. The current implementation checks many, not + * all, syntax rules. Note that, even though the syntax of + * the address may be correct, there's no guarantee that a + * mailbox of that name exists. + * + * @exception AddressException if the address + * isn't valid. + * @since JavaMail 1.3 + */ + public void validate() throws AddressException + + +To control the strict flag when constructing a single InternetAddress +object we add a new constructor. + + /** + * Parse the given string and create an InternetAddress. + * If <code>strict</code> is false, the detailed syntax of the + * address isn't checked. + * + * @param address the address in RFC822 format + * @param strict enforce RFC822 syntax + * @exception AddressException if the parse failed + * @since JavaMail 1.3 + */ + public InternetAddress(String address, boolean strict) + throws AddressException + +=================================================================== + +6. Add mail.mime.address.strict property (4650940) +-------------------------------------------------- + +The MimeMessage class will use the new parseHeader method introduced +above to parse headers in messages. The "mail.mime.address.strict" +Session property will control the strict flag passed to the parseHeader +method. The default is true. + +=================================================================== + +7. Add mail.mime.decodetext.strict property (4201203) +----------------------------------------------------- + +RFC 2047 requires that encoded text start at the beginning of a +whitespace separated word. Some mailers, especially Japanese mailers, +improperly encode text and included encoded text in the middle of words. +The "mail.mime.decodetext.strict" System property (NOTE: *not* Session +property) controls whether JavaMail will attempt to decode such +incorrectly encoded text. The default is true. + +=================================================================== + +8. Add mail.mime.encodeeol.strict property (4650949) +---------------------------------------------------- + +When choosing an encoding for the data of a message, JavaMail assumes +that any of CR, LF, or CRLF are valid line terminators in message +parts that contain only printable ASCII characters, even if the part is +not a MIME text type. It's common, especially on UNIX systems, for +data of MIME type application/octet-stream (for example) to really be +textual data that should be transmitted with the encoding rules for +MIME text. In rare cases, such pure ASCII text may in fact be binary +data in which the CR and LF characters must be preserved exactly. The +"mail.mime.encodeeol.strict" System property (NOTE: *not* Session +property) controls whether JavaMail will consider a lone CR or LF in a +body part that's not a MIME text type to indicate that the body part +needs to be encoded. + +=================================================================== + +9. Add isGroup and getGroup methods to InternetAddress (4650952) +---------------------------------------------------------------- + +To better support RFC822 group addresses, the following methods +would be added. + + /** + * Indicates whether this address is an RFC 822 group address. + * Note that a group address is different than the mailing + * list addresses supported by most mail servers. Group addresses + * are rarely used; see RFC 822 for details. + * + * @return true if this address represents a group + * @since JavaMail 1.3 + */ + public boolean isGroup() + + /** + * Return the members of a group address. A group may have zero, + * one, or more members. If this address is not a group, null + * is returned. The <code>strict</code> parameter controls whether + * the group list is parsed using strict RFC 822 rules or not. + * The parsing is done using the <code>parseHeader</code> method. + * + * @return array of InternetAddress objects, or null + * @exception AddressException if the group list can't be parsed + * @since JavaMail 1.3 + */ + public InternetAddress[] getGroup(boolean strict) throws AddressException + + +=================================================================== + +10. Support per-session debug output stream (4517686) +----------------------------------------------------- + +To allow the debugging output for a session to be redirected, we add +the following methods to Session. + + /** + * Set the stream to be used for debugging output for this session. + * If <code>out</code> is null, <code>System.out</code> will be used. + * Note that debugging output that occurs before any session is created, + * as a result of setting the <code>mail.debug</code> system property, + * will always be sent to <code>System.out</code>. + * + * @param out the PrintStream to use for debugging output + * @since JavaMail 1.3 + */ + public void setDebugOut(PrintStream out) + + /** + * Returns the stream to be used for debugging output. If no stream + * has been set, <code>System.out</code> is returned. + * + * @return the PrintStream to use for debugging output + * @since JavaMail 1.3 + */ + public PrintStream getDebugOut()
diff --git a/doc/spec/JavaMail-1.4-changes.txt b/doc/spec/JavaMail-1.4-changes.txt new file mode 100644 index 0000000..d030996 --- /dev/null +++ b/doc/spec/JavaMail-1.4-changes.txt
@@ -0,0 +1,1061 @@ + + JavaMail 1.4 + ============ + + (Updated August 15, 2005) + +Following is a description of the new APIs that are being +introduced in JavaMail 1.4. The numbers in parentheses +are bug numbers; you can find more information about the +bug reports at: + + http://bugs.sun.com/bugdatabase/index.jsp + +Please send comments and feedback to javamail@sun.com. + +Many of these changes expand JavaMail's conformance with Internet +standards, or make JavaMail more tolerant of messages that don't +quite conform to the standards. "Be liberal in what you receive +and conservative in what you send." + +JavaMail 1.4 will also require at least J2SE 1.4. This allows +JavaMail to take advantage of features of more modern J2SE releases. + + +=================================================================== + +1. Add MimePart.setText(text, charset, subtype) method (6300765) +---------------------------------------------------------------- + +The setText method is a convenience method used to set the content +for a text/plain part. With the increased use of HTML and XML in +mail messages, it would be useful to have a convenience method to +set content of those types as well. To support this usage we add +a new method to the MimePart interface: + + /** + * Convenience method that sets the given String as this part's + * content, with a primary MIME type of "text" and the specified + * MIME subtype. The given Unicode string will be charset-encoded + * using the specified charset. The charset is also used to set + * the "charset" parameter. + * + * @param text the text content to set + * @param charset the charset to use for the text + * @param subtype the MIME subtype to use (e.g., "html") + * @exception MessagingException if an error occurs + * @since JavaMail 1.4 + */ + public void setText(String text, String charset, String subtype) + throws MessagingException; + +The MimeMessage and MimeBodyPart classes, which implement the +MimePart interface, will be updated to provide implementations +of the new method. + + +=================================================================== + +2. Add mail.mime.encodefilename and decodefilename properties (6300768) +----------------------------------------------------------------------- + +According to the MIME spec (RFC 2047), filenames included in the +filename parameter of the Content-Disposition header may not +include MIME "encoded-words", and thus may contain only US-ASCII +characters. However, many mailers violate this spec requirement +and use standard MIME encoding techniques to store non-ASCII +filenames in this filename parameter. + +If the mail.mime.encodefilename System property is set to "true". +the MimeMessage and MimeBodyPart setFileName methods will use the +MimeUtility.encodeText method to encode the filename. + +If the mail.mime.decodefilename System property is set to "true". +the MimeMessage and MimeBodyPart getFileName methods will use the +MimeUtility.decodeText method to decode the filename. + +Both of these properties default to "false". + +The following text is added to the MimeMessage and MimeBodyPart +setFileName methods: + + * If the <code>mail.mime.encodefilename</code> System property + * is set to true, the {@link MimeUtility#encodeText + * MimeUtility.encodeText method will be used to encode the + * filename. While such encoding is not supported by the MIME + * spec, many mailers use this technique to support non-ASCII + * characters in filenames. The default value of this property + * is false. + +The following text is added to the MimeMessage and MimeBodyPart +getFileName methods: + + * If the <code>mail.mime.encodefilename</code> System property + * is set to true, the {@link MimeUtility#decodeText + * MimeUtility.decodeText method will be used to decode the + * filename. While such encoding is not supported by the MIME + * spec, many mailers use this technique to support non-ASCII + * characters in filenames. The default value of this property + * is false. + + +=================================================================== + +3. Add Service.connect(user, password) (6300771) +------------------------------------------------ + +This convenience method uses the host already known to the Service +(Transport or Store). Equivalent to connect(null, user, password). + + /** + * Connect to the current host using the specified username + * and password. This method is equivalent to calling the + * <code>connect(host, user, password)</code> method with null + * for the host name. + * + * @param user the user name + * @param password this user's password + * @exception AuthenticationFailedException for authentication failures + * @exception MessagingException for other failures + * @exception IllegalStateException if the service is already connected + * @see javax.mail.event.ConnectionEvent + * @see javax.mail.Session#setPasswordAuthentication + * @see #connect(java.lang.String, java.lang.String, java.lang.String) + * @since JavaMail 1.4 + */ + public void connect(String user, String password) throws MessagingException + + +=================================================================== + +4. Add mail.mime.multipart.ignoremissingendboundary System property (4971381) +----------------------------------------------------------------------------- + +The current implementation of the MimeMultipart class will +ignore a missing end boundary line; if EOF is reached when +parsing the content before seeing an end boundary line, the +last part of the multipart is terminated and no error is +returned. + +Some users have requested a way to force the multipart +parsing to more strictly enforce the MIME specification. +To support this we we introduce a new System property: + + mail.mime.multipart.ignoremissingendboundary + +If this property is set to "false" MimeMultipart will throw a +MessagingException when parsing a multipart that does not +include the proper end boundary line. + +This property is already supported as part of the JavaMail +implementation. This change makes the property a part of the +standard API. + + * The <code>mail.mime.multipart.ignoremissingendboundary</code> + * System property may be set to <code>false</code> to cause a + * <code>MessagingException</code> to be thrown if the multipart + * data does not end with the required end boundary line. If this + * property is set to <code>true</code> or not set, missing end + * boundaries are not considered an error and the final body part + * ends at the end of the data. <p> + + +=================================================================== + +5. Add MimeMultipart.isComplete() method (6300811) +-------------------------------------------------- + +As described above, parsing of a MIME multipart may terminate +without an error, even though no final boundary line was seen. +This method will return true if the final boundary line was +seen. This will allow applications to successfully parse +mal-formed messages, while also being able to tell that they +were mal-formed. + + /** + * Return true if the final boundary line for this + * multipart was seen. When parsing multipart content, + * this class will (by default) terminate parsing with + * no error if the end of input is reached before seeing + * the final multipart boundary line. In such a case, + * this method will return false. (If the System property + * "mail.mime.multipart.ignoremissingendboundary" is set to + * false, parsing such a message will instead throw a + * MessagingException.) + * + * @return true if the final boundary line was seen + * @since JavaMail 1.4 + */ + public boolean isComplete() throws MessagingException + + +=================================================================== + +6. Add mail.mime.multipart.ignoremissingboundaryparameter property (6300814) +---------------------------------------------------------------------------- + +The following property is already supported as part of the JavaMail +implementation. This change makes the property a part of the +standard API. + + * The <code>mail.mime.multipart.ignoremissingboundaryparameter</code> + * System property may be set to <code>false</code> to cause a + * <code>MessagingException</code> to be thrown if the Content-Type + * of the MimeMultipart does not include a <code>boundary</code> parameter. + * If this property is set to <code>true</code> or not set, the multipart + * parsing code will look for a line that looks like a bounary line and + * use that as the boundary separating the parts. + + +=================================================================== + +7. Add MimeMultipart getPreamble and setPreamble methods (6300828) +------------------------------------------------------------------ + +In a MIME multipart message, it's possible to include text between +the headers and the first boundary line. This text is called the +preamble. It may include instructions for users of non-MIME +compliant software. The getPreamble method allows access to this +text when available. (Note that IMAP servers provide no convenient +access to this text.) The setPreamble method allows an application +to set the preamble for a message being constructed. + + /** + * Get the preamble text, if any, that appears before the + * first body part of this multipart. Some protocols, + * such as IMAP, will not allow access to the preamble text. + * + * @return the preamble text, or null if no preamble + * @since JavaMail 1.4 + */ + public String getPreamble() throws MessagingException + + /** + * Set the preamble text to be included before the first + * body part. Applications should generally not include + * any preamble text. In some cases it may be helpful to + * include preamble text with instructions for users of + * pre-MIME software. + * + * @param preamble the preamble text + * @since JavaMail 1.4 + */ + public void setPreamble(String preamble) throws MessagingException + + +=================================================================== + +8. Add MimeMessage.updateMessageID() protected method (6300831) +--------------------------------------------------------------- + +Some applications want more control over the data that's used +to create the Message-ID for a message. This method allows +an application to provide a simple subclass of MimeMessage +that overrides the Message-ID algorithm. + + /** + * Update the Message-ID header. This method is called + * by the <code>updateHeaders</code> and allows a subclass + * to override only the algorithm for choosing a Message-ID. + * + * @since JavaMail 1.4 + */ + protected void updateMessageID() throws MessagingException + + +=================================================================== + +9. Add MimeMessage.createMimeMessage() protected method (6300833) +----------------------------------------------------------------- + +The MimeMessage.reply method creates and returns a new MimeMessage. +Subclasses of MimeMessage may need the reply method to create a new +message of the appropriate subclass. This method allows subclasses +to control the class created in this case. + + /** + * Create and return a MimeMessage object. The reply method + * uses this method to create the MimeMessage object that it + * will return. Subclasses can override this method to return + * a subclass of MimeMessage. This implementation simply constructs + * and returns a MimeMessage object using the supplied Session. + * + * @param session the Session to use for the new message + * @return the new MimeMessage object + * @since JavaMail 1.4 + */ + protected MimeMessage createMimeMessage(Session session) + throws MessagingException + + +=================================================================== + +10. Make the "part" field of MimePartDataSource protected (6300834) +------------------------------------------------------------------- + +Subclasses of MimePartDataSource may need access to the "part" +field in order to implement the getInputStream method. The +part field is currently private, this change will make it protected. + + /** + * The MimePart that provides the data for this DataSource. + * + * @since JavaMail 1.4 + */ + protected MimePart part; + + +=================================================================== + +11. Folder.getSeparator should not require the folder to exist (6301381) +------------------------------------------------------------------------ + +IMAP folders are able to determine the separator character without +knowing whether the folder exists. Checking whether the folder +exists in order to throw FolderNotFoundException introduces additional +overhead. Because other methods often need to know the separator +character, this overhead can be noticable. The specification of this +method is changed as follows: + + /** + * Return the delimiter character that separates this Folder's pathname + * from the names of immediate subfolders. This method can be invoked + * on a closed Folder. + * + * @exception FolderNotFoundException if the implementation + * requires the folder to exist, but it does not + * @return Hierarchy separator character + */ + public abstract char getSeparator() throws MessagingException; + + +=================================================================== + +12. Add PreencodedMimeBodyPart class (6301386) +---------------------------------------------- + +In some cases an application will have data that has already +been encoded using (for example) base64 encoding. There should +be an easy way to attach such data to a message without the need +to decode it and reencode it. This class provides such support. + +/** + * A MimeBodyPart that handles data that has already been encoded. + * This class is useful when constructing a message and attaching + * data that has already been encoded (for example, using base64 + * encoding). The data may have been encoded by the application, + * or may have been stored in a file or database in encoded form. + * The encoding is supplied when this object is created. The data + * is attached to this object in the usual fashion, by using the + * <code>setText</code>, <code>setContent</code>, or + * <code>setDataHandler</code> methods. + * + * @since JavaMail 1.4 + */ + +public class PreencodedMimeBodyPart extends MimeBodyPart { + /** + * Create a PreencodedMimeBodyPart that assumes the data is + * encoded using the specified encoding. The encoding must + * be a MIME supported Content-Transfer-Encoding. + */ + public PreencodedMimeBodyPart(String encoding) +} + + +=================================================================== + +13. Add MimeBodyPart attachFile and saveFile methods (6301390) +-------------------------------------------------------------- + +It's very common for applications to create messages with files +as attachments, and to receive attachments and save them in files. +To simplify this usable, we add several convenience methods to the +MimeBodyPart class: + + /** + * Use the specified file to provide the data for this part. + * The simple file name is used as the file name for this + * part and the data in the file is used as the data for this + * part. The encoding will be chosen appropriately for the + * file data. + * + * @param file the File object to attach + * @exception IOException errors related to accessing the file + * @exception MessagingException message related errors + * @since JavaMail 1.4 + */ + public void attachFile(File file) throws IOException, MessagingException + + /** + * Use the specified file to provide the data for this part. + * The simple file name is used as the file name for this + * part and the data in the file is used as the data for this + * part. The encoding will be chosen appropriately for the + * file data. + * + * @param file the name of the file to attach + * @exception IOException errors related to accessing the file + * @exception MessagingException message related errors + * @since JavaMail 1.4 + */ + public void attachFile(String file) throws IOException, MessagingException + + /** + * Save the contents of this part in the specified file. The content + * is decoded and saved, without any of the MIME headers. + * + * @param file the File object to write to + * @exception IOException errors related to accessing the file + * @exception MessagingException message related errors + * @since JavaMail 1.4 + */ + public void saveFile(File file) throws IOException, MessagingException + + /** + * Save the contents of this part in the specified file. The content + * is decoded and saved, without any of the MIME headers. + * + * @param file the name of the file to write to + * @exception IOException errors related to accessing the file + * @exception MessagingException message related errors + * @since JavaMail 1.4 + */ + public void saveFile(String file) throws IOException, MessagingException + + +=================================================================== + +14. Add MimeUtility fold and unfold methods (6302118) +-------------------------------------------------------------- + +When dealing with long header lines, it's often necessary to fold +the lines to avoid exceeding line length limitations. When retrieving +the data from such headers, the folding needs to be undone. The JavaMail +implementation includes private fold and unfold methods for this purpose. +These methods should be made public. + + /** + * Fold a string at linear whitespace so that each line is no longer + * than 76 characters, if possible. If there are more than 76 + * non-whitespace characters consecutively, the string is folded at + * the first whitespace after that sequence. The parameter + * <code>used</code> indicates how many characters have been used in + * the current line; it is usually the length of the header name. <p> + * + * Note that line breaks in the string aren't escaped; they probably + * should be. + * + * @param used characters used in line so far + * @param s the string to fold + * @return the folded string + */ + public static String fold(int used, String s) + + /** + * Unfold a folded header. Any line breaks that aren't escaped and + * are followed by whitespace are removed. + * + * @param s the string to unfold + * @return the unfolded string + */ + public static String unfold(String s) + + +=================================================================== + +15. Allow more control over headers in InternetHeaders object (6302832) +----------------------------------------------------------------------- + +Some applications, such as mail server applications, need more control +over the order of headers in the InternetHeaders class. To support +such usage, we allow such applications to subclass InternetHeaders +and access the List of headers directly. InternetHeaders exposes a +protected field: + + protected List headers; + +The elements of the list are objects of a new protected final class +InternetHeaders.InternetHeader that extends the javax.mail.Header class. +To allow the InternetHeader class to make use of the Header class, we +make the following fields of Header protected: + + /** + * The name of the header. + * + * @since JavaMail 1.4 + */ + protected String name; + + /** + * The value of the header. + * + * @since JavaMail 1.4 + */ + protected String value; + + +=================================================================== + +16. Allow applications to dynamically register new protocol providers (6302835) +----------------------------------------------------------------------- + +Some applications would like to register new protocol providers at runtime +rather than depending on the JavaMail configuration files and resources. +To support such usage we make the constructor for the Provider class +public: + + /** + * Create a new provider of the specified type for the specified + * protocol. The specified class implements the provider. + * + * @param type Type.STORE or Type.TRANSPORT + * @param protocol valid protocol for the type + * @param classname class name that implements this protocol + * @param vendor optional string identifying the vendor (may be null) + * @param version optional implementation version string (may be null) + * @since JavaMail 1.4 + */ + public Provider(Type type, String protocol, String classname, + String vendor, String version) + +We also add a new method to Session to allow registering such Providers: + + /** + * Add a provider to the session. + * + * @param provider the provider to add + * @since JavaMail 1.4 + */ + public void addProvider(Provider provider) + + +=================================================================== + +17. Allow applications to dynamically register address type mappings (4377727) +------------------------------------------------------------------------------ + +Along with the above item, some applications will want to dynamically +control the mapping from address type to protocol. This could also +be used to change the default internet protocol from "smtp" to "smtps". +We add the following method to Session: + + /** + * Set the default transport protocol to use for addresses of + * the specified type. Normally the default is set by the + * <code>javamail.default.address.map</code> or + * <code>javamail.address.map</code> files or resources. + * + * @param addresstype type of address + * @param protocol name of protocol + * @see #getTransport(Address) + * @since JavaMail 1.4 + */ + public void setProtocolForAddress(String addresstype, String protocol) + + +=================================================================== + +18. ParameterList class should support non US-ASCII parameters (4107342) +------------------------------------------------------------------------ + +RFC 2231 describes a method for encoding non-ASCII parameters in MIME +headers. We introduce the following System properties to control +encoding and decoding such parameters. + +If the mail.mime.encodeparameters System property is set to "true". +non-ASCII parameters will be encoded per RFC 2231. + +If the mail.mime.decodeparameters System property is set to "true". +parameters encoded per RFC 2231 will be decoded. + +Both of these properties default to "false". + +Note that RFC 2231 also describes a technique for splitting long +parameter values across multiple parameters. We do not plan to +support such parameter continuations. + +To allow specifying the charset to use for a parameter, we add +the following method to ParameterList: + + /** + * Set a parameter. If this parameter already exists, it is + * replaced by this new value. If the + * <code>mail.mime.encodeparameters</code> System property + * is true, and the parameter value is non-ASCII, it will be + * encoded with the specified charset. + * + * @param name name of the parameter. + * @param value value of the parameter. + * @param charset charset of the parameter value. + * @since JavaMail 1.4 + */ + public void set(String name, String value, String charset) + + +=================================================================== + +19. Standard interface for Stores that support quotas (6304051) +--------------------------------------------------------------- + +Some IMAP stores support quotas. To allow applications to make +use of quota support without depending on IMAP-specific APIs, +we provide a QuotaAwareStore interface that Stores, such as the +IMAP Store, can implement. We also provide a Quota class to +represent a set of quotas for a quota root. + +package javax.mail; + +/** + * An interrface implemented by Stores that support quotas. + * The {@link #getQuota getQuota} and {@link #setQuota setQuota} methods + * support the quota model defined by the IMAP QUOTA extension. + * Refer to <A HREF="http://www.ietf.org/rfc/rfc2087.txt">RFC 2087</A> + * for more information. <p> + * + * @since JavaMail 1.4 + */ +public interface QuotaAwareStore { + /** + * Get the quotas for the named quota root. + * Quotas are controlled on the basis of a quota root, not + * (necessarily) a folder. The relationship between folders + * and quota roots depends on the server. Some servers + * might implement a single quota root for all folders owned by + * a user. Other servers might implement a separate quota root + * for each folder. A single folder can even have multiple + * quota roots, perhaps controlling quotas for different + * resources. + * + * @param root the name of the quota root + * @return array of Quota objects + * @exception MessagingException if the server doesn't support the + * QUOTA extension + */ + Quota[] getQuota(String root) throws MessagingException; + + /** + * Set the quotas for the quota root specified in the quota argument. + * Typically this will be one of the quota roots obtained from the + * <code>getQuota</code> method, but it need not be. + * + * @param quota the quota to set + * @exception MessagingException if the server doesn't support the + * QUOTA extension + */ + void setQuota(Quota quota) throws MessagingException; +} + +package javax.mail; + +/** + * This class represents a set of quotas for a given quota root. + * Each quota root has a set of resources, represented by the + * <code>Quota.Resource</code> class. Each resource has a name + * (for example, "STORAGE"), a current usage, and a usage limit. + * See RFC 2087. + * + * @since JavaMail 1.4 + */ + +public class Quota { + + /** + * An individual resource in a quota root. + * + * @since JavaMail 1.4 + */ + public static class Resource { + /** The name of the resource. */ + public String name; + /** The current usage of the resource. */ + public long usage; + /** The usage limit for the resource. */ + public long limit; + + /** + * Construct a Resource object with the given name, + * usage, and limit. + * + * @param name the resource name + * @param usage the current usage of the resource + * @param limit the usage limit for the resource + */ + public Resource(String name, long usage, long limit) + } + + /** + * The name of the quota root. + */ + public String quotaRoot; + + /** + * The set of resources associated with this quota root. + */ + public Quota.Resource[] resources; + + /** + * Create a Quota object for the named quotaroot with no associated + * resources. + * + * @param quotaRoot the name of the quota root + */ + public Quota(String quotaRoot) + + /** + * Set a resource limit for this quota root. + * + * @param name the name of the resource + * @param limit the resource limit + */ + public void setResourceLimit(String name, long limit) +} + +=================================================================== + +20. Add ByteArrayDataSource class (4623517) +------------------------------------------- + +The ByteArrayDataSource has been included in the JavaMail demo +source code for quite some time. Quite a few applications need +a class of this sort. It's time to add it as a standard API. +To avoid conflicting with applications that have used the demo +version, we put this version in a new javax.mail.util package. + +package javax.mail.util; + +/** + * A DataSource backed by a byte array. The byte array may be + * passed in directly, or may be initialized from an InputStream + * or a String. + * + * @since JavaMail 1.4 + */ +public class ByteArrayDataSource implements DataSource { + /** + * Create a ByteArrayDataSource with data from the + * specified byte array and with the specified MIME type. + */ + public ByteArrayDataSource(byte[] data, String type) + + /** + * Create a ByteArrayDataSource with data from the + * specified InputStream and with the specified MIME type. + * The InputStream is read completely and the data is + * stored in a byte array. + */ + public ByteArrayDataSource(InputStream is, String type) throws IOException + + /** + * Create a ByteArrayDataSource with data from the + * specified String and with the specified MIME type. + * The MIME type should include a <code>charset</code> + * parameter specifying the charset to be used for the + * string. If the parameter is not included, the + * default charset is used. + */ + public ByteArrayDataSource(String data, String type) throws IOException + + /** + * Return an InputStream for the data. + * Note that a new stream is returned each time + * this method is called. + */ + public InputStream getInputStream() throws IOException + + /** + * Return an OutputStream for the data. + * Writing the data is not supported; an <code>IOException</code> + * is always thrown. + */ + public OutputStream getOutputStream() throws IOException + + /** + * Get the MIME content type of the data. + */ + public String getContentType() + + /** + * Get the name of the data. + * By default, an empty string ("") is returned. + */ + public String getName() + + /** + * Set the name of the data. + */ + public void setName(String name) +} + + +=================================================================== + +21. Add SharedByteArrayInputStream class (6304189) +-------------------------------------------------- + +The SharedInputStream interface allows the JavaMail implementation to +efficiently process data when parsing messages, without needing to +make many copies of the data. This class is an implementation of the +SharedInputStream interface that uses a byte array as the backing store. + +package javax.mail.util; + +/** + * A ByteArrayInputStream that implements the SharedInputStream interface, + * allowing the underlying byte array to be shared between multiple readers. + * + * @since JavaMail 1.4 + */ +public class SharedByteArrayInputStream extends ByteArrayInputStream + implements SharedInputStream { + /** + * Position within shared buffer that this stream starts at. + */ + protected int start; + + /** + * Create a SharedByteArrayInputStream representing the entire + * byte array. + */ + public SharedByteArrayInputStream(byte[] buf) + + /** + * Create a SharedByteArrayInputStream representing the part + * of the byte array from <code>offset</code> for <code>length</code> + * bytes. + */ + public SharedByteArrayInputStream(byte[] buf, int offset, int length) + + /** + * Return the current position in the InputStream, as an + * offset from the beginning of the InputStream. + * + * @return the current position + */ + public long getPosition() + + /** + * Return a new InputStream representing a subset of the data + * from this InputStream, starting at <code>start</code> (inclusive) + * up to <code>end</code> (exclusive). <code>start</code> must be + * non-negative. If <code>end</code> is -1, the new stream ends + * at the same place as this stream. The returned InputStream + * will also implement the SharedInputStream interface. + * + * @param start the starting position + * @param end the ending position + 1 + * @return the new stream + */ + public InputStream newStream(long start, long end) +} + + +=================================================================== + +22. Add SharedFileInputStream class (6304193) +--------------------------------------------- + +Finally, SharedFileInputStream is an implementation of the +SharedInputStream interface that uses a file as the backing store. + +package javax.mail.util; + +/** + * A <code>SharedFileInputStream</code> is a + * <code>BufferedInputStream</code> that buffers + * data from the file and supports the <code>mark</code> + * and <code>reset</code> methods. It also supports the + * <code>newStream</code> method that allows you to create + * other streams that represent subsets of the file. + * A <code>RandomAccessFile</code> object is used to + * access the file data. + * + * @since JavaMail 1.4 + */ +public class SharedFileInputStream extends BufferedInputStream + implements SharedInputStream { + + /** + * The file containing the data. + * Shared by all related SharedFileInputStream instances. + */ + protected RandomAccessFile in; + + /** + * The normal size of the read buffer. + */ + protected int bufsize; + + /** + * The file offset that corresponds to the first byte in + * the read buffer. + */ + protected long bufpos; + + /** + * The file offset of the start of data in this subset of the file. + */ + protected long start = 0; + + /** + * The amount of data in this subset of the file. + */ + protected long datalen; + + /** + * Creates a <code>SharedFileInputStream</code> + * for the file. + * + * @param file the file + */ + public SharedFileInputStream(File file) throws IOException + + /** + * Creates a <code>SharedFileInputStream</code> + * for the named file. + * + * @param file the file + */ + public SharedFileInputStream(String file) throws IOException + + /** + * Creates a <code>SharedFileInputStream</code> + * with the specified buffer size. + * + * @param file the file + * @param size the buffer size. + * @exception IllegalArgumentException if size <= 0. + */ + public SharedFileInputStream(File file, int size) throws IOException + + /** + * Creates a <code>SharedFileInputStream</code> + * with the specified buffer size. + * + * @param file the file + * @param size the buffer size. + * @exception IllegalArgumentException if size <= 0. + */ + public SharedFileInputStream(String file, int size) throws IOException + + /** + * See the general contract of the <code>read</code> + * method of <code>InputStream</code>. + * + * @return the next byte of data, or <code>-1</code> if the end of the + * stream is reached. + * @exception IOException if an I/O error occurs. + */ + public int read() throws IOException + + /** + * Reads bytes from this stream into the specified byte array, + * starting at the given offset. + * + * <p> This method implements the general contract of the corresponding + * <code>{@link java.io.InputStream#read(byte[], int, int) read}</code> + * method of the <code>{@link java.io.InputStream}</code> class. + * + * @param b destination buffer. + * @param off offset at which to start storing bytes. + * @param len maximum number of bytes to read. + * @return the number of bytes read, or <code>-1</code> if the end of + * the stream has been reached. + * @exception IOException if an I/O error occurs. + */ + public int read(byte b[], int off, int len) throws IOException + + /** + * See the general contract of the <code>skip</code> + * method of <code>InputStream</code>. + * + * @param n the number of bytes to be skipped. + * @return the actual number of bytes skipped. + * @exception IOException if an I/O error occurs. + */ + public long skip(long n) throws IOException + + /** + * Returns the number of bytes that can be read from this input + * stream without blocking. + * + * @return the number of bytes that can be read from this input + * stream without blocking. + * @exception IOException if an I/O error occurs. + */ + public int available() throws IOException + + /** + * See the general contract of the <code>mark</code> + * method of <code>InputStream</code>. + * + * @param readlimit the maximum limit of bytes that can be read before + * the mark position becomes invalid. + * @see #reset() + */ + public void mark(int readlimit) + + /** + * See the general contract of the <code>reset</code> + * method of <code>InputStream</code>. + * <p> + * If <code>markpos</code> is <code>-1</code> + * (no mark has been set or the mark has been + * invalidated), an <code>IOException</code> + * is thrown. Otherwise, <code>pos</code> is + * set equal to <code>markpos</code>. + * + * @exception IOException if this stream has not been marked or + * if the mark has been invalidated. + * @see #mark(int) + */ + public void reset() throws IOException + + /** + * Tests if this input stream supports the <code>mark</code> + * and <code>reset</code> methods. The <code>markSupported</code> + * method of <code>SharedFileInputStream</code> returns + * <code>true</code>. + * + * @return a <code>boolean</code> indicating if this stream type supports + * the <code>mark</code> and <code>reset</code> methods. + * @see java.io.InputStream#mark(int) + * @see java.io.InputStream#reset() + */ + public boolean markSupported() + + /** + * Closes this input stream and releases any system resources + * associated with the stream. + * + * @exception IOException if an I/O error occurs. + */ + public void close() throws IOException + + /** + * Return the current position in the InputStream, as an + * offset from the beginning of the InputStream. + * + * @return the current position + */ + public long getPosition() + + /** + * Return a new InputStream representing a subset of the data + * from this InputStream, starting at <code>start</code> (inclusive) + * up to <code>end</code> (exclusive). <code>start</code> must be + * non-negative. If <code>end</code> is -1, the new stream ends + * at the same place as this stream. The returned InputStream + * will also implement the SharedInputStream interface. + * + * @param start the starting position + * @param end the ending position + 1 + * @return the new stream + */ + public InputStream newStream(long start, long end) + + /** + * Force this stream to close. + */ + protected void finalize() throws Throwable +}
diff --git a/doc/spec/JavaMail-1.5-changes.txt b/doc/spec/JavaMail-1.5-changes.txt new file mode 100644 index 0000000..655400d --- /dev/null +++ b/doc/spec/JavaMail-1.5-changes.txt
@@ -0,0 +1,823 @@ + + JavaMail 1.5 + ============ + + (Updated February 7, 2013) + +Following is a description of the changes to the JavaMail +API introduced in JavaMail 1.5. The numbers in parentheses +are bug numbers; you can find more information about the +bug reports at: + + https://github.com/javaee/javamail/issues/<bug number> + +Please send comments and feedback to javamail_ww@oracle.com. + +JavaMail 1.5 will also require at least J2SE 5.0. This allows +JavaMail to take advantage of features of more modern Java SE releases. + + +=================================================================== + +1. Add FetchProfile.Item.SIZE (37) +---------------------------------- + +The FetchProfile.Item.SIZE item allows prefetching the size of +a message. Previously this was an IMAP-specific fetch item. + + /** + * SIZE is a fetch profile item that can be included in a + * <code>FetchProfile</code> during a fetch request to a Folder. + * This item indicates that the sizes of the messages in the specified + * range should be prefetched. <p> + * + * @since JavaMail 1.5 + */ + public static final Item SIZE; + + +=================================================================== + +2. Fix protected fields in final classes in javax.mail.search (38) +------------------------------------------------------------------ + +Several final classes in the javax.mail.search package contain +protected fields. Since the classes are final, they can't be +subclassed, and the protected fields can not be accessed. This +change cleans up these fields by making them private. The following +fields are changed: + +javax.mail.search.AndTerm: + private SearchTerm[] terms; + +javax.mail.search.FlagTerm: + private boolean set; + private Flags flags; + +javax.mail.search.HeaderTerm: + private String headerName; + +javax.mail.search.NotTerm: + private SearchTerm term; + +javax.mail.search.OrTerm: + private SearchTerm[] terms; + +javax.mail.search.RecipientTerm: + private Message.RecipientType type; + + +=================================================================== + +3. Add MimeMultipart(String subtype, BodyPart... bps) constructor (39) +---------------------------------------------------------------------- + +These convenience constructors create a MimeMultipart object given an +array or varargs list of BodyParts. + + /** + * Construct a MimeMultipart object of the default "mixed" subtype, + * and with the given body parts. More body parts may be added later. + * + * @since JavaMail 1.5 + */ + public MimeMultipart(BodyPart... parts) throws MessagingException + + /** + * Construct a MimeMultipart object of the given subtype + * and with the given body parts. More body parts may be added later. + * + * @since JavaMail 1.5 + */ + public MimeMultipart(String subtype, BodyPart... parts) + throws MessagingException + + +=================================================================== + +4. Exceptions should support exception chaining (40) +---------------------------------------------------- + +javax.mail.MessagingException was designed before exception chainging +was added to Java SE, but it does support a similar concept itself, +and that support should be made available to all subclasses. + +javax.mail.AuthenticationFailedException: + /** + * Constructs an AuthenticationFailedException with the specified + * detail message and embedded exception. The exception is chained + * to this exception. + * + * @param message The detailed error message + * @param e The embedded exception + * @since JavaMail 1.5 + */ + public AuthenticationFailedException(String message, Exception e) + +javax.mail.FolderClosedException: + /** + * Constructs a FolderClosedException with the specified + * detail message and embedded exception. The exception is chained + * to this exception. + * + * @param folder The Folder + * @param message The detailed error message + * @param e The embedded exception + * @since JavaMail 1.5 + */ + public FolderClosedException(Folder folder, String message, Exception e) + +javax.mail.FolderNotFoundException: + /** + * Constructs a FolderNotFoundException with the specified + * detail message and embedded exception. The exception is chained + * to this exception. + * + * @param folder The Folder + * @param s The detailed error message + * @param e The embedded exception + * @since JavaMail 1.5 + */ + public FolderNotFoundException(Folder folder, String s, Exception e) + +javax.mail.IllegalWriteException: + /** + * Constructs an IllegalWriteException with the specified + * detail message and embedded exception. The exception is chained + * to this exception. + * + * @param s The detailed error message + * @param e The embedded exception + * @since JavaMail 1.5 + */ + public IllegalWriteException(String s, Exception e) + +javax.mail.MessageRemovedException: + /** + * Constructs a MessageRemovedException with the specified + * detail message and embedded exception. The exception is chained + * to this exception. + * + * @param s The detailed error message + * @param e The embedded exception + * @since JavaMail 1.5 + */ + public MessageRemovedException(String s, Exception e) + +javax.mail.MethodNotSupportedException: + /** + * Constructs a MethodNotSupportedException with the specified + * detail message and embedded exception. The exception is chained + * to this exception. + * + * @param s The detailed error message + * @param e The embedded exception + * @since JavaMail 1.5 + */ + public MethodNotSupportedException(String s, Exception e) + +javax.mail.NoSuchProviderException: + /** + * Constructs a NoSuchProviderException with the specified + * detail message and embedded exception. The exception is chained + * to this exception. + * + * @param message The detailed error message + * @param e The embedded exception + * @since JavaMail 1.5 + */ + public NoSuchProviderException(String message, Exception e) + +javax.mail.ReadOnlyFolderException: + /** + * Constructs a ReadOnlyFolderException with the specified + * detail message and embedded exception. The exception is chained + * to this exception. + * + * @param folder The Folder + * @param message The detailed error message + * @param e The embedded exception + * @since JavaMail 1.5 + */ + public ReadOnlyFolderException(Folder folder, String message, Exception e) + +javax.mail.StoreClosedException: + /** + * Constructs a StoreClosedException with the specified + * detail message and embedded exception. The exception is chained + * to this exception. + * + * @param store The dead Store object + * @param message The detailed error message + * @param e The embedded exception + * @since JavaMail 1.5 + */ + public StoreClosedException(Store store, String message, Exception e) + + +=================================================================== + +5. ParameterList needs to support use by IMAP (41) +-------------------------------------------------- + +The IMAP provider has special needs when processing multi-segment +parameters defined by RFC 2231. This new method supports such use +by the IMAP provider. + + /** + * Normal users of this class will use simple parameter names. + * In some cases, for example, when processing IMAP protocol + * messages, individual segments of a multi-segment name + * (specified by RFC 2231) will be encountered and passed to + * the {@link #set} method. After all these segments are added + * to this ParameterList, they need to be combined to represent + * the logical parameter name and value. This method will combine + * all segments of multi-segment names. <p> + * + * Normal users should never need to call this method. + * + * @since JavaMail 1.5 + */ + public void combineSegments() + + +=================================================================== + +6. ContentType and ContentDisposition toString should never return null (42) +---------------------------------------------------------------------------- + +The general contract of Object.toString is that it never returns null. +The toString methods of ContentType and ContentDisposition were defined +to return null in certain error cases. Given the general toString contract +it seems unlikely that anyone ever depended on these special cases, and +it would be more useful for these classes to obey the general contract. +These methods have been changed to return an empty string in these error +cases. + +javax.mail.internet.ContentType: + /** + * Retrieve a RFC2045 style string representation of + * this Content-Type. Returns an empty string if + * the conversion failed. + * + * @return RFC2045 style string + */ + public String toString() + +javax.mail.internet.ContentDisposition: + /** + * Retrieve a RFC2045 style string representation of + * this ContentDisposition. Returns an empty string if + * the conversion failed. + * + * @return RFC2045 style string + * @since JavaMail 1.2 + */ + public String toString() + + +=================================================================== + +7. Add Transport.send(msg, username, password) method (44) +---------------------------------------------------------- + +It's now very common that email servers require authentication before +sending a message, so we add these new convenience methods. + + /** + * Send a message. The message will be sent to all recipient + * addresses specified in the message (as returned from the + * <code>Message</code> method <code>getAllRecipients</code>). + * The <code>send</code> method calls the <code>saveChanges</code> + * method on the message before sending it. <p> + * + * Use the specified user name and password to authenticate to + * the mail server. + * + * @param msg the message to send + * @param user the user name + * @param password this user's password + * @exception SendFailedException if the message could not + * be sent to some or any of the recipients. + * @exception MessagingException + * @see Message#saveChanges + * @see #send(Message) + * @see javax.mail.SendFailedException + * @since JavaMail 1.5 + */ + public static void send(Message msg, + String user, String password) throws MessagingException + + /** + * Send the message to the specified addresses, ignoring any + * recipients specified in the message itself. The + * <code>send</code> method calls the <code>saveChanges</code> + * method on the message before sending it. <p> + * + * Use the specified user name and password to authenticate to + * the mail server. + * + * @param msg the message to send + * @param addresses the addresses to which to send the message + * @param user the user name + * @param password this user's password + * @exception SendFailedException if the message could not + * be sent to some or any of the recipients. + * @exception MessagingException + * @see Message#saveChanges + * @see #send(Message) + * @see javax.mail.SendFailedException + * @since JavaMail 1.5 + */ + public static void send(Message msg, Address[] addresses, + String user, String password) throws MessagingException + + +=================================================================== + +8. Add MimeMessage.setFrom(String) method (45) +---------------------------------------------- + +This new convenience method allows the From header to be set using a String. + + /** + * Set the RFC 822 "From" header field. Any existing values are + * replaced with the given addresses. If address is <code>null</code>, + * this header is removed. + * + * @param address the sender(s) of this message + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException + * @since JvaMail 1.5 + */ + public void setFrom(String address) throws MessagingException + + +=================================================================== + +9. Add Message.getSesssion() method (46) +---------------------------------------- + +Alow access to the Session used when the Message was created. + + /** + * Return the Session used when this message was created. + * + * @return the message's Session + * @since JavaMail 1.5 + */ + public Session getSession() + + +=================================================================== + +10. MimeBodyPart.attachFile should set the disposition to ATTACHMENT (47) +------------------------------------------------------------------------- + +An oversight when these methods were originally added. Clearly attachments +should set the disposition to ATTACHMENT. + + /** + * Use the specified file to provide the data for this part. + * The simple file name is used as the file name for this + * part and the data in the file is used as the data for this + * part. The encoding will be chosen appropriately for the + * file data. The disposition of this part is set to + * {@link Part#ATTACHMENT Part.ATTACHMENT}. + * + * @param file the File object to attach + * @exception IOException errors related to accessing the file + * @exception MessagingException message related errors + * @since JavaMail 1.4 + */ + public void attachFile(File file) throws IOException, MessagingException + + /** + * Use the specified file to provide the data for this part. + * The simple file name is used as the file name for this + * part and the data in the file is used as the data for this + * part. The encoding will be chosen appropriately for the + * file data. + * + * @param file the name of the file to attach + * @exception IOException errors related to accessing the file + * @exception MessagingException message related errors + * @since JavaMail 1.4 + */ + public void attachFile(String file) throws IOException, MessagingException + + +=================================================================== + +11. Add MimeMessage.reply(replyToAll, setAnswered) method (48) +-------------------------------------------------------------- + +Add a method to control whether the ANSWERED flag is set in the original +message when creating a reply message. + + /** + * Get a new Message suitable for a reply to this message. + * The new Message will have its attributes and headers + * set up appropriately. Note that this new message object + * will be empty, i.e., it will <strong>not</strong> have a "content". + * These will have to be suitably filled in by the client. <p> + * + * If <code>replyToAll</code> is set, the new Message will be addressed + * to all recipients of this message. Otherwise, the reply will be + * addressed to only the sender of this message (using the value + * of the <code>getReplyTo</code> method). <p> + * + * If <code>setAnswered</code> is set, the + * {@link javax.mail.Flags.Flag#ANSWERED ANSWERED} flag is set + * in this message. <p> + * + * The "Subject" field is filled in with the original subject + * prefixed with "Re:" (unless it already starts with "Re:"). + * The "In-Reply-To" header is set in the new message if this + * message has a "Message-Id" header. + * + * The current implementation also sets the "References" header + * in the new message to include the contents of the "References" + * header (or, if missing, the "In-Reply-To" header) in this message, + * plus the contents of the "Message-Id" header of this message, + * as described in RFC 2822. + * + * @param replyToAll reply should be sent to all recipients + * of this message + * @param setAnswered set the ANSWERED flag in this message? + * @return the reply Message + * @exception MessagingException + * @since JavaMail 1.5 + */ + public Message reply(boolean replyToAll, boolean setAnswered) + throws MessagingException + + +=================================================================== + +12. Add additional "next" methods to HeaderTokenizer (49) +--------------------------------------------------------- + +These additional "next" methods make it easier to parse headers that don't +obey the MIME syntax requirements. + + /** + * Parses the next token from this String. + * If endOfAtom is not NUL, the token extends until the + * endOfAtom character is seen, or to the end of the header. + * This method is useful when parsing headers that don't + * obey the MIME specification, e.g., by failing to quote + * parameter values that contain spaces. + * + * @param endOfAtom if not NUL, character marking end of token + * @return the next Token + * @exception ParseException if the parse fails + * @since JavaMail 1.5 + */ + public Token next(char endOfAtom) throws ParseException + + /** + * Parses the next token from this String. + * endOfAtom is handled as above. If keepEscapes is true, + * any backslash escapes are preserved in the returned string. + * This method is useful when parsing headers that don't + * obey the MIME specification, e.g., by failing to escape + * backslashes in the filename parameter. + * + * @param endOfAtom if not NUL, character marking end of token + * @param keepEscapes keep all backslashes in returned string? + * @return the next Token + * @exception ParseException if the parse fails + * @since JavaMail 1.5 + */ + public Token next(char endOfAtom, boolean keepEscapes) + throws ParseException + + +=================================================================== + +13. Add @MailSessionDefinition and @MailSessionDefinitions for Java EE 7 (51) +----------------------------------------------------------------------------- + +These new annotations support configuring JavaMail Session resources in +Java EE 7 application servers. + +javax.mail.MailSessionDefinition: + +/** + * Annotation used by Java EE applications to define a <code>MailSession</code> + * to be registered with JNDI. The <code>MailSession</code> may be configured + * by setting the annotation elements for commonly used <code>Session</code> + * properties. Additional standard and vendor-specific properties may be + * specified using the <code>properties</code> element. + * <p/> + * The session will be registered under the name specified in the + * <code>name</code> element. It may be defined to be in any valid + * <code>Java EE</code> namespace, and will determine the accessibility of + * the session from other components. + * + * @since JavaMail 1.5 + */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface MailSessionDefinition { + + /** + * Description of this mail session. + */ + String description() default ""; + + /** + * JNDI name by which the mail session will be registered. + */ + String name(); + + /** + * Store protocol name. + */ + String storeProtocol() default ""; + + /** + * Transport protocol name. + */ + String transportProtocol() default ""; + + /** + * Host name for the mail server. + */ + String host() default ""; + + /** + * User name to use for authentication. + */ + String user() default ""; + + /** + * Password to use for authentication. + */ + String password() default ""; + + /** + * From address for the user. + */ + String from() default ""; + + /** + * Properties to include in the Session. + * Properties are specified using the format: + * <i>propertyName=propertyValue</i> with one property per array element. + */ + String[] properties() default {}; +} + + +javax.mail.MailSessionDefinitions: + +import java.lang.annotation.Target; +import java.lang.annotation.Retention; +import java.lang.annotation.ElementType; +import java.lang.annotation.RetentionPolicy; + +/** + * Declares one or more <code>MailSessionDefinition</code> annotations. + * + * @see MailSessionDefinition + * @since JavaMail 1.5 + */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface MailSessionDefinitions { + MailSessionDefinition[] value(); +} + + +=================================================================== + +14. Make cachedContent field protected in MimeMessage and MimeBodyPart (52) +--------------------------------------------------------------------------- + +Exposing this previously private field makes it easier to subclass these +classes. + + /** + * If our content is a Multipart or Message object, we save it + * the first time it's created by parsing a stream so that changes + * to the contained objects will not be lost. <p> + * + * If this field is not null, it's return by the {@link #getContent} + * method. The {@link #getContent} method sets this field if it + * would return a Multipart or MimeMessage object. This field is + * is cleared by the {@link #setDataHandler} method. + * + * @since JavaMail 1.5 + */ + protected Object cachedContent; + + +=================================================================== + +15. Make MimeMultipart fields protected to allow subclassing (53) +----------------------------------------------------------------- + +Most of these fields control how the MimeMultipart class parses messages +that don't conform to the MIME spec. The new initializeProperties method +initializes these fields based on System properties. Exposing these +previously private fields makes it easier to subclass MimeMultipart. + + /** + * Have we seen the final bounary line? + * + * @since JavaMail 1.5 + */ + protected boolean complete = true; + + /** + * The MIME multipart preamble text, the text that + * occurs before the first boundary line. + * + * @since JavaMail 1.5 + */ + protected String preamble = null; + + /** + * Flag corresponding to the "mail.mime.multipart.ignoremissingendboundary" + * property, set in the {@link #initializeProperties} method called from + * constructors and the parse method. + * + * @since JavaMail 1.5 + */ + protected boolean ignoreMissingEndBoundary = true; + + /** + * Flag corresponding to the + * "mail.mime.multipart.ignoremissingboundaryparameter" + * property, set in the {@link #initializeProperties} method called from + * constructors and the parse method. + * + * @since JavaMail 1.5 + */ + protected boolean ignoreMissingBoundaryParameter = true; + + /** + * Flag corresponding to the + * "mail.mime.multipart.ignoreexistingboundaryparameter" + * property, set in the {@link #initializeProperties} method called from + * constructors and the parse method. + * + * @since JavaMail 1.5 + */ + protected boolean ignoreExistingBoundaryParameter = false; + + /** + * Flag corresponding to the "mail.mime.multipart.allowempty" + * property, set in the {@link #initializeProperties} method called from + * constructors and the parse method. + * + * @since JavaMail 1.5 + */ + protected boolean allowEmpty = false; + + /** + * Initialize flags that control parsing behavior, + * based on System properties described above in + * the class documentation. + * + * @since JavaMail 1.5 + */ + protected void initializeProperties() + +The following additional System properties are defined corresponding to +the last two fields above: + +mail.mime.multipart.ignoreexistingboundaryparameter: + + Normally the boundary parameter in the Content-Type header of a multipart + body part is used to specify the separator between parts of the multipart + body. This System property may be set to <code>"true"</code> to cause + the parser to look for a line in the multipart body that looks like a + boundary line and use that value as the separator between subsequent parts. + This may be useful in cases where a broken anti-virus product has rewritten + the message incorrectly such that the boundary parameter and the actual + boundary value no longer match. + The default value of this property is false. + +mail.mime.multipart.allowempty: + + Normally, when writing out a MimeMultipart that contains no body + parts, or when trying to parse a multipart message with no body parts, + a <code>MessagingException</code> is thrown. The MIME spec does not allow + multipart content with no body parts. This + System property may be set to <code>"true"</code> to override this behavior. + When writing out such a MimeMultipart, a single empty part will be + included. When reading such a multipart, a MimeMultipart will be created + with no body parts. + The default value of this property is false. + + +=================================================================== + +16. Need simple way to override MIME type and encoding of attachment (55) +------------------------------------------------------------------------- + +First, we define an interface that allows a DataSource to specify the +Content-Transfer-Encoding to use: + +package javax.mail; + +/** + * A {@link javax.activation.DataSource DataSource} that also implements + * <code>EncodingAware</code> may specify the Content-Transfer-Encoding + * to use for its data. Valid Content-Transfer-Encoding values specified + * by RFC 2045 are "7bit", "8bit", "quoted-printable", "base64", and "binary". + * <p> + * For example, a {@link javax.activation.FileDataSource FileDataSource} + * could be created that forces all files to be base64 encoded: <p> + * <blockquote><pre> + * public class Base64FileDataSource extends FileDataSource + * implements EncodingAware { + * public Base64FileDataSource(File file) { + * super(file); + * } + * + * // implements EncodingAware.getEncoding() + * public String getEncoding() { + * return "base64"; + * } + * } + * </pre></blockquote><p> + * + * @since JavaMail 1.5 + * @author Bill Shannon + */ + +public interface EncodingAware { + + /** + * Return the MIME Content-Transfer-Encoding to use for this data, + * or null to indicate that an appropriate value should be chosen + * by the caller. + * + * @return the Content-Transfer-Encoding value, or null + */ + public String getEncoding(); +} + +Then we add new methods to MimeBodyPart: + + /** + * Use the specified file with the specified Content-Type and + * Content-Transfer-Encoding to provide the data for this part. + * If contentType or encoding are null, appropriate values will + * be chosen. + * The simple file name is used as the file name for this + * part and the data in the file is used as the data for this + * part. The disposition of this part is set to + * {@link Part#ATTACHMENT Part.ATTACHMENT}. + * + * @param file the File object to attach + * @param contentType the Content-Type, or null + * @param encoding the Content-Transfer-Encoding, or null + * @exception IOException errors related to accessing the file + * @exception MessagingException message related errors + * @since JavaMail 1.5 + */ + public void attachFile(File file, String contentType, String encoding) + throws IOException, MessagingException + + /** + * Use the specified file with the specified Content-Type and + * Content-Transfer-Encoding to provide the data for this part. + * If contentType or encoding are null, appropriate values will + * be chosen. + * The simple file name is used as the file name for this + * part and the data in the file is used as the data for this + * part. The disposition of this part is set to + * {@link Part#ATTACHMENT Part.ATTACHMENT}. + * + * @param file the name of the file + * @param contentType the Content-Type, or null + * @param encoding the Content-Transfer-Encoding, or null + * @exception IOException errors related to accessing the file + * @exception MessagingException message related errors + * @since JavaMail 1.5 + */ + public void attachFile(String file, String contentType, String encoding) + throws IOException, MessagingException + + +=================================================================== + +17. Enable RFC 2231 support by default (56) +------------------------------------------- + +RFC 2231 support for encoded parameter values is now widely implemented, +it's time to change the default to support this standard. Given the +way RFC 2231 is defined, it's extremely unlikely that this would cause +compatibility problems with existing applications. + +The System properties mail.mime.decodeparameters and mail.mime.encodeparameters +now default to true instead of false.
diff --git a/doc/spec/JavaMail-1.6-changes.txt b/doc/spec/JavaMail-1.6-changes.txt new file mode 100644 index 0000000..a6906b6 --- /dev/null +++ b/doc/spec/JavaMail-1.6-changes.txt
@@ -0,0 +1,677 @@ + + JavaMail 1.6 + ============ + + (Updated July 17, 2017) + +Following is a description of the changes to the JavaMail +API introduced in JavaMail 1.6. The numbers in parentheses +are bug numbers; you can find more information about the +bug reports at: + + https://github.com/javaee/javamail/issues/<bug number> + +Please send comments and feedback to javamail_ww@oracle.com. + +JavaMail 1.6 will also require at least Java SE 7. + + +=================================================================== + +1. MailSessionDefinition should use Repeatable annotation for Java EE 8 (226) +----------------------------------------------------------------------------- + +The MailSessionDefinition annotation now includes the Repeatable annotation: + + @Target({ElementType.TYPE}) + @Retention(RetentionPolicy.RUNTIME) + @Repeatable(MailSessionDefinitions.class) + public @interface MailSessionDefinition { + ... + } + +The Repeatable annotation is not known to Java SE 7 and will be ignored +(as expected) when JavaMail 1.6 is used on Java SE 7. + + +=================================================================== + +2. MimeMessage.updateHeaders should set Date header if not already set (77) +--------------------------------------------------------------------------- + +RFC 2822 requires a Date header. The MimeMessage.updateHeaders method +now sets the Date header if it's not already set: + + /** + * Called by the <code>saveChanges</code> method to actually + * update the MIME headers. The implementation here sets the + * <code>Content-Transfer-Encoding</code> header (if needed + * and not already set), the <code>Date</code> header (if + * not already set), the <code>MIME-Version</code> header + * and the <code>Message-ID</code> header. Also, if the content + * of this message is a <code>MimeMultipart</code>, its + * <code>updateHeaders</code> method is called. <p> + * + * If the {@link #cachedContent} field is not null (that is, + * it references a Multipart or Message object), then + * that object is used to set a new DataHandler, any + * stream data used to create this object is discarded, + * and the {@link #cachedContent} field is cleared. + * + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + protected synchronized void updateHeaders() throws MessagingException { + ... + } + + +=================================================================== + +3. Update public API to use generics (232) +------------------------------------------ + +Methods on the following APIs have been updated to use generics +when appropriate: + + javax.mail.Multipart + javax.mail.Part + javax.mail.Service + javax.mail.internet.InternetHeaders + javax.mail.internet.MimeBodyPart + javax.mail.internet.MimeMessage + javax.mail.internet.MimePart + javax.mail.internet.ParameterList + +Details follow: + +diff -r 1f6b2c17e291 mail/src/main/java/javax/mail/Multipart.java +- a/mail/src/main/java/javax/mail/Multipart.java ++++ b/mail/src/main/java/javax/mail/Multipart.java +@@ -72,2 +72,1 @@ +- @SuppressWarnings("rawtypes") +- protected Vector parts = new Vector(); // Holds BodyParts ++ protected Vector<BodyPart> parts = new Vector<>(); // Holds BodyParts +diff -r 1f6b2c17e291 mail/src/main/java/javax/mail/Part.java +- a/mail/src/main/java/javax/mail/Part.java ++++ b/mail/src/main/java/javax/mail/Part.java +@@ -452,2 +452,1 @@ +- @SuppressWarnings("rawtypes") +- public Enumeration getAllHeaders() throws MessagingException; ++ public Enumeration<Header> getAllHeaders() throws MessagingException; +@@ -463,2 +462,1 @@ +- @SuppressWarnings("rawtypes") +- public Enumeration getMatchingHeaders(String[] header_names) ++ public Enumeration<Header> getMatchingHeaders(String[] header_names) +@@ -475,2 +473,1 @@ +- @SuppressWarnings("rawtypes") +- public Enumeration getNonMatchingHeaders(String[] header_names) ++ public Enumeration<Header> getNonMatchingHeaders(String[] header_names) +diff -r 1f6b2c17e291 mail/src/main/java/javax/mail/Service.java +- a/mail/src/main/java/javax/mail/Service.java ++++ b/mail/src/main/java/javax/mail/Service.java +@@ -640,2 +640,2 @@ +- @SuppressWarnings("rawtypes") +- protected void queueEvent(MailEvent event, Vector vector) { ++ protected void queueEvent(MailEvent event, ++ Vector<? extends EventListener> vector) { +diff -r 1f6b2c17e291 mail/src/main/java/javax/mail/internet/InternetHeaders.java +- a/mail/src/main/java/javax/mail/internet/InternetHeaders.java ++++ b/mail/src/main/java/javax/mail/internet/InternetHeaders.java +@@ -300,2 +300,1 @@ +- @SuppressWarnings("rawtypes") +- protected List headers; ++ protected List<InternetHeader> headers; +@@ -589,2 +585,1 @@ +- @SuppressWarnings({"rawtypes", "unchecked"}) +- public Enumeration getAllHeaders() { ++ public Enumeration<Header> getAllHeaders() { +@@ -600,2 +595,1 @@ +- @SuppressWarnings({"rawtypes", "unchecked"}) +- public Enumeration getMatchingHeaders(String[] names) { ++ public Enumeration<Header> getMatchingHeaders(String[] names) { +@@ -611,2 +605,1 @@ +- @SuppressWarnings({"rawtypes", "unchecked"}) +- public Enumeration getNonMatchingHeaders(String[] names) { ++ public Enumeration<Header> getNonMatchingHeaders(String[] names) { +@@ -649,2 +640,1 @@ +- @SuppressWarnings("rawtypes") +- public Enumeration getAllHeaderLines() { ++ public Enumeration<String> getAllHeaderLines() { +@@ -660,2 +650,1 @@ +- @SuppressWarnings({"rawtypes", "unchecked"}) +- public Enumeration getMatchingHeaderLines(String[] names) { ++ public Enumeration<String> getMatchingHeaderLines(String[] names) { +@@ -671,2 +660,1 @@ +- @SuppressWarnings({"rawtypes", "unchecked"}) +- public Enumeration getNonMatchingHeaderLines(String[] names) { ++ public Enumeration<String> getNonMatchingHeaderLines(String[] names) { +diff -r 1f6b2c17e291 mail/src/main/java/javax/mail/internet/MimeBodyPart.java +- a/mail/src/main/java/javax/mail/internet/MimeBodyPart.java ++++ b/mail/src/main/java/javax/mail/internet/MimeBodyPart.java +@@ -1036,2 +1036,1 @@ +- @SuppressWarnings("rawtypes") +- public Enumeration getAllHeaders() throws MessagingException { ++ public Enumeration<Header> getAllHeaders() throws MessagingException { +@@ -1045,2 +1044,1 @@ +- @SuppressWarnings("rawtypes") +- public Enumeration getMatchingHeaders(String[] names) ++ public Enumeration<Header> getMatchingHeaders(String[] names) +@@ -1055,2 +1053,1 @@ +- @SuppressWarnings("rawtypes") +- public Enumeration getNonMatchingHeaders(String[] names) ++ public Enumeration<Header> getNonMatchingHeaders(String[] names) +@@ -1073,2 +1070,1 @@ +- @SuppressWarnings("rawtypes") +- public Enumeration getAllHeaderLines() throws MessagingException { ++ public Enumeration<String> getAllHeaderLines() throws MessagingException { +@@ -1083,2 +1079,1 @@ +- @SuppressWarnings("rawtypes") +- public Enumeration getMatchingHeaderLines(String[] names) ++ public Enumeration<String> getMatchingHeaderLines(String[] names) +@@ -1094,2 +1089,1 @@ +- @SuppressWarnings("rawtypes") +- public Enumeration getNonMatchingHeaderLines(String[] names) ++ public Enumeration<String> getNonMatchingHeaderLines(String[] names) +diff -r 1f6b2c17e291 mail/src/main/java/javax/mail/internet/MimeMessage.java +- a/mail/src/main/java/javax/mail/internet/MimeMessage.java ++++ b/mail/src/main/java/javax/mail/internet/MimeMessage.java +@@ -1992,2 +1991,1 @@ +- @SuppressWarnings("rawtypes") +- public Enumeration getAllHeaders() throws MessagingException { ++ public Enumeration<Header> getAllHeaders() throws MessagingException { +@@ -2004,2 +2002,1 @@ +- @SuppressWarnings("rawtypes") +- public Enumeration getMatchingHeaders(String[] names) ++ public Enumeration<Header> getMatchingHeaders(String[] names) +@@ -2017,2 +2014,1 @@ +- @SuppressWarnings("rawtypes") +- public Enumeration getNonMatchingHeaders(String[] names) ++ public Enumeration<Header> getNonMatchingHeaders(String[] names) +@@ -2043,2 +2039,1 @@ +- @SuppressWarnings("rawtypes") +- public Enumeration getAllHeaderLines() throws MessagingException { ++ public Enumeration<String> getAllHeaderLines() throws MessagingException { +@@ -2055,2 +2050,1 @@ +- @SuppressWarnings("rawtypes") +- public Enumeration getMatchingHeaderLines(String[] names) ++ public Enumeration<String> getMatchingHeaderLines(String[] names) +@@ -2068,2 +2062,1 @@ +- @SuppressWarnings("rawtypes") +- public Enumeration getNonMatchingHeaderLines(String[] names) ++ public Enumeration<String> getNonMatchingHeaderLines(String[] names) +diff -r 1f6b2c17e291 mail/src/main/java/javax/mail/internet/MimePart.java +- a/mail/src/main/java/javax/mail/internet/MimePart.java ++++ b/mail/src/main/java/javax/mail/internet/MimePart.java +@@ -111,2 +111,1 @@ +- @SuppressWarnings("rawtypes") +- public Enumeration getAllHeaderLines() throws MessagingException; ++ public Enumeration<String> getAllHeaderLines() throws MessagingException; +@@ -123,2 +122,1 @@ +- @SuppressWarnings("rawtypes") +- public Enumeration getMatchingHeaderLines(String[] names) ++ public Enumeration<String> getMatchingHeaderLines(String[] names) +@@ -136,2 +134,1 @@ +- @SuppressWarnings("rawtypes") +- public Enumeration getNonMatchingHeaderLines(String[] names) ++ public Enumeration<String> getNonMatchingHeaderLines(String[] names) +diff -r 1f6b2c17e291 mail/src/main/java/javax/mail/internet/ParameterList.java +- a/mail/src/main/java/javax/mail/internet/ParameterList.java ++++ b/mail/src/main/java/javax/mail/internet/ParameterList.java +@@ -627,2 +627,1 @@ +- @SuppressWarnings("rawtypes") +- public Enumeration getNames() { ++ public Enumeration<String> getNames() { + + +=================================================================== + +4. MailDateFormat changes for version 1.6 (174) +----------------------------------------------- + +The parse(String) method currently returns null for invalid dates, +which violates the contract of DateFormat. It should throw ParseException. + +Some methods of MailDateFormat throw UnsupportedOperationException. +The following methods should also do so: + + /** + * This method always throws an UnsupportedOperationException and + * should not be used because RFC 2822 mandates a specific pattern. + * + * @throws UnsupportedOperationException if this method is invoked + * @since JavaMail 1.6 + */ + @Override + public void applyLocalizedPattern(String pattern) { + throw new UnsupportedOperationException("Method " + + "applyLocalizedPattern() shouldn't be called"); + } + + /** + * This method always throws an UnsupportedOperationException and + * should not be used because RFC 2822 mandates a specific pattern. + * + * @throws UnsupportedOperationException if this method is invoked + * @since JavaMail 1.6 + */ + @Override + public void applyPattern(String pattern) { + throw new UnsupportedOperationException("Method " + + "applyPattern() shouldn't be called"); + } + + /** + * This method always throws an UnsupportedOperationException and + * should not be used because RFC 2822 mandates another strategy + * for interpreting 2-digits years. + * + * @return the start of the 100-year period into which two digit + * years are parsed + * @throws UnsupportedOperationException if this method is invoked + * @since JavaMail 1.6 + */ + @Override + public Date get2DigitYearStart() { + throw new UnsupportedOperationException("Method " + + "get2DigitYearStart() shouldn't be called"); + } + + /** + * This method always throws an UnsupportedOperationException and + * should not be used because RFC 2822 mandates another strategy + * for interpreting 2-digits years. + * + * @throws UnsupportedOperationException if this method is invoked + * @since JavaMail 1.6 + */ + @Override + public void set2DigitYearStart(Date startDate) { + throw new UnsupportedOperationException("Method " + + "set2DigitYearStart() shouldn't be called"); + } + + /** + * This method always throws an UnsupportedOperationException and + * should not be used because RFC 2822 mandates specific date + * format symbols. + * + * @throws UnsupportedOperationException if this method is invoked + * @since JavaMail 1.6 + */ + @Override + public void setDateFormatSymbols(DateFormatSymbols newFormatSymbols) { + throw new UnsupportedOperationException("Method " + + "setDateFormatSymbols() shouldn't be called"); + } + +MailDateFormat should include a proper clone method: + + /** + * Overrides Cloneable. + * + * @return a clone of this instance + * @since JavaMail 1.6 + */ + @Override + public MailDateFormat clone() { + return (MailDateFormat) super.clone(); + } + + +=================================================================== + +5. Store, Transport, and Folder should implement AutoCloseable (159) +-------------------------------------------------------------------- + +To enable use in a try-with-resources block, the Store, Transport, +and Folder classes should implement the java.lang.AutoCloseable interface. +Store and Transport are subclasses of Service, which already has the +required close() method, so having Service implement AutoCloseable is +sufficient. The Folder class includes a close method with a required +"expunge" parameter so we add a new close() method with no parameter +that behaves the same as close(true), and have Folder implement AutoCloseable. + + public abstract class Service implements AutoCloseable { + ... + } + + public abstract class Folder implements AutoCloseable { + ... + + /** + * Close this Folder and expunge deleted messages. <p> + * + * A CLOSED ConnectionEvent is delivered to any ConnectionListeners + * registered on this Folder. Note that the folder is closed even + * if this method terminates abnormally by throwing a + * MessagingException. <p> + * + * This method supports the {@link java.lang.AutoCloseable AutoCloseable} + * interface. <p> + * + * This implementation calls <code>close(true)</code>. + * + * @exception IllegalStateException if this folder is not opened + * @exception MessagingException for other failures + * @see javax.mail.event.ConnectionEvent + * @since JavaMail 1.6 + */ + @Override + public void close() throws MessagingException { + close(true); + } + } + + +=================================================================== + +6. The UIDFolder interface should have a getter for UIDNEXT (104) +----------------------------------------------------------------- + +The UIDFolder interface models the UID support in the IMAP protocol. +After UIDFolder was originally created, the IMAP protocol added +support for getting the value of the next UID that will be assigned. +The IMAP provider in JavaMail has supported this for quite some time; +it should be added to the UIDFolder interface: + + /** + * Returns the predicted UID that will be assigned to the + * next message that is appended to this folder. + * Messages might be appended to the folder after this value + * is retrieved, causing this value to be out of date. + * This value might only be updated when a folder is first opened. + * Note that messages may have been appended to the folder + * while it was open and thus this value may be out of + * date. <p> + * + * If the value is unknown, -1 is returned. <p> + * + * @return the UIDNEXT value, or -1 if unknown + * @exception MessagingException for failures + * @since JavaMail 1.6 + */ + public long getUIDNext() throws MessagingException; + +Ideally this new method added to the interface would include a default +implementation to provide compatibility with existing classes that +implement this method. However, since the JavaMail 1.6 reference +implementation targets Java SE 7, this is not possible. It's very +likely that the only class implementing this interface is the IMAPFolder +class in the JavaMail reference implementation, thus this incompatibility +is extremely unlikely to cause a problem in practice. + + +=================================================================== + +7. The UIDFolder interface should have a MAXUID constant (244) +-------------------------------------------------------------- + +An IMAP UID is a 32-bit unsigned integer and is represented as a Java long. +A new constant indicates the maximum value of a UID: + + /** + * The largest value possible for a UID, a 32-bit unsigned integer. + * This can be used to fetch all new messages by keeping track of the + * last UID that was seen and using: + * <blockquote><pre> + * + * Folder f = store.getFolder("whatever"); + * UIDFolder uf = (UIDFolder)f; + * Message[] newMsgs = + * uf.getMessagesByUID(lastSeenUID + 1, UIDFolder.MAXUID); + * + * </pre></blockquote><p> + * + * @since JavaMail 1.6 + */ + public static final long MAXUID = 0xffffffffL; + + +=================================================================== + +8. MimeMultipart should throw ParseException for parsing errors (75) +-------------------------------------------------------------------- + +ParseException indicates an error parsing MIME messages. In addition +to applying to MIME headers, it seems reasonable to expand it to cover +multipart message parsing. Since ParseException is a subclass of +MessagingException, it merely reports more precisely the cause of the +error. + +The description of ParseException is changed to: + + * The exception thrown due to an error in parsing RFC822 + * or MIME headers, including multipart bodies. + +MimeMultipart documents that ParseException can be thrown from an +existing constructor and method: + + /** + * Constructs a MimeMultipart object and its bodyparts from the + * given DataSource. <p> + * + * This constructor handles as a special case the situation where the + * given DataSource is a MultipartDataSource object. In this case, this + * method just invokes the superclass (i.e., Multipart) constructor + * that takes a MultipartDataSource object. <p> + * + * Otherwise, the DataSource is assumed to provide a MIME multipart + * byte stream. The <code>parsed</code> flag is set to false. When + * the data for the body parts are needed, the parser extracts the + * "boundary" parameter from the content type of this DataSource, + * skips the 'preamble' and reads bytes till the terminating + * boundary and creates MimeBodyParts for each part of the stream. + * + * @param ds DataSource, can be a MultipartDataSource + * @exception ParseException for failures parsing the message + * @exception MessagingException for other failures + */ + public MimeMultipart(DataSource ds) throws MessagingException + + ... + + /** + * Parse the InputStream from our DataSource, constructing the + * appropriate MimeBodyParts. The <code>parsed</code> flag is + * set to true, and if true on entry nothing is done. This + * method is called by all other methods that need data for + * the body parts, to make sure the data has been parsed. + * The {@link #initializeProperties} method is called before + * parsing the data. + * + * @exception ParseException for failures parsing the message + * @exception MessagingException for other failures + * @since JavaMail 1.2 + */ + protected synchronized void parse() throws MessagingException + + +=================================================================== + +9. Support addressing i18n via RFC 6530/6531/6532 (93) +------------------------------------------------------ + +To enable support for UTF-8 email addresses, the following methods +are added to InternetAddress: + + /** + * Convert the given array of InternetAddress objects into + * a comma separated sequence of address strings. The + * resulting string contains Unicode characters. <p> + * + * @param addresses array of InternetAddress objects + * @exception ClassCastException if any address object in the + * given array is not an InternetAddress object. + * Note that this is a RuntimeException. + * @return comma separated string of addresses + * @since JavaMail 1.6 + */ + public static String toUnicodeString(Address[] addresses) + + /** + * Convert the given array of InternetAddress objects into + * a comma separated sequence of address strings. The + * resulting string contains Unicode characters. <p> + * + * The 'used' parameter specifies the number of character positions + * already taken up in the field into which the resulting address + * sequence string is to be inserted. It is used to determine the + * line-break positions in the resulting address sequence string. + * + * @param addresses array of InternetAddress objects + * @param used number of character positions already used, in + * the field into which the address string is to + * be inserted. + * @exception ClassCastException if any address object in the + * given array is not an InternetAddress object. + * Note that this is a RuntimeException. + * @return comma separated string of addresses + * @since JavaMail 1.6 + */ + public static String toUnicodeString(Address[] addresses, int used) + + +The following constructor and method are added to InternetHeaders: + + /** + * Read and parse the given RFC822 message stream till the + * blank line separating the header from the body. The input + * stream is left positioned at the start of the body. The + * header lines are stored internally. <p> + * + * For efficiency, wrap a BufferedInputStream around the actual + * input stream and pass it as the parameter. <p> + * + * No placeholder entries are inserted; the original order of + * the headers is preserved. + * + * @param is RFC822 input stream + * @param allowutf8 if UTF-8 encoded headers are allowed + * @exception MessagingException for any I/O error reading the stream + * @since JavaMail 1.6 + */ + public InternetHeaders(InputStream is, boolean allowutf8) + throws MessagingException + + + /** + * Read and parse the given RFC822 message stream till the + * blank line separating the header from the body. Store the + * header lines inside this InternetHeaders object. The order + * of header lines is preserved. <p> + * + * Note that the header lines are added into this InternetHeaders + * object, so any existing headers in this object will not be + * affected. Headers are added to the end of the existing list + * of headers, in order. + * + * @param is RFC822 input stream + * @param allowutf8 if UTF-8 encoded headers are allowed + * @exception MessagingException for any I/O error reading the stream + * @since JavaMail 1.6 + */ + public void load(InputStream is, boolean allowutf8) + throws MessagingException + +The following Session property can be set to enable implicit use of +these new methods: + +mail.mime.allowutf8: + + If set to "true", UTF-8 strings are allowed in message headers, + e.g., in addresses. This should only be set if the mail server also + supports UTF-8. + + +=================================================================== + +10. Look for resource files in <java.home>/conf on JDK 1.9 (247) +---------------------------------------------------------------- + +JDK 1.9 adds a new <java.home>/conf directory to hold configuration +files that were previously stored in <java.home>/lib. When using +JavaMail on JDK 1.9, it should look for its (optional) configuration +files in the <java.home>/conf directory. + +The specification of the Session class is changed as follows: + + /** + * The Session class represents a mail session and is not subclassed. + * It collects together properties and defaults used by the mail API's. + * A single default session can be shared by multiple applications on + * the desktop. Unshared sessions can also be created. <p> + * + * The Session class provides access to the protocol providers that + * implement the <code>Store</code>, <code>Transport</code>, and related + * classes. The protocol providers are configured using the following + * files: + * <ul> + * <li> <code>javamail.providers</code> and + * <code>javamail.default.providers</code> </li> + * <li> <code>javamail.address.map</code> and + * <code>javamail.default.address.map</code> </li> + * </ul> + * <p> + * Each <code>javamail.</code><i>X</i> resource file is searched for + * using three methods in the following order: + * <ol> + * <li> <code><i>java.home</i>/<i>conf</i>/javamail.</code><i>X</i> </li> + * <li> <code>META-INF/javamail.</code><i>X</i> </li> + * <li> <code>META-INF/javamail.default.</code><i>X</i> </li> + * </ol> + * <p> + * (Where <i>java.home</i> is the value of the "java.home" System + * property and <i>conf</i> is the directory named "conf" if it exists, + * otherwise the directory named "lib"; the "conf" directory was + * introduced in JDK 1.9.) + * <p> + * The first method allows the user to include their own version of the + * resource file by placing it in the <i>conf</i> directory where the + * <code>java.home</code> property points. The second method allows an + * application that uses the JavaMail APIs to include their own resource + * files in their application's or jar file's <code>META-INF</code> + * directory. The <code>javamail.default.</code><i>X</i> default files + * are part of the JavaMail <code>mail.jar</code> file and should not be + * supplied by users. <p> + + +=================================================================== + +11. Flags convenience methods (249) +----------------------------------- + +When copying messages from one server to another, it's sometimes necessary +to adjust the message flags based on the capabilities of the target server. +It would be convenient if the Flags class had methods to clear any flags +not supported by the target server, to clear all user flags, and to clear +all system flags. + + /** + * Remove any flags <strong>not</strong> in the given Flags object. + * Useful for clearing flags not supported by a server. If the + * given Flags object includes the Flags.Flag.USER flag, all user + * flags in this Flags object are retained. + * + * @param f the flags to keep + * @return true if this Flags object changed + * @since JavaMail 1.6 + */ + public boolean retainAll(Flags f) + + /** + * Clear all of the system flags. + * + * @since JavaMail 1.6 + */ + public void clearSystemFlags() + + /** + * Clear all of the user flags. + * + * @since JavaMail 1.6 + */ + public void clearUserFlags()
diff --git a/dsn/pom.xml b/dsn/pom.xml new file mode 100644 index 0000000..c61952c --- /dev/null +++ b/dsn/pom.xml
@@ -0,0 +1,98 @@ +<?xml version="1.0" encoding="iso-8859-1"?> +<!-- + + Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 + http://maven.apache.org/maven-v4_0_0.xsd"> + <parent> + <groupId>com.sun.mail</groupId> + <artifactId>all</artifactId> + <version>1.6.3</version> + </parent> + <modelVersion>4.0.0</modelVersion> + <groupId>com.sun.mail</groupId> + <artifactId>dsn</artifactId> + <packaging>jar</packaging> + <name>JavaMail API dsn support</name> + + <properties> + <mail.packages.export> + com.sun.mail.dsn; version=${mail.osgiversion} + </mail.packages.export> + </properties> + + <build> + <plugins> + <!-- + Configure SpotBugs to run with "mvn findbugs:findbugs" + and generate XML output that can be used by the Hudson + FindBugs plugin. + --> + <plugin> + <groupId>com.github.spotbugs</groupId> + <artifactId>spotbugs-maven-plugin</artifactId> + <configuration> + <skip>false</skip> + <threshold>${findbugs.threshold}</threshold> + <!-- + <excludeFilterFile> + ${project.basedir}/exclude.xml + </excludeFilterFile> + --> + <findbugsXmlWithMessages>true</findbugsXmlWithMessages> + </configuration> + </plugin> + </plugins> + </build> + + <dependencies> + <dependency> + <groupId>com.sun.mail</groupId> + <artifactId>jakarta.mail</artifactId> + </dependency> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <version>4.12</version> + <scope>test</scope> + <optional>true</optional> + </dependency> + </dependencies> + + <reporting> + <plugins> + <!-- + Configure SpotBugs to run with "mvn site" and + generate html output that can be viewed directly. + --> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>findbugs-maven-plugin</artifactId> + <configuration> + <skip>false</skip> + <threshold>${findbugs.threshold}</threshold> + <!-- + <excludeFilterFile>exclude.xml</excludeFilterFile> + --> + </configuration> + </plugin> + </plugins> + </reporting> +</project>
diff --git a/dsn/src/main/java/com/sun/mail/dsn/DeliveryStatus.java b/dsn/src/main/java/com/sun/mail/dsn/DeliveryStatus.java new file mode 100644 index 0000000..c20e6eb --- /dev/null +++ b/dsn/src/main/java/com/sun/mail/dsn/DeliveryStatus.java
@@ -0,0 +1,188 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.dsn; + +import java.io.*; +import java.util.*; +import java.util.logging.Level; + +import javax.activation.*; +import javax.mail.*; +import javax.mail.internet.*; + +import com.sun.mail.util.LineOutputStream; // XXX +import com.sun.mail.util.PropUtil; +import com.sun.mail.util.MailLogger; + +/** + * A message/delivery-status message content, as defined in + * <A HREF="http://www.ietf.org/rfc/rfc3464.txt" TARGET="_top">RFC 3464</A>. + * + * @since JavaMail 1.4 + */ +public class DeliveryStatus extends Report { + + private static MailLogger logger = new MailLogger( + DeliveryStatus.class, + "DEBUG DSN", + PropUtil.getBooleanSystemProperty("mail.dsn.debug", false), + System.out); + + /** + * The DSN fields for the message. + */ + protected InternetHeaders messageDSN; + + /** + * The DSN fields for each recipient. + */ + protected InternetHeaders[] recipientDSN; + + /** + * Construct a delivery status notification with no content. + * + * @exception MessagingException for failures + */ + public DeliveryStatus() throws MessagingException { + super("delivery-status"); + messageDSN = new InternetHeaders(); + recipientDSN = new InternetHeaders[0]; + } + + /** + * Construct a delivery status notification by parsing the + * supplied input stream. + * + * @param is the input stream + * @exception IOException for I/O errors reading the stream + * @exception MessagingException for other failures + */ + public DeliveryStatus(InputStream is) + throws MessagingException, IOException { + super("delivery-status"); + messageDSN = new InternetHeaders(is); + logger.fine("got messageDSN"); + Vector<InternetHeaders> v = new Vector<>(); + try { + while (is.available() > 0) { + InternetHeaders h = new InternetHeaders(is); + logger.fine("got recipientDSN"); + v.addElement(h); + } + } catch (EOFException ex) { + logger.log(Level.FINE, "got EOFException", ex); + } + if (logger.isLoggable(Level.FINE)) + logger.fine("recipientDSN size " + v.size()); + recipientDSN = new InternetHeaders[v.size()]; + v.copyInto(recipientDSN); + } + + /** + * Return all the per-message fields in the delivery status notification. + * The fields are defined as: + * + * <pre> + * per-message-fields = + * [ original-envelope-id-field CRLF ] + * reporting-mta-field CRLF + * [ dsn-gateway-field CRLF ] + * [ received-from-mta-field CRLF ] + * [ arrival-date-field CRLF ] + * *( extension-field CRLF ) + * </pre> + * + * @return the per-message DSN fields + */ + // XXX - could parse each of these fields + public InternetHeaders getMessageDSN() { + return messageDSN; + } + + /** + * Set the per-message fields in the delivery status notification. + * + * @param messageDSN the per-message DSN fields + */ + public void setMessageDSN(InternetHeaders messageDSN) { + this.messageDSN = messageDSN; + } + + /** + * Return the number of recipients for which we have + * per-recipient delivery status notification information. + * + * @return the number of recipients + */ + public int getRecipientDSNCount() { + return recipientDSN.length; + } + + /** + * Return the delivery status notification information for + * the specified recipient. + * + * @param n the recipient number + * @return the DSN fields for the recipient + */ + public InternetHeaders getRecipientDSN(int n) { + return recipientDSN[n]; + } + + /** + * Add deliver status notification information for another + * recipient. + * + * @param h the DSN fields for the recipient + */ + public void addRecipientDSN(InternetHeaders h) { + InternetHeaders[] rh = new InternetHeaders[recipientDSN.length + 1]; + System.arraycopy(recipientDSN, 0, rh, 0, recipientDSN.length); + recipientDSN = rh; + recipientDSN[recipientDSN.length - 1] = h; + } + + public void writeTo(OutputStream os) throws IOException { + // see if we already have a LOS + LineOutputStream los = null; + if (os instanceof LineOutputStream) { + los = (LineOutputStream) os; + } else { + los = new LineOutputStream(os); + } + + writeInternetHeaders(messageDSN, los); + los.writeln(); + for (int i = 0; i < recipientDSN.length; i++) { + writeInternetHeaders(recipientDSN[i], los); + los.writeln(); + } + } + + private static void writeInternetHeaders(InternetHeaders h, + LineOutputStream los) throws IOException { + Enumeration e = h.getAllHeaderLines(); + while (e.hasMoreElements()) + los.writeln((String)e.nextElement()); + } + + public String toString() { + return "DeliveryStatus: Reporting-MTA=" + + messageDSN.getHeader("Reporting-MTA", null) + ", #Recipients=" + + recipientDSN.length; + } +}
diff --git a/dsn/src/main/java/com/sun/mail/dsn/DispositionNotification.java b/dsn/src/main/java/com/sun/mail/dsn/DispositionNotification.java new file mode 100644 index 0000000..30f6e9d --- /dev/null +++ b/dsn/src/main/java/com/sun/mail/dsn/DispositionNotification.java
@@ -0,0 +1,133 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.dsn; + +import java.io.*; +import java.util.*; + +import javax.mail.*; +import javax.mail.internet.*; + +import com.sun.mail.util.LineOutputStream; // XXX +import com.sun.mail.util.PropUtil; +import com.sun.mail.util.MailLogger; + +/** + * A message/disposition-notification message content, as defined in + * <A HREF="http://www.ietf.org/rfc/rfc3798.txt" TARGET="_top">RFC 3798</A>. + * + * @since JavaMail 1.4.2 + */ +public class DispositionNotification extends Report { + + private static MailLogger logger = new MailLogger( + DeliveryStatus.class, + "DEBUG DSN", + PropUtil.getBooleanSystemProperty("mail.dsn.debug", false), + System.out); + + /** + * The disposition notification content fields. + */ + protected InternetHeaders notifications; + + /** + * Construct a disposition notification with no content. + * + * @exception MessagingException for failures + */ + public DispositionNotification() throws MessagingException { + super("disposition-notification"); + notifications = new InternetHeaders(); + } + + /** + * Construct a disposition notification by parsing the + * supplied input stream. + * + * @param is the input stream + * @exception IOException for I/O errors reading the stream + * @exception MessagingException for other failures + */ + public DispositionNotification(InputStream is) + throws MessagingException, IOException { + super("disposition-notification"); + notifications = new InternetHeaders(is); + logger.fine("got MDN notification content"); + } + + /** + * Return all the disposition notification fields in the + * disposition notification. + * The fields are defined as: + * + * <pre> + * disposition-notification-content = + * [ reporting-ua-field CRLF ] + * [ mdn-gateway-field CRLF ] + * [ original-recipient-field CRLF ] + * final-recipient-field CRLF + * [ original-message-id-field CRLF ] + * disposition-field CRLF + * *( failure-field CRLF ) + * *( error-field CRLF ) + * *( warning-field CRLF ) + * *( extension-field CRLF ) + * </pre> + * + * @return the DSN fields + */ + // XXX - could parse each of these fields + public InternetHeaders getNotifications() { + return notifications; + } + + /** + * Set the disposition notification fields in the + * disposition notification. + * + * @param notifications the DSN fields + */ + public void setNotifications(InternetHeaders notifications) { + this.notifications = notifications; + } + + public void writeTo(OutputStream os) throws IOException { + // see if we already have a LOS + LineOutputStream los = null; + if (os instanceof LineOutputStream) { + los = (LineOutputStream) os; + } else { + los = new LineOutputStream(os); + } + + writeInternetHeaders(notifications, los); + los.writeln(); + } + + private static void writeInternetHeaders(InternetHeaders h, + LineOutputStream los) throws IOException { + Enumeration e = h.getAllHeaderLines(); + while (e.hasMoreElements()) + los.writeln((String)e.nextElement()); + } + + public String toString() { + return "DispositionNotification: Reporting-UA=" + + notifications.getHeader("Reporting-UA", null); + } +}
diff --git a/dsn/src/main/java/com/sun/mail/dsn/MessageHeaders.java b/dsn/src/main/java/com/sun/mail/dsn/MessageHeaders.java new file mode 100644 index 0000000..9acdea0 --- /dev/null +++ b/dsn/src/main/java/com/sun/mail/dsn/MessageHeaders.java
@@ -0,0 +1,91 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.dsn; + +import java.io.*; + +import javax.activation.*; +import javax.mail.*; +import javax.mail.internet.*; + +/** + * A special MimeMessage object that contains only message headers, + * no content. Used to represent the MIME type text/rfc822-headers. + * + * @since JavaMail 1.4 + */ +public class MessageHeaders extends MimeMessage { + + /** + * Construct a MessageHeaders object. + * + * @exception MessagingException for failures + */ + public MessageHeaders() throws MessagingException { + super((Session)null); + content = new byte[0]; + } + + /** + * Constructs a MessageHeaders object from the given InputStream. + * + * @param is InputStream + * @exception MessagingException for failures + */ + public MessageHeaders(InputStream is) throws MessagingException { + super(null, is); + content = new byte[0]; + } + + /** + * Constructs a MessageHeaders object using the given InternetHeaders. + * + * @param headers InternetHeaders to use + * @exception MessagingException for failures + */ + public MessageHeaders(InternetHeaders headers) throws MessagingException { + super((Session)null); + this.headers = headers; + content = new byte[0]; + } + + /** + * Return the size of this message. + * Always returns zero. + */ + public int getSize() { + return 0; + } + + public InputStream getInputStream() { + return new ByteArrayInputStream(content); + } + + protected InputStream getContentStream() { + return new ByteArrayInputStream(content); + } + + /** + * Can't set any content for a MessageHeaders object. + * + * @exception MessagingException always + */ + public void setDataHandler(DataHandler dh) throws MessagingException { + throw new MessagingException("Can't set content for MessageHeaders"); + } + +}
diff --git a/dsn/src/main/java/com/sun/mail/dsn/MultipartReport.java b/dsn/src/main/java/com/sun/mail/dsn/MultipartReport.java new file mode 100644 index 0000000..ee19efe --- /dev/null +++ b/dsn/src/main/java/com/sun/mail/dsn/MultipartReport.java
@@ -0,0 +1,430 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.dsn; + +import java.io.*; +import java.util.Vector; + +import javax.activation.*; +import javax.mail.*; +import javax.mail.internet.*; + +/** + * A multipart/report message content, as defined in + * <A HREF="http://www.ietf.org/rfc/rfc3462.txt" TARGET="_top">RFC 3462</A>. + * A multipart/report content is a container for mail reports + * of any kind, and is most often used to return a delivery + * status report or a disposition notification report. <p> + * + * A MultipartReport object is a special type of MimeMultipart + * object with a restricted set of body parts. A MultipartReport + * object contains: + * <ul> + * <li>[Required] A human readable text message describing the + * reason the report was generated.</li> + * <li>[Required] A {@link Report} object containing the + * details for why the report was generated.</li> + * <li>[Optional] A returned copy of the entire message, or just + * its headers, which caused the generation of this report. + * </ul> + * Many of the normal MimeMultipart operations are restricted to + * ensure that the MultipartReport object always follows this + * structure. + * + * @since JavaMail 1.4 + */ +public class MultipartReport extends MimeMultipart { + protected boolean constructed; // true when done with constructor + + /** + * Construct a multipart/report object with no content. + * + * @exception MessagingException for failures + */ + public MultipartReport() throws MessagingException { + super("report"); + // always at least two body parts + MimeBodyPart mbp = new MimeBodyPart(); + setBodyPart(mbp, 0); + mbp = new MimeBodyPart(); + setBodyPart(mbp, 1); + constructed = true; + } + + /** + * Construct a multipart/report object with the specified plain + * text and report type (DeliveryStatus or DispositionNotification) + * to be returned to the user. + * + * @param text the plain text + * @param report the Report object + * @exception MessagingException for failures + */ + public MultipartReport(String text, Report report) + throws MessagingException { + super("report"); + ContentType ct = new ContentType(contentType); + String reportType = report.getType(); + ct.setParameter("report-type", reportType); + contentType = ct.toString(); + MimeBodyPart mbp = new MimeBodyPart(); + mbp.setText(text); + setBodyPart(mbp, 0); + mbp = new MimeBodyPart(); + ct = new ContentType("message", reportType, null); + mbp.setContent(report, ct.toString()); + setBodyPart(mbp, 1); + constructed = true; + } + + /** + * Construct a multipart/report object with the specified plain + * text, report, and original message to be returned to the user. + * + * @param text the plain text + * @param report the Report object + * @param msg the message this report is about + * @exception MessagingException for failures + */ + public MultipartReport(String text, Report report, MimeMessage msg) + throws MessagingException { + this(text, report); + if (msg != null) { + MimeBodyPart mbp = new MimeBodyPart(); + mbp.setContent(msg, "message/rfc822"); + setBodyPart(mbp, 2); + } + } + + /** + * Construct a multipart/report object with the specified plain + * text, report, and headers from the original message + * to be returned to the user. + * + * @param text the plain text + * @param report the Report object + * @param hdr the headers of the message this report is about + * @exception MessagingException for failures + */ + public MultipartReport(String text, Report report, InternetHeaders hdr) + throws MessagingException { + this(text, report); + if (hdr != null) { + MimeBodyPart mbp = new MimeBodyPart(); + mbp.setContent(new MessageHeaders(hdr), "text/rfc822-headers"); + setBodyPart(mbp, 2); + } + } + + /** + * Constructs a MultipartReport object and its bodyparts from the + * given DataSource. <p> + * + * @param ds DataSource, can be a MultipartDataSource + * @exception MessagingException for failures + */ + public MultipartReport(DataSource ds) throws MessagingException { + super(ds); + parse(); + constructed = true; + /* + * Can't fail to construct object because some programs just + * want to treat this as a Multipart and examine the parts. + * + if (getCount() < 2 || getCount() > 3) // XXX allow extra parts + throw new MessagingException( + "Wrong number of parts in multipart/report: " + getCount()); + */ + } + + /** + * Get the plain text to be presented to the user, if there is any. + * Rarely, the message may contain only HTML text, or no text at + * all. If the text body part of this multipart/report object is + * of type text/plain, or if it is of type multipart/alternative + * and contains a text/plain part, the text from that part is + * returned. Otherwise, null is return and the {@link #getTextBodyPart + * getTextBodyPart} method may be used to extract the data. + * + * @return the text + * @exception MessagingException for failures + */ + public synchronized String getText() throws MessagingException { + try { + BodyPart bp = getBodyPart(0); + if (bp.isMimeType("text/plain")) + return (String)bp.getContent(); + if (bp.isMimeType("multipart/alternative")) { + Multipart mp = (Multipart)bp.getContent(); + for (int i = 0; i < mp.getCount(); i++) { + bp = mp.getBodyPart(i); + if (bp.isMimeType("text/plain")) + return (String)bp.getContent(); + } + } + } catch (IOException ex) { + throw new MessagingException("Exception getting text content", ex); + } + return null; + } + + /** + * Set the message to be presented to the user as just a text/plain + * part containing the specified text. + * + * @param text the text + * @exception MessagingException for failures + */ + public synchronized void setText(String text) throws MessagingException { + MimeBodyPart mbp = new MimeBodyPart(); + mbp.setText(text); + setBodyPart(mbp, 0); + } + + /** + * Return the body part containing the message to be presented to + * the user, usually just a text/plain part. + * + * @return the body part containing the text + * @exception MessagingException for failures + */ + public synchronized MimeBodyPart getTextBodyPart() + throws MessagingException { + return (MimeBodyPart)getBodyPart(0); + } + + /** + * Set the body part containing the text to be presented to the + * user. Usually this a text/plain part, but it might also be + * a text/html part or a multipart/alternative part containing + * text/plain and text/html parts. Any type is allowed here + * but these types are most common. + * + * @param mbp the body part containing the text + * @exception MessagingException for failures + */ + public synchronized void setTextBodyPart(MimeBodyPart mbp) + throws MessagingException { + setBodyPart(mbp, 0); + } + + /** + * Get the report associated with this multipart/report. + * + * @return the Report object + * @exception MessagingException for failures + * @since JavaMail 1.4.2 + */ + public synchronized Report getReport() throws MessagingException { + if (getCount() < 2) + return null; + BodyPart bp = getBodyPart(1); + try { + Object content = bp.getContent(); + if (!(content instanceof Report)) + return null; + return (Report)content; + } catch (IOException ex) { + throw new MessagingException("IOException getting Report", ex); + } + } + + /** + * Set the report associated with this multipart/report. + * + * @param report the Report object + * @exception MessagingException for failures + * @since JavaMail 1.4.2 + */ + public synchronized void setReport(Report report) + throws MessagingException { + MimeBodyPart mbp = new MimeBodyPart(); + ContentType ct = new ContentType(contentType); + String reportType = report.getType(); + ct.setParameter("report-type", reportType); + contentType = ct.toString(); + ct = new ContentType("message", reportType, null); + mbp.setContent(report, ct.toString()); + setBodyPart(mbp, 1); + } + + /** + * Get the delivery status associated with this multipart/report. + * + * @return the delivery status + * @exception MessagingException for failures + * @deprecated use getReport instead + */ + @Deprecated + public synchronized DeliveryStatus getDeliveryStatus() + throws MessagingException { + if (getCount() < 2) + return null; + BodyPart bp = getBodyPart(1); + if (!bp.isMimeType("message/delivery-status")) + return null; + try { + return (DeliveryStatus)bp.getContent(); + } catch (IOException ex) { + throw new MessagingException("IOException getting DeliveryStatus", + ex); + } + } + + /** + * Set the delivery status associated with this multipart/report. + * + * @param status the deliver status + * @exception MessagingException for failures + * @deprecated use setReport instead + */ + public synchronized void setDeliveryStatus(DeliveryStatus status) + throws MessagingException { + MimeBodyPart mbp = new MimeBodyPart(); + mbp.setContent(status, "message/delivery-status"); + setBodyPart(mbp, 1); + ContentType ct = new ContentType(contentType); + ct.setParameter("report-type", "delivery-status"); + contentType = ct.toString(); + } + + /** + * Get the original message that is being returned along with this + * multipart/report. If no original message is included, null is + * returned. In some cases only the headers of the original + * message will be returned as an object of type MessageHeaders. + * + * @return the returned message + * @exception MessagingException for failures + */ + public synchronized MimeMessage getReturnedMessage() + throws MessagingException { + if (getCount() < 3) + return null; + BodyPart bp = getBodyPart(2); + if (!bp.isMimeType("message/rfc822") && + !bp.isMimeType("text/rfc822-headers")) + return null; + try { + return (MimeMessage)bp.getContent(); + } catch (IOException ex) { + throw new MessagingException("IOException getting ReturnedMessage", + ex); + } + } + + /** + * Set the original message to be returned as part of the + * multipart/report. If msg is null, any previously set + * returned message or headers is removed. + * + * @param msg the returned message + * @exception MessagingException for failures + */ + public synchronized void setReturnedMessage(MimeMessage msg) + throws MessagingException { + if (msg == null) { + super.removeBodyPart(2); + return; + } + MimeBodyPart mbp = new MimeBodyPart(); + if (msg instanceof MessageHeaders) + mbp.setContent(msg, "text/rfc822-headers"); + else + mbp.setContent(msg, "message/rfc822"); + setBodyPart(mbp, 2); + } + + private synchronized void setBodyPart(BodyPart part, int index) + throws MessagingException { + if (parts == null) // XXX - can never happen? + parts = new Vector<BodyPart>(); + + if (index < parts.size()) + super.removeBodyPart(index); + super.addBodyPart(part, index); + } + + + // Override Multipart methods to preserve integrity of multipart/report. + + /** + * Set the subtype. Throws MessagingException. + * + * @param subtype Subtype + * @exception MessagingException always; can't change subtype + */ + public synchronized void setSubType(String subtype) + throws MessagingException { + throw new MessagingException("Can't change subtype of MultipartReport"); + } + + /** + * Remove the specified part from the multipart message. + * Not allowed on a multipart/report object. + * + * @param part The part to remove + * @exception MessagingException always + */ + public boolean removeBodyPart(BodyPart part) throws MessagingException { + throw new MessagingException( + "Can't remove body parts from multipart/report"); + } + + /** + * Remove the part at specified location (starting from 0). + * Not allowed on a multipart/report object. + * + * @param index Index of the part to remove + * @exception MessagingException always + */ + public void removeBodyPart(int index) throws MessagingException { + throw new MessagingException( + "Can't remove body parts from multipart/report"); + } + + /** + * Adds a Part to the multipart. + * Not allowed on a multipart/report object. + * + * @param part The Part to be appended + * @exception MessagingException always + */ + public synchronized void addBodyPart(BodyPart part) + throws MessagingException { + // Once constructor is done, don't allow this anymore. + if (!constructed) + super.addBodyPart(part); + else + throw new MessagingException( + "Can't add body parts to multipart/report 1"); + } + + /** + * Adds a BodyPart at position <code>index</code>. + * Not allowed on a multipart/report object. + * + * @param part The BodyPart to be inserted + * @param index Location where to insert the part + * @exception MessagingException always + */ + public synchronized void addBodyPart(BodyPart part, int index) + throws MessagingException { + throw new MessagingException( + "Can't add body parts to multipart/report 2"); + } +}
diff --git a/dsn/src/main/java/com/sun/mail/dsn/Report.java b/dsn/src/main/java/com/sun/mail/dsn/Report.java new file mode 100644 index 0000000..b103d56 --- /dev/null +++ b/dsn/src/main/java/com/sun/mail/dsn/Report.java
@@ -0,0 +1,48 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.dsn; + +/** + * An abstract report type, to be included in a MultipartReport. + * Subclasses define specific report types, such as {@link DeliveryStatus} + * and {@link DispositionNotification}. + * + * @since JavaMail 1.4.2 + */ +public abstract class Report { + protected String type; // the MIME subtype of the report + + /** + * Construct a report of the indicated MIME subtype. + * The primary MIME type is always "message". + * + * @param type the MIME subtype + */ + protected Report(String type) { + this.type = type; + } + + /** + * Get the MIME subtype of the report. + * The primary MIME type is always "message". + * + * @return the MIME subtype + */ + public String getType() { + return type; + } +}
diff --git a/dsn/src/main/java/com/sun/mail/dsn/message_deliverystatus.java b/dsn/src/main/java/com/sun/mail/dsn/message_deliverystatus.java new file mode 100644 index 0000000..a59ac8a --- /dev/null +++ b/dsn/src/main/java/com/sun/mail/dsn/message_deliverystatus.java
@@ -0,0 +1,106 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.dsn; + +import java.io.*; +import java.util.Properties; +import java.awt.datatransfer.DataFlavor; +import javax.activation.*; +import javax.mail.*; +import javax.mail.internet.*; + + +/** + * DataContentHandler for message/delivery-status MIME type. + * Applications should not use this class directly, it's used indirectly + * through the JavaBeans Activation Framework. + * + * @since JavaMail 1.4 + */ +public class message_deliverystatus implements DataContentHandler { + + ActivationDataFlavor ourDataFlavor = new ActivationDataFlavor( + DeliveryStatus.class, + "message/delivery-status", + "Delivery Status"); + + /** + * return the DataFlavors for this <code>DataContentHandler</code> + * @return The DataFlavors. + */ + public DataFlavor[] getTransferDataFlavors() { + return new DataFlavor[] { ourDataFlavor }; + } + + /** + * return the Transfer Data of type DataFlavor from InputStream + * @param df The DataFlavor. + * @param ds The DataSource corresponding to the data. + * @return a Message object + */ + public Object getTransferData(DataFlavor df, DataSource ds) + throws IOException { + // make sure we can handle this DataFlavor + if (ourDataFlavor.equals(df)) + return getContent(ds); + else + return null; + } + + /** + * Return the content. + */ + public Object getContent(DataSource ds) throws IOException { + // create a new DeliveryStatus + try { + /* + Session session; + if (ds instanceof MessageAware) { + javax.mail.MessageContext mc = + ((MessageAware)ds).getMessageContext(); + session = mc.getSession(); + } else { + // Hopefully a rare case. Also hopefully the application + // has created a default Session that can just be returned + // here. If not, the one we create here is better than + // nothing, but overall not a really good answer. + session = Session.getDefaultInstance(new Properties(), null); + } + return new DeliveryStatus(session, ds.getInputStream()); + */ + return new DeliveryStatus(ds.getInputStream()); + } catch (MessagingException me) { + throw new IOException("Exception creating DeliveryStatus in " + + "message/delivery-status DataContentHandler: " + + me.toString()); + } + } + + /** + */ + public void writeTo(Object obj, String mimeType, OutputStream os) + throws IOException { + // if the object is a DeliveryStatus, we know how to write that out + if (obj instanceof DeliveryStatus) { + DeliveryStatus ds = (DeliveryStatus)obj; + ds.writeTo(os); + + } else { + throw new IOException("unsupported object"); + } + } +}
diff --git a/dsn/src/main/java/com/sun/mail/dsn/message_dispositionnotification.java b/dsn/src/main/java/com/sun/mail/dsn/message_dispositionnotification.java new file mode 100644 index 0000000..6eba802 --- /dev/null +++ b/dsn/src/main/java/com/sun/mail/dsn/message_dispositionnotification.java
@@ -0,0 +1,106 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.dsn; + +import java.io.*; +import java.util.Properties; +import java.awt.datatransfer.DataFlavor; +import javax.activation.*; +import javax.mail.*; +import javax.mail.internet.*; + + +/** + * DataContentHandler for message/disposition-notification MIME type. + * Applications should not use this class directly, it's used indirectly + * through the JavaBeans Activation Framework. + * + * @since JavaMail 1.4.2 + */ +public class message_dispositionnotification implements DataContentHandler { + + ActivationDataFlavor ourDataFlavor = new ActivationDataFlavor( + DispositionNotification.class, + "message/disposition-notification", + "Disposition Notification"); + + /** + * return the DataFlavors for this <code>DataContentHandler</code> + * @return The DataFlavors. + */ + public DataFlavor[] getTransferDataFlavors() { + return new DataFlavor[] { ourDataFlavor }; + } + + /** + * return the Transfer Data of type DataFlavor from InputStream + * @param df The DataFlavor. + * @param ds The DataSource corresponding to the data. + * @return a Message object + */ + public Object getTransferData(DataFlavor df, DataSource ds) + throws IOException { + // make sure we can handle this DataFlavor + if (ourDataFlavor.equals(df)) + return getContent(ds); + else + return null; + } + + /** + * Return the content. + */ + public Object getContent(DataSource ds) throws IOException { + // create a new DispositionNotification + try { + /* + Session session; + if (ds instanceof MessageAware) { + javax.mail.MessageContext mc = + ((MessageAware)ds).getMessageContext(); + session = mc.getSession(); + } else { + // Hopefully a rare case. Also hopefully the application + // has created a default Session that can just be returned + // here. If not, the one we create here is better than + // nothing, but overall not a really good answer. + session = Session.getDefaultInstance(new Properties(), null); + } + return new DispositionNotification(session, ds.getInputStream()); + */ + return new DispositionNotification(ds.getInputStream()); + } catch (MessagingException me) { + throw new IOException( + "Exception creating DispositionNotification in " + + "message/disposition-notification DataContentHandler: " + + me.toString()); + } + } + + /** + */ + public void writeTo(Object obj, String mimeType, OutputStream os) + throws IOException { + // if it's a DispositionNotification, we know how to write that out + if (obj instanceof DispositionNotification) { + DispositionNotification dn = (DispositionNotification)obj; + dn.writeTo(os); + } else { + throw new IOException("unsupported object"); + } + } +}
diff --git a/dsn/src/main/java/com/sun/mail/dsn/multipart_report.java b/dsn/src/main/java/com/sun/mail/dsn/multipart_report.java new file mode 100644 index 0000000..4fbccfa --- /dev/null +++ b/dsn/src/main/java/com/sun/mail/dsn/multipart_report.java
@@ -0,0 +1,92 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.dsn; + +import java.io.*; +import java.awt.datatransfer.DataFlavor; +import javax.activation.*; +import javax.mail.MessagingException; +import javax.mail.internet.*; + + +/** + * DataContentHandler for multipart/report MIME type. + * Applications should not use this class directly, it's used indirectly + * through the JavaBeans Activation Framework. + * + * @since JavaMail 1.4 + */ +public class multipart_report implements DataContentHandler { + private ActivationDataFlavor myDF = new ActivationDataFlavor( + MultipartReport.class, + "multipart/report", + "Multipart Report"); + + /** + * Return the DataFlavors for this <code>DataContentHandler</code>. + * + * @return The DataFlavors + */ + public DataFlavor[] getTransferDataFlavors() { // throws Exception; + return new DataFlavor[] { myDF }; + } + + /** + * Return the Transfer Data of type DataFlavor from InputStream. + * + * @param df The DataFlavor + * @param ds The DataSource corresponding to the data + * @return String object + */ + public Object getTransferData(DataFlavor df, DataSource ds) + throws IOException { + // use myDF.equals to be sure to get ActivationDataFlavor.equals, + // which properly ignores Content-Type parameters in comparison + if (myDF.equals(df)) + return getContent(ds); + else + return null; + } + + /** + * Return the content. + */ + public Object getContent(DataSource ds) throws IOException { + try { + return new MultipartReport(ds); + } catch (MessagingException e) { + IOException ioex = + new IOException("Exception while constructing MultipartReport"); + ioex.initCause(e); + throw ioex; + } + } + + /** + * Write the object to the output stream, using the specific MIME type. + */ + public void writeTo(Object obj, String mimeType, OutputStream os) + throws IOException { + if (obj instanceof MultipartReport) { + try { + ((MultipartReport)obj).writeTo(os); + } catch (MessagingException e) { + throw new IOException(e.toString()); + } + } + } +}
diff --git a/dsn/src/main/java/com/sun/mail/dsn/package.html b/dsn/src/main/java/com/sun/mail/dsn/package.html new file mode 100644 index 0000000..0f96802 --- /dev/null +++ b/dsn/src/main/java/com/sun/mail/dsn/package.html
@@ -0,0 +1,83 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> +<HTML> +<HEAD> +<meta http-equiv="Content-Type" content="text/html; charset=utf-8"> +<!-- + + Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<TITLE>com.sun.mail.dsn package</TITLE> +</HEAD> +<BODY BGCOLOR="white"> + +<P> +Support for creating and parsing Delivery Status Notifications. +Refer to <A HREF="http://www.ietf.org/rfc/rfc3462.txt" TARGET="_top"> +RFC 3462</A> +and <A HREF="http://www.ietf.org/rfc/rfc3464.txt" TARGET="_top">RFC 3464</A> +for more information. +</P> +<P> +A Delivery Status Notification is a MIME message with a Content-Type +of <code>multipart/report</code>. +A {@link com.sun.mail.dsn.MultipartReport MultipartReport} object +represents the content of such a message. +The MultipartReport object contains several parts that represent the +information in a delivery status notification. +The first part is usually a <code>text/plain</code> part that +describes the reason for the notification. +The second part is a <code>message/delivery-status</code> part, +which is represented by a +{@link com.sun.mail.dsn.DeliveryStatus DeliveryStatus} object, and contains +details about the notification. +The third part is either an entire copy of the original message +that is returned, represented by a +{@link javax.mail.internet.MimeMessage MimeMessage} object, or +just the headers of the original message, represented by a +{@link com.sun.mail.dsn.MessageHeaders MessageHeaders} object. +</P> +<P> +To use the classes in this package, include <code>dsn.jar</code> +in your class path. +</P> +<P> +Classes in this package log debugging information using +{@link java.util.logging} as described in the following table: +</P> +<TABLE BORDER SUMMARY="com.sun.mail.dsn Loggers"> +<TR> +<TH>Logger Name</TH> +<TH>Logging Level</TH> +<TH>Purpose</TH> +</TR> + +<TR> +<TD>com.sun.mail.dsn</TD> +<TD>FINER</TD> +<TD>General debugging output</TD> +</TR> +</TABLE> + +<P> +<strong>WARNING:</strong> The APIs unique to this package should be +considered <strong>EXPERIMENTAL</strong>. They may be changed in the +future in ways that are incompatible with applications using the +current APIs. +</P> + +</BODY> +</HTML>
diff --git a/dsn/src/main/java/com/sun/mail/dsn/text_rfc822headers.java b/dsn/src/main/java/com/sun/mail/dsn/text_rfc822headers.java new file mode 100644 index 0000000..e60782f --- /dev/null +++ b/dsn/src/main/java/com/sun/mail/dsn/text_rfc822headers.java
@@ -0,0 +1,185 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.dsn; + +import java.io.*; +import java.awt.datatransfer.DataFlavor; +import javax.activation.*; +import javax.mail.*; +import javax.mail.internet.*; + +/** + * DataContentHandler for text/rfc822-headers MIME type. + * Applications should not use this class directly, it's used indirectly + * through the JavaBeans Activation Framework. + * + * @since JavaMail 1.4 + * + */ +public class text_rfc822headers implements DataContentHandler { + private static ActivationDataFlavor myDF = new ActivationDataFlavor( + MessageHeaders.class, + "text/rfc822-headers", + "RFC822 headers"); + private static ActivationDataFlavor myDFs = new ActivationDataFlavor( + java.lang.String.class, + "text/rfc822-headers", + "RFC822 headers"); + + /** + * Return the DataFlavors for this <code>DataContentHandler</code>. + * + * @return The DataFlavors + */ + public DataFlavor[] getTransferDataFlavors() { + return new DataFlavor[] { myDF, myDFs }; + } + + /** + * Return the Transfer Data of type DataFlavor from InputStream. + * + * @param df The DataFlavor + * @param ds The DataSource corresponding to the data + * @return String object + */ + public Object getTransferData(DataFlavor df, DataSource ds) + throws IOException { + // use myDF.equals to be sure to get ActivationDataFlavor.equals, + // which properly ignores Content-Type parameters in comparison + if (myDF.equals(df)) + return getContent(ds); + else if (myDFs.equals(df)) + return getStringContent(ds); + else + return null; + } + + public Object getContent(DataSource ds) throws IOException { + try { + return new MessageHeaders(ds.getInputStream()); + } catch (MessagingException mex) { +//System.out.println("Exception creating MessageHeaders: " + mex); + throw new IOException("Exception creating MessageHeaders: " + mex); + } + } + + private Object getStringContent(DataSource ds) throws IOException { + String enc = null; + InputStreamReader is = null; + + try { + enc = getCharset(ds.getContentType()); + is = new InputStreamReader(ds.getInputStream(), enc); + } catch (IllegalArgumentException iex) { + /* + * An unknown charset of the form ISO-XXX-XXX will cause + * the JDK to throw an IllegalArgumentException. The + * JDK will attempt to create a classname using this string, + * but valid classnames must not contain the character '-', + * and this results in an IllegalArgumentException, rather than + * the expected UnsupportedEncodingException. Yikes. + */ + throw new UnsupportedEncodingException(enc); + } + + try { + int pos = 0; + int count; + char buf[] = new char[1024]; + + while ((count = is.read(buf, pos, buf.length - pos)) != -1) { + pos += count; + if (pos >= buf.length) { + int size = buf.length; + if (size < 256*1024) + size += size; + else + size += 256*1024; + char tbuf[] = new char[size]; + System.arraycopy(buf, 0, tbuf, 0, pos); + buf = tbuf; + } + } + return new String(buf, 0, pos); + } finally { + try { + is.close(); + } catch (IOException ex) { + // ignore it + } + } + } + + /** + * Write the object to the output stream, using the specified MIME type. + */ + public void writeTo(Object obj, String type, OutputStream os) + throws IOException { + if (obj instanceof MessageHeaders) { + MessageHeaders mh = (MessageHeaders)obj; + try { + mh.writeTo(os); + } catch (MessagingException mex) { + Exception ex = mex.getNextException(); + if (ex instanceof IOException) + throw (IOException)ex; + else + throw new IOException("Exception writing headers: " + mex); + } + return; + } + if (!(obj instanceof String)) + throw new IOException("\"" + myDFs.getMimeType() + + "\" DataContentHandler requires String object, " + + "was given object of type " + obj.getClass().toString()); + + String enc = null; + OutputStreamWriter osw = null; + + try { + enc = getCharset(type); + osw = new OutputStreamWriter(os, enc); + } catch (IllegalArgumentException iex) { + /* + * An unknown charset of the form ISO-XXX-XXX will cause + * the JDK to throw an IllegalArgumentException. The + * JDK will attempt to create a classname using this string, + * but valid classnames must not contain the character '-', + * and this results in an IllegalArgumentException, rather than + * the expected UnsupportedEncodingException. Yikes. + */ + throw new UnsupportedEncodingException(enc); + } + + String s = (String)obj; + osw.write(s, 0, s.length()); + osw.flush(); + } + + private String getCharset(String type) { + try { + ContentType ct = new ContentType(type); + String charset = ct.getParameter("charset"); + if (charset == null) + // If the charset parameter is absent, use US-ASCII. + charset = "us-ascii"; + return MimeUtility.javaCharset(charset); + } catch (Exception ex) { + return null; + } + } +}
diff --git a/dsn/src/main/resources/META-INF/MANIFEST.MF b/dsn/src/main/resources/META-INF/MANIFEST.MF new file mode 100644 index 0000000..c24177b --- /dev/null +++ b/dsn/src/main/resources/META-INF/MANIFEST.MF
@@ -0,0 +1,9 @@ +Manifest-Version: 1.0 +Extension-Name: com.sun.mail.dsn +Specification-Title: com.sun.mail.dsn +Specification-Version: ${mail.spec.version} +Specification-Vendor: ${project.organization.name} +Implementation-Title: com.sun.mail.dsn +Implementation-Version: ${mail.version} +Implementation-Vendor: ${project.organization.name} +Implementation-Vendor-Id: com.sun
diff --git a/dsn/src/main/resources/META-INF/mailcap b/dsn/src/main/resources/META-INF/mailcap new file mode 100644 index 0000000..d42de7c --- /dev/null +++ b/dsn/src/main/resources/META-INF/mailcap
@@ -0,0 +1,8 @@ +# +# +# JavaMail content-handlers: +# +multipart/report;; x-java-content-handler=com.sun.mail.dsn.multipart_report +message/delivery-status;; x-java-content-handler=com.sun.mail.dsn.message_deliverystatus +message/disposition-notification;; x-java-content-handler=com.sun.mail.dsn.message_dispositionnotification +text/rfc822-headers;; x-java-content-handler=com.sun.mail.dsn.text_rfc822headers
diff --git a/dsn/src/test/java/com/sun/mail/dsn/MultipartReportTest.java b/dsn/src/test/java/com/sun/mail/dsn/MultipartReportTest.java new file mode 100644 index 0000000..cedafe6 --- /dev/null +++ b/dsn/src/test/java/com/sun/mail/dsn/MultipartReportTest.java
@@ -0,0 +1,58 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.dsn; + +import java.util.Properties; + +import javax.mail.Session; +import javax.mail.internet.MimeMessage; +import javax.mail.internet.InternetHeaders; + +import org.junit.*; +import static org.junit.Assert.assertTrue; + +/** + */ +public class MultipartReportTest { + + private static Session session = Session.getInstance(new Properties()); + + @Test + public void testWrongIndexBug() throws Exception { + MimeMessage msg = new MimeMessage(session); + + // create the Multipart and its parts to it + MultipartReport mp = new MultipartReport(); + mp.setText("test Multipart Report\n"); + + DeliveryStatus ds = new DeliveryStatus(); + InternetHeaders mdsn = new InternetHeaders(); + mdsn.setHeader("Reporting-MTA", "test"); + ds.setMessageDSN(mdsn); + InternetHeaders rdsn = new InternetHeaders(); + rdsn.setHeader("Final-Recipient", "joe"); + rdsn.setHeader("Action", "none"); + rdsn.setHeader("Status", "none"); + ds.addRecipientDSN(rdsn); + mp.setReport(ds); + msg.setContent(mp); + msg.saveChanges(); + msg.writeTo(new NullOutputStream()); + // anything other than an exception is success + assertTrue("MultipartReport constructed", true); + } +}
diff --git a/dsn/src/test/java/com/sun/mail/dsn/NullOutputStream.java b/dsn/src/test/java/com/sun/mail/dsn/NullOutputStream.java new file mode 100644 index 0000000..1b2a7a7 --- /dev/null +++ b/dsn/src/test/java/com/sun/mail/dsn/NullOutputStream.java
@@ -0,0 +1,34 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.dsn; + +import java.io.*; + +/** + * An OutputStream that throws away all data written to it. + */ +public class NullOutputStream extends OutputStream { + + public void write(int b) throws IOException { + } + + public void write(byte[] b) throws IOException { + } + + public void write(byte[] b, int off, int len) throws IOException { + } +}
diff --git a/gimap/exclude.xml b/gimap/exclude.xml new file mode 100644 index 0000000..e95e3a8 --- /dev/null +++ b/gimap/exclude.xml
@@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<!-- FindBugs exclude list for JavaMail gimap provider --> + +<FindBugsFilter> + <!-- + These returned arrays are never exposed to applications so it's + safe that it doesn't return a copy. + --> + <Match> + <Class name="com.sun.mail.gimap.protocol.GmailProtocol"/> + <Method name="getFetchItems"/> + <Bug pattern="EI_EXPOSE_REP"/> + </Match> + + <!-- + The setLabels method needs to be synchronized because of its + use of the protocol object. The getLabels method depends on + the getItem method to do the synchronization. + --> + <Match> + <Class name="com.sun.mail.gimap.GmailMessage"/> + <Method name="getLabels"/> + <Bug pattern="UG_SYNC_SET_UNSYNC_GET"/> + </Match> + +</FindBugsFilter>
diff --git a/gimap/pom.xml b/gimap/pom.xml new file mode 100644 index 0000000..13f25eb --- /dev/null +++ b/gimap/pom.xml
@@ -0,0 +1,90 @@ +<?xml version="1.0" encoding="iso-8859-1"?> +<!-- + + Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 + http://maven.apache.org/maven-v4_0_0.xsd"> + <parent> + <groupId>com.sun.mail</groupId> + <artifactId>all</artifactId> + <version>1.6.3</version> + </parent> + <modelVersion>4.0.0</modelVersion> + <groupId>com.sun.mail</groupId> + <artifactId>gimap</artifactId> + <packaging>jar</packaging> + <name>JavaMail API Gmail IMAP provider</name> + + <properties> + <mail.packages.export> + com.sun.mail.gimap; version=${mail.osgiversion} + </mail.packages.export> + <findbugs.exclude> + ${project.basedir}/exclude.xml + </findbugs.exclude> + </properties> + + <build> + <plugins> + <!-- + Configure SpotBugs to run with "mvn spotbugs:spotbugs" + and generate XML output that can be used by the Hudson + FindBugs plugin. + --> + <plugin> + <groupId>com.github.spotbugs</groupId> + <artifactId>spotbugs-maven-plugin</artifactId> + <configuration> + <skip>false</skip> + <threshold>${findbugs.threshold}</threshold> + <excludeFilterFile>${findbugs.exclude}</excludeFilterFile> + <findbugsXmlWithMessages>true</findbugsXmlWithMessages> + </configuration> + </plugin> + </plugins> + </build> + + <dependencies> + <dependency> + <groupId>com.sun.mail</groupId> + <artifactId>jakarta.mail</artifactId> + </dependency> + </dependencies> + + <reporting> + <plugins> + <!-- + Configure SpotBugs to run with "mvn site" and + generate html output that can be viewed directly. + --> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>findbugs-maven-plugin</artifactId> + <configuration> + <skip>false</skip> + <threshold>${findbugs.threshold}</threshold> + <!-- + <excludeFilterFile>exclude.xml</excludeFilterFile> + --> + </configuration> + </plugin> + </plugins> + </reporting> +</project>
diff --git a/gimap/src/main/java/com/sun/mail/gimap/GmailFolder.java b/gimap/src/main/java/com/sun/mail/gimap/GmailFolder.java new file mode 100644 index 0000000..cdbde00 --- /dev/null +++ b/gimap/src/main/java/com/sun/mail/gimap/GmailFolder.java
@@ -0,0 +1,210 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.gimap; + +import java.io.*; + +import javax.mail.*; +import javax.mail.internet.*; + +import com.sun.mail.iap.*; +import com.sun.mail.imap.*; +import com.sun.mail.imap.protocol.*; +import com.sun.mail.gimap.protocol.*; + +/** + * A Gmail folder. Defines new FetchProfile items and + * uses GmailMessage to store additional Gmail message attributes. + * + * @since JavaMail 1.4.6 + * @author Bill Shannon + */ + +public class GmailFolder extends IMAPFolder { + /** + * A fetch profile item for fetching headers. + * This inner class extends the <code>FetchProfile.Item</code> + * class to add new FetchProfile item types, specific to Gmail. + * + * @see FetchProfile + */ + public static class FetchProfileItem extends FetchProfile.Item { + protected FetchProfileItem(String name) { + super(name); + } + + /** + * MSGID is a fetch profile item that can be included in a + * <code>FetchProfile</code> during a fetch request to a Folder. + * This item indicates that the Gmail unique message ID for messages + * in the specified range are desired to be prefetched. <p> + * + * An example of how a client uses this is below: + * <blockquote><pre> + * + * FetchProfile fp = new FetchProfile(); + * fp.add(GmailFolder.FetchProfileItem.MSGID); + * folder.fetch(msgs, fp); + * + * </pre></blockquote> + */ + public static final FetchProfileItem MSGID = + new FetchProfileItem("X-GM-MSGID"); + + /** + * THRID is a fetch profile item that can be included in a + * <code>FetchProfile</code> during a fetch request to a Folder. + * This item indicates that the Gmail unique thread ID for messages + * in the specified range are desired to be prefetched. <p> + * + * An example of how a client uses this is below: + * <blockquote><pre> + * + * FetchProfile fp = new FetchProfile(); + * fp.add(GmailFolder.FetchProfileItem.THRID); + * folder.fetch(msgs, fp); + * + * </pre></blockquote> + */ + public static final FetchProfileItem THRID = + new FetchProfileItem("X-GM-THRID"); + + /** + * LABELS is a fetch profile item that can be included in a + * <code>FetchProfile</code> during a fetch request to a Folder. + * This item indicates that the Gmail labels for messages + * in the specified range are desired to be prefetched. <p> + * + * An example of how a client uses this is below: + * <blockquote><pre> + * + * FetchProfile fp = new FetchProfile(); + * fp.add(GmailFolder.FetchProfileItem.LABELS); + * folder.fetch(msgs, fp); + * + * </pre></blockquote> + */ + public static final FetchProfileItem LABELS = + new FetchProfileItem("X-GM-LABELS"); + } + + /** + * Set the specified labels for the given array of messages. + * + * @param msgs the messages + * @param labels the labels to add or remove + * @param set true to add, false to remove + * @exception MessagingException for failures + * @since JavaMail 1.5.5 + */ + public synchronized void setLabels(Message[] msgs, + String[] labels, boolean set) + throws MessagingException { + checkOpened(); + + if (msgs.length == 0) // boundary condition + return; + + synchronized(messageCacheLock) { + try { + IMAPProtocol ip = getProtocol(); + assert ip instanceof GmailProtocol; + GmailProtocol p = (GmailProtocol)ip; + MessageSet[] ms = Utility.toMessageSetSorted(msgs, null); + if (ms == null) + throw new MessageRemovedException( + "Messages have been removed"); + p.storeLabels(ms, labels, set); + } catch (ConnectionException cex) { + throw new FolderClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + } + } + + /** + * Set the specified labels for the given range of message numbers. + * + * @param start first message number + * @param end last message number + * @param labels the labels to add or remove + * @param set true to add, false to remove + * @exception MessagingException for failures + * @since JavaMail 1.5.5 + */ + public synchronized void setLabels(int start, int end, + String[] labels, boolean set) + throws MessagingException { + checkOpened(); + Message[] msgs = new Message[end - start + 1]; + int i = 0; + for (int n = start; n <= end; n++) + msgs[i++] = getMessage(n); + setLabels(msgs, labels, set); + } + + /** + * Set the specified labels for the given array of message numbers. + * + * @param msgnums the message numbers + * @param labels the labels to add or remove + * @param set true to add, false to remove + * @exception MessagingException for failures + * @since JavaMail 1.5.5 + */ + public synchronized void setLabels(int[] msgnums, + String[] labels, boolean set) + throws MessagingException { + checkOpened(); + Message[] msgs = new Message[msgnums.length]; + for (int i = 0; i < msgnums.length; i++) + msgs[i] = getMessage(msgnums[i]); + setLabels(msgs, labels, set); + } + + /** + * Constructor used to create a possibly non-existent folder. + * + * @param fullName fullname of this folder + * @param separator the default separator character for this + * folder's namespace + * @param store the Store + * @param isNamespace does this name represent a namespace? + */ + protected GmailFolder(String fullName, char separator, IMAPStore store, + Boolean isNamespace) { + super(fullName, separator, store, isNamespace); + } + + /** + * Constructor used to create an existing folder. + * + * @param li the ListInfo for this folder + * @param store the store containing this folder + */ + protected GmailFolder(ListInfo li, IMAPStore store) { + super(li, store); + } + + /** + * Create a new IMAPMessage object to represent the given message number. + */ + protected IMAPMessage newIMAPMessage(int msgnum) { + return new GmailMessage(this, msgnum); + } +}
diff --git a/gimap/src/main/java/com/sun/mail/gimap/GmailMessage.java b/gimap/src/main/java/com/sun/mail/gimap/GmailMessage.java new file mode 100644 index 0000000..52e1a7a --- /dev/null +++ b/gimap/src/main/java/com/sun/mail/gimap/GmailMessage.java
@@ -0,0 +1,139 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.gimap; + +import java.io.*; + +import javax.mail.*; +import javax.mail.internet.*; + +import com.sun.mail.util.*; +import com.sun.mail.iap.*; +import com.sun.mail.imap.*; +import com.sun.mail.imap.protocol.*; +import com.sun.mail.gimap.protocol.*; + +/** + * A Gmail message. Adds methods to access Gmail-specific per-message data. + * + * @since JavaMail 1.4.6 + * @author Bill Shannon + */ + +public class GmailMessage extends IMAPMessage { + /** + * Constructor. + * + * @param folder the containing folder + * @param msgnum the message sequence number + */ + protected GmailMessage(IMAPFolder folder, int msgnum) { + super(folder, msgnum); + } + + /** + * Constructor, for use by IMAPNestedMessage. + * + * @param session the Session + */ + protected GmailMessage(Session session) { + super(session); + } + + /** + * Return the Gmail unique message ID. + * + * @return the message ID + * @exception MessagingException for failures + */ + public long getMsgId() throws MessagingException { + Long msgid = (Long)getItem(GmailProtocol.MSGID_ITEM); + if (msgid != null) + return msgid.longValue(); + else + return -1; + } + + /** + * Return the Gmail unique thread ID. + * + * @return the thread ID + * @exception MessagingException for failures + */ + public long getThrId() throws MessagingException { + Long thrid = (Long)getItem(GmailProtocol.THRID_ITEM); + if (thrid != null) + return thrid.longValue(); + else + return -1; + } + + /** + * Return the Gmail labels associated with this message. + * + * @return array of labels, or empty array if none + * @exception MessagingException for failures + */ + public String[] getLabels() throws MessagingException { + String[] labels = (String[])getItem(GmailProtocol.LABELS_ITEM); + if (labels != null) + return (String[])(labels.clone()); + else + return new String[0]; + } + + /** + * Set/Unset the given labels on this message. + * + * @param labels the labels to add or remove + * @param set true to add labels, false to remove + * @exception MessagingException for failures + * @since JavaMail 1.5.5 + */ + public synchronized void setLabels(String[] labels, boolean set) + throws MessagingException { + // Acquire MessageCacheLock, to freeze seqnum. + synchronized(getMessageCacheLock()) { + try { + IMAPProtocol ip = getProtocol(); + assert ip instanceof GmailProtocol; + GmailProtocol p = (GmailProtocol)ip; + checkExpunged(); // Insure that this message is not expunged + p.storeLabels(getSequenceNumber(), labels, set); + } catch (ConnectionException cex) { + throw new FolderClosedException(folder, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + } + } + + /** + * Clear any cached labels for this message. + * The Gmail labels for a messge will be cached when first accessed + * using either the fetch method or the getLabels method. Gmail provides + * no notification when the labels have been changed by another application + * so applications may need to clear the cache if accessing the labels for + * a message more than once while the Folder is open. + * + * @since JavaMail 1.5.6 + */ + public synchronized void clearCachedLabels() { + if (items != null) + items.remove(GmailProtocol.LABELS_ITEM.getName()); + } +}
diff --git a/gimap/src/main/java/com/sun/mail/gimap/GmailMsgIdTerm.java b/gimap/src/main/java/com/sun/mail/gimap/GmailMsgIdTerm.java new file mode 100644 index 0000000..9b3a60d --- /dev/null +++ b/gimap/src/main/java/com/sun/mail/gimap/GmailMsgIdTerm.java
@@ -0,0 +1,74 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.gimap; + +import javax.mail.Message; +import javax.mail.search.StringTerm; + +/** + * This class implements searching for the Gmail message ID. + * + * @since JavaMail 1.4.6 + * @author Bill Shannon + */ + +public final class GmailMsgIdTerm extends LongTerm { + + private static final long serialVersionUID = -7090909401071626866L; + + /** + * Constructor. + * + * @param msgId the message ID + */ + public GmailMsgIdTerm(long msgId) { + super(msgId); + } + + /** + * The match method. + * + * @param msg the Message number is matched with this Message + * @return true if the match succeeds, otherwise false + */ + public boolean match(Message msg) { + long msgId; + + try { + if (msg instanceof GmailMessage) + msgId = ((GmailMessage)msg).getMsgId(); + else + return false; + } catch (Exception e) { + return false; + } + + return super.match(msgId); + } + + /** + * Equality comparison. + * + * @param obj the object to compare with + * @return true if equal + */ + public boolean equals(Object obj) { + if (!(obj instanceof GmailMsgIdTerm)) + return false; + return super.equals(obj); + } +}
diff --git a/gimap/src/main/java/com/sun/mail/gimap/GmailProvider.java b/gimap/src/main/java/com/sun/mail/gimap/GmailProvider.java new file mode 100644 index 0000000..8a56263 --- /dev/null +++ b/gimap/src/main/java/com/sun/mail/gimap/GmailProvider.java
@@ -0,0 +1,29 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.gimap; + +import javax.mail.Provider; + +/** + * The Gmail IMAP protocol provider. + */ +public class GmailProvider extends Provider { + public GmailProvider() { + super(Provider.Type.STORE, "gimap", GmailStore.class.getName(), + "Oracle", null); + } +}
diff --git a/gimap/src/main/java/com/sun/mail/gimap/GmailRawSearchTerm.java b/gimap/src/main/java/com/sun/mail/gimap/GmailRawSearchTerm.java new file mode 100644 index 0000000..6626467 --- /dev/null +++ b/gimap/src/main/java/com/sun/mail/gimap/GmailRawSearchTerm.java
@@ -0,0 +1,62 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.gimap; + +import javax.mail.Message; +import javax.mail.search.StringTerm; + +/** + * This class implements searching using the Gmail X-GM-RAW extension. + * + * @since JavaMail 1.4.6 + * @author Bill Shannon + */ + +public final class GmailRawSearchTerm extends StringTerm { + + private static final long serialVersionUID = 6284730140424242662L; + + /** + * Constructor. + * + * @param pattern the pattern to search for + */ + public GmailRawSearchTerm(String pattern) { + // Note: comparison is case-insensitive + super(pattern); + } + + /** + * The match method. + * + * @param msg the pattern match is applied to this Message's + * subject header + * @exception RuntimeException this can't be supported locally + */ + public boolean match(Message msg) { + throw new RuntimeException("GmailRawSearchTerm not supported locally"); + } + + /** + * Equality comparison. + */ + public boolean equals(Object obj) { + if (!(obj instanceof GmailRawSearchTerm)) + return false; + return super.equals(obj); + } +}
diff --git a/gimap/src/main/java/com/sun/mail/gimap/GmailSSLProvider.java b/gimap/src/main/java/com/sun/mail/gimap/GmailSSLProvider.java new file mode 100644 index 0000000..ed86523 --- /dev/null +++ b/gimap/src/main/java/com/sun/mail/gimap/GmailSSLProvider.java
@@ -0,0 +1,29 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.gimap; + +import javax.mail.Provider; + +/** + * The Gmail IMAP protocol provider. + */ +public class GmailSSLProvider extends Provider { + public GmailSSLProvider() { + super(Provider.Type.STORE, "gimaps", GmailSSLStore.class.getName(), + "Oracle", null); + } +}
diff --git a/gimap/src/main/java/com/sun/mail/gimap/GmailSSLStore.java b/gimap/src/main/java/com/sun/mail/gimap/GmailSSLStore.java new file mode 100644 index 0000000..df855f4 --- /dev/null +++ b/gimap/src/main/java/com/sun/mail/gimap/GmailSSLStore.java
@@ -0,0 +1,41 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.gimap; + +import javax.mail.*; + +/** + * Support "gimaps" protocol name. + * Unnecessary, since Gmail always uses SSL, but someone might + * expect this name to work. + * + * @since JavaMail 1.4.6 + * @author Bill Shannon + */ + +public class GmailSSLStore extends GmailStore { + /** + * Constructor that takes a Session object and a URLName that + * represents a specific IMAP server. + * + * @param session the Session + * @param url the URLName of this store + */ + public GmailSSLStore(Session session, URLName url) { + super(session, url, "gimaps", true); + } +}
diff --git a/gimap/src/main/java/com/sun/mail/gimap/GmailStore.java b/gimap/src/main/java/com/sun/mail/gimap/GmailStore.java new file mode 100644 index 0000000..fb651af --- /dev/null +++ b/gimap/src/main/java/com/sun/mail/gimap/GmailStore.java
@@ -0,0 +1,94 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.gimap; + +import java.io.IOException; + +import javax.mail.*; + +import com.sun.mail.iap.ProtocolException; +import com.sun.mail.imap.IMAPStore; +import com.sun.mail.imap.IMAPFolder; +import com.sun.mail.imap.protocol.IMAPProtocol; +import com.sun.mail.imap.protocol.ListInfo; +import com.sun.mail.gimap.protocol.GmailProtocol; + +/** + * A Gmail Store. Defaults to imap.gmail.com with SSL. + * Uses a GmailProtocol and Gmail Folder to support Gmail extensions. + * + * @since JavaMail 1.4.6 + * @author Bill Shannon + */ + +public class GmailStore extends IMAPStore { + /** + * Constructor that takes a Session object and a URLName that + * represents a specific IMAP server. + * + * @param session the Session + * @param url the URLName of this store + */ + public GmailStore(Session session, URLName url) { + this(session, url, "gimap", true); + } + + /** + * Constructor used by GmailSSLStore subclass. + * + * @param session the Session + * @param url the URLName of this store + * @param name the protocol name + * @param isSSL use SSL to connect? + */ + protected GmailStore(Session session, URLName url, + String name, boolean isSSL) { + super(session, url, name, true); // Gmail requires SSL + } + + protected boolean protocolConnect(String host, int pport, + String user, String password) + throws MessagingException { + if (host == null) + host = "imap.gmail.com"; // default to Gmail host + return super.protocolConnect(host, pport, user, password); + } + + protected IMAPProtocol newIMAPProtocol(String host, int port) + throws IOException, ProtocolException { + return new GmailProtocol(name, host, port, + session.getProperties(), + isSSL, + logger + ); + } + + /** + * Create an IMAPFolder object. + */ + protected IMAPFolder newIMAPFolder(String fullName, char separator, + Boolean isNamespace) { + return new GmailFolder(fullName, separator, this, isNamespace); + } + + /** + * Create an IMAPFolder object. + */ + protected IMAPFolder newIMAPFolder(ListInfo li) { + return new GmailFolder(li, this); + } +}
diff --git a/gimap/src/main/java/com/sun/mail/gimap/GmailThrIdTerm.java b/gimap/src/main/java/com/sun/mail/gimap/GmailThrIdTerm.java new file mode 100644 index 0000000..9101d73 --- /dev/null +++ b/gimap/src/main/java/com/sun/mail/gimap/GmailThrIdTerm.java
@@ -0,0 +1,74 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.gimap; + +import javax.mail.Message; +import javax.mail.search.StringTerm; + +/** + * This class implements searching for the Gmail thread ID. + * + * @since JavaMail 1.4.6 + * @author Bill Shannon + */ + +public final class GmailThrIdTerm extends LongTerm { + + private static final long serialVersionUID = -4549268502183739744L; + + /** + * Constructor. + * + * @param thrId the thread ID + */ + public GmailThrIdTerm(long thrId) { + super(thrId); + } + + /** + * The match method. + * + * @param msg the Message number is matched with this Message + * @return true if the match succeeds, otherwise false + */ + public boolean match(Message msg) { + long thrId; + + try { + if (msg instanceof GmailMessage) + thrId = ((GmailMessage)msg).getThrId(); + else + return false; + } catch (Exception e) { + return false; + } + + return super.match(thrId); + } + + /** + * Equality comparison. + * + * @param obj the object to compare with + * @return true if equal + */ + public boolean equals(Object obj) { + if (!(obj instanceof GmailThrIdTerm)) + return false; + return super.equals(obj); + } +}
diff --git a/gimap/src/main/java/com/sun/mail/gimap/LongTerm.java b/gimap/src/main/java/com/sun/mail/gimap/LongTerm.java new file mode 100644 index 0000000..2ec189d --- /dev/null +++ b/gimap/src/main/java/com/sun/mail/gimap/LongTerm.java
@@ -0,0 +1,71 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.gimap; + +import javax.mail.search.SearchTerm; + +/** + * This class implements a long integer search term. + * + * @since JavaMail 1.4.6 + * @author Bill Shannon + */ + +abstract class LongTerm extends SearchTerm { + /** + * The number. + * + * @serial + */ + protected long number; + + private static final long serialVersionUID = 5285147193246128043L; + + protected LongTerm(long number) { + this.number = number; + } + + /** + * Return the number to compare with. + * + * @return the number + */ + public long getNumber() { + return number; + } + + protected boolean match(long i) { + return i == number; + } + + /** + * Equality comparison. + */ + public boolean equals(Object obj) { + if (!(obj instanceof LongTerm)) + return false; + LongTerm t = (LongTerm)obj; + return t.number == this.number && super.equals(obj); + } + + /** + * Compute a hashCode for this object. + */ + public int hashCode() { + return (int)number + super.hashCode(); + } +}
diff --git a/gimap/src/main/java/com/sun/mail/gimap/package.html b/gimap/src/main/java/com/sun/mail/gimap/package.html new file mode 100644 index 0000000..9161372 --- /dev/null +++ b/gimap/src/main/java/com/sun/mail/gimap/package.html
@@ -0,0 +1,105 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> +<HTML> +<HEAD> +<meta http-equiv="Content-Type" content="text/html; charset=utf-8"> +<!-- + + Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<TITLE>com.sun.mail.gimap package</TITLE> +</HEAD> +<BODY BGCOLOR="white"> + +<P> +An EXPERIMENTAL IMAP protocol provider that supports the +<A HREF="https://developers.google.com/google-apps/gmail/imap_extensions" +TARGET="_top"> +Gmail-specific IMAP protocol extensions +</A>. +This provider supports all the features of the IMAP provider, plus +additional Gmail-specific features. +This provider can be used by including gimap.jar in your CLASSPATH +along with mail.jar, and by using the "gimap" protocol instead of +the "imap" protocol. +Remember that this means that all properties should be named "mail.gimap.*", +but that otherwise this provider supports all the same properties as the +IMAP protocol provider. +The Gmail IMAP provider defaults to using SSL to connect to "imap.gmail.com". +</P> +<P> +In general, applications should not need to use the classes in this +package directly. Instead, they should use the APIs defined by the +<code>javax.mail</code> package (and subpackages). Applications should +never construct instances of <code>GmailStore</code> or +<code>GmailFolder</code> directly. Instead, they should use the +<code>Session</code> method <code>getStore</code> to acquire an +appropriate <code>Store</code> object, and from that acquire +<code>Folder</code> objects. +</P> +<P> +Message objects returned by this provider may be cast to GmailMessage +to access Gmail-specific data, e.g., using the methods GmailMessage.getMsgId(), +GmailMessage.getThrId(), and GmailMessage.getLabels(). +For example: +</P> +<PRE> + GmailMessage gmsg = (GmailMessage)msg; + System.out.println("Gmail message ID is " + gmsg.getMsgId()); + String[] labels = gmsg.getLabels(); + for (String s : labels) + System.out.println("Gmail message label: " + s); +</PRE> +<P> +Gmail-specific data may be prefetched using the GmailFolder.FetchProfileItems +MSGID, THRID, and LABELS. +For example: +</P> +<PRE> + FetchProfile fp = new FetchProfile(); + fp.add(GmailFolder.FetchProfileItem.MSGID); + folder.fetch(fp); +</PRE> +<P> +You can search using Gmail-specific data using the GmailMsgIdTerm, +GmailThrIdTerm, and GmailRawSearchTerm search terms. +For example: +</P> +<PRE> + // find the message with this Gmail unique message ID + long msgid = ...; + Message[] msgs = folder.search(new GmailMsgIdTerm(msgid)); +</PRE> +<P> +You can access the Gmail extended attributes (returned by XLIST) for a +folder using the IMAPFolder.getAttributes() method. +For example: +</P> +<PRE> + IMAPFolder ifolder = (IMAPFolder)folder; + String[] attrs = ifolder.getAttributes(); + for (String s : attrs) + System.out.println("Folder attribute: " + s); +</PRE> +<P> +<strong>WARNING:</strong> The APIs unique to this package should be +considered <strong>EXPERIMENTAL</strong>. They may be changed in the +future in ways that are incompatible with applications using the +current APIs. +</P> + +</BODY> +</HTML>
diff --git a/gimap/src/main/java/com/sun/mail/gimap/protocol/GmailProtocol.java b/gimap/src/main/java/com/sun/mail/gimap/protocol/GmailProtocol.java new file mode 100644 index 0000000..c3e3fb1 --- /dev/null +++ b/gimap/src/main/java/com/sun/mail/gimap/protocol/GmailProtocol.java
@@ -0,0 +1,190 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.gimap.protocol; + +import java.io.*; +import java.util.*; +import java.nio.charset.StandardCharsets; + +import com.sun.mail.iap.*; +import com.sun.mail.imap.protocol.*; +import com.sun.mail.gimap.GmailFolder.FetchProfileItem; + +import com.sun.mail.util.MailLogger; + +/** + * Extend IMAP support to handle Gmail-specific protocol extensions. + * + * @since JavaMail 1.4.6 + * @author Bill Shannon + */ + +public class GmailProtocol extends IMAPProtocol { + + /* + * Define the Gmail-specific FETCH items. + */ + public static final FetchItem MSGID_ITEM = + new FetchItem("X-GM-MSGID", FetchProfileItem.MSGID) { + public Object parseItem(FetchResponse r) { + return Long.valueOf(r.readLong()); + } + }; + public static final FetchItem THRID_ITEM = + new FetchItem("X-GM-THRID", FetchProfileItem.THRID) { + public Object parseItem(FetchResponse r) { + return Long.valueOf(r.readLong()); + } + }; + public static final FetchItem LABELS_ITEM = + new FetchItem("X-GM-LABELS", FetchProfileItem.LABELS) { + public Object parseItem(FetchResponse r) { + return r.readAtomStringList(); + } + }; + + private static final FetchItem[] myFetchItems = { + MSGID_ITEM, + THRID_ITEM, + LABELS_ITEM + }; + + private FetchItem[] fetchItems = null; + + /** + * Connect to Gmail. + * + * @param name the protocol name + * @param host host to connect to + * @param port portnumber to connect to + * @param props Properties object used by this protocol + * @param isSSL use SSL? + * @param logger for log messages + * @exception IOException for I/O errors + * @exception ProtocolException for protocol failures + */ + public GmailProtocol(String name, String host, int port, + Properties props, boolean isSSL, MailLogger logger) + throws IOException, ProtocolException { + super(name, host, port, props, isSSL, logger); + + // check to see if this is really Gmail + if (!hasCapability("X-GM-EXT-1")) { + logger.fine("WARNING! Not connected to Gmail!"); + // XXX - could call "disconnect()" here and make this a fatal error + } else { + logger.fine("connected to Gmail"); + } + } + + /** + * Return the additional fetch items supported by the Gmail protocol. + * Combines our fetch items with those supported by the superclass. + */ + public FetchItem[] getFetchItems() { + if (fetchItems != null) + return fetchItems; + FetchItem[] sfi = super.getFetchItems(); + if (sfi == null || sfi.length == 0) + fetchItems = myFetchItems; + else { + fetchItems = new FetchItem[sfi.length + myFetchItems.length]; + System.arraycopy(sfi, 0, fetchItems, 0, sfi.length); + System.arraycopy(myFetchItems, 0, fetchItems, sfi.length, + myFetchItems.length); + } + return fetchItems; + } + + /** + * Set the specified labels on this message. + * + * @param msgsets the message sets + * @param labels the labels + * @param set true to set, false to clear + * @exception ProtocolException for protocol failures + * @since JavaMail 1.5.5 + */ + public void storeLabels(MessageSet[] msgsets, String[] labels, boolean set) + throws ProtocolException { + storeLabels(MessageSet.toString(msgsets), labels, set); + } + + /** + * Set the specified labels on this message. + * + * @param start the first message number + * @param end the last message number + * @param labels the labels + * @param set true to set, false to clear + * @exception ProtocolException for protocol failures + * @since JavaMail 1.5.5 + */ + public void storeLabels(int start, int end, String[] labels, boolean set) + throws ProtocolException { + storeLabels(String.valueOf(start) + ":" + String.valueOf(end), + labels, set); + } + + /** + * Set the specified labels on this message. + * + * @param msg the message number + * @param labels the labels + * @param set true to set, false to clear + * @exception ProtocolException for protocol failures + * @since JavaMail 1.5.5 + */ + public void storeLabels(int msg, String[] labels, boolean set) + throws ProtocolException { + storeLabels(String.valueOf(msg), labels, set); + } + + private void storeLabels(String msgset, String[] labels, boolean set) + throws ProtocolException { + Response[] r; + if (set) + r = command("STORE " + msgset + " +X-GM-LABELS", + createLabelList(labels)); + else + r = command("STORE " + msgset + " -X-GM-LABELS", + createLabelList(labels)); + + // Dispatch untagged responses + notifyResponseHandlers(r); + handleResult(r[r.length-1]); + } + + // XXX - assume Gmail always supports UTF-8 + private Argument createLabelList(String[] labels) { + Argument args = new Argument(); + Argument itemArgs = new Argument(); + for (int i = 0, len = labels.length; i < len; i++) + itemArgs.writeString(labels[i], StandardCharsets.UTF_8); + args.writeArgument(itemArgs); + return args; + } + + /** + * Return a GmailSearchSequence. + */ + protected SearchSequence getSearchSequence() { + if (searchSequence == null) + searchSequence = new GmailSearchSequence(this); + return searchSequence; + } +}
diff --git a/gimap/src/main/java/com/sun/mail/gimap/protocol/GmailSearchSequence.java b/gimap/src/main/java/com/sun/mail/gimap/protocol/GmailSearchSequence.java new file mode 100644 index 0000000..8689726 --- /dev/null +++ b/gimap/src/main/java/com/sun/mail/gimap/protocol/GmailSearchSequence.java
@@ -0,0 +1,76 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.gimap.protocol; + +import java.io.*; + +import javax.mail.*; +import javax.mail.internet.*; +import javax.mail.search.*; + +import com.sun.mail.iap.*; +import com.sun.mail.imap.protocol.*; +import com.sun.mail.gimap.*; + +/** + * Support Gmail-specific search extensions. + * + * @since JavaMail 1.4.6 + * @author Bill Shannon + */ + +public class GmailSearchSequence extends SearchSequence { + public GmailSearchSequence(IMAPProtocol p) { + super(p); + } + + public Argument generateSequence(SearchTerm term, String charset) + throws SearchException, IOException { + if (term instanceof GmailMsgIdTerm) + return gmailMsgidSearch((GmailMsgIdTerm)term); + else if (term instanceof GmailThrIdTerm) + return gmailThridSearch((GmailThrIdTerm)term); + else if (term instanceof GmailRawSearchTerm) + return gmailRawSearch((GmailRawSearchTerm)term, charset); + else + return super.generateSequence(term, charset); + } + + protected Argument gmailMsgidSearch(GmailMsgIdTerm term) + throws IOException { + Argument result = new Argument(); + result.writeAtom("X-GM-MSGID"); + result.writeNumber(term.getNumber()); + return result; + } + + protected Argument gmailThridSearch(GmailThrIdTerm term) + throws IOException { + Argument result = new Argument(); + result.writeAtom("X-GM-THRID"); + result.writeNumber(term.getNumber()); + return result; + } + + protected Argument gmailRawSearch(GmailRawSearchTerm term, String charset) + throws IOException { + Argument result = new Argument(); + result.writeAtom("X-GM-RAW"); + result.writeString(term.getPattern(), charset); + return result; + } +}
diff --git a/gimap/src/main/java/module-info.java b/gimap/src/main/java/module-info.java new file mode 100644 index 0000000..cda246e --- /dev/null +++ b/gimap/src/main/java/module-info.java
@@ -0,0 +1,27 @@ +/* + * Copyright (c) 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +module com.sun.mail.gimap { + exports com.sun.mail.gimap; + exports com.sun.mail.gimap.protocol; + provides javax.mail.Provider with + com.sun.mail.gimap.GmailProvider, com.sun.mail.gimap.GmailSSLProvider; + + requires jakarta.mail; + requires java.logging; + requires java.security.sasl; + requires com.sun.mail.imap; +}
diff --git a/gimap/src/main/resources/META-INF/MANIFEST.MF b/gimap/src/main/resources/META-INF/MANIFEST.MF new file mode 100644 index 0000000..6ccf342 --- /dev/null +++ b/gimap/src/main/resources/META-INF/MANIFEST.MF
@@ -0,0 +1,9 @@ +Manifest-Version: 1.0 +Extension-Name: com.sun.mail.gimap +Specification-Title: com.sun.mail.gimap +Specification-Version: ${mail.spec.version} +Specification-Vendor: ${project.organization.name} +Implementation-Title: com.sun.mail.gimap +Implementation-Version: ${mail.version} +Implementation-Vendor: ${project.organization.name} +Implementation-Vendor-Id: com.sun
diff --git a/gimap/src/main/resources/META-INF/javamail.providers b/gimap/src/main/resources/META-INF/javamail.providers new file mode 100644 index 0000000..e9d3890 --- /dev/null +++ b/gimap/src/main/resources/META-INF/javamail.providers
@@ -0,0 +1,2 @@ +protocol=gimap; type=store; class=com.sun.mail.gimap.GmailStore; vendor=Oracle; +protocol=gimaps; type=store; class=com.sun.mail.gimap.GmailSSLStore; vendor=Oracle;
diff --git a/gimap/src/main/resources/META-INF/services/javax.mail.Provider b/gimap/src/main/resources/META-INF/services/javax.mail.Provider new file mode 100644 index 0000000..bf49a14 --- /dev/null +++ b/gimap/src/main/resources/META-INF/services/javax.mail.Provider
@@ -0,0 +1,2 @@ +com.sun.mail.gimap.GmailProvider +com.sun.mail.gimap.GmailSSLProvider
diff --git a/imap/pom.xml b/imap/pom.xml new file mode 100644 index 0000000..d45590f --- /dev/null +++ b/imap/pom.xml
@@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="iso-8859-1"?> +<!-- + + Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 + http://maven.apache.org/maven-v4_0_0.xsd"> + <parent> + <groupId>com.sun.mail</groupId> + <artifactId>parent-distrib</artifactId> + <version>1.6.3</version> + <relativePath>../parent-distrib/pom.xml</relativePath> + </parent> + <modelVersion>4.0.0</modelVersion> + <groupId>com.sun.mail</groupId> + <artifactId>imap</artifactId> + <packaging>jar</packaging> + <name>JavaMail API imap provider</name> + + <properties> + <mail.extraClasses>com/sun/mail/iap/**</mail.extraClasses> + <mail.packages.export> + com.sun.mail.imap; version=${mail.osgiversion}, + com.sun.mail.imap.protocol; version=${mail.osgiversion}, + com.sun.mail.iap; version=${mail.osgiversion} + </mail.packages.export> + </properties> +</project>
diff --git a/imap/src/main/java/module-info.java b/imap/src/main/java/module-info.java new file mode 100644 index 0000000..37a3c34 --- /dev/null +++ b/imap/src/main/java/module-info.java
@@ -0,0 +1,27 @@ +/* + * Copyright (c) 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +module com.sun.mail.imap { + exports com.sun.mail.iap; + exports com.sun.mail.imap; + exports com.sun.mail.imap.protocol; + provides javax.mail.Provider with + com.sun.mail.imap.IMAPProvider, com.sun.mail.imap.IMAPSSLProvider; + + requires jakarta.mail; + requires java.logging; + requires java.security.sasl; +}
diff --git a/imap/src/main/resources/META-INF/MANIFEST.MF b/imap/src/main/resources/META-INF/MANIFEST.MF new file mode 100644 index 0000000..64f8156 --- /dev/null +++ b/imap/src/main/resources/META-INF/MANIFEST.MF
@@ -0,0 +1,9 @@ +Manifest-Version: 1.0 +Extension-Name: com.sun.mail.imap +Specification-Title: com.sun.mail.imap +Specification-Version: ${mail.spec.version} +Specification-Vendor: ${project.organization.name} +Implementation-Title: com.sun.mail.imap +Implementation-Version: ${mail.version} +Implementation-Vendor: ${project.organization.name} +Implementation-Vendor-Id: com.sun
diff --git a/imap/src/main/resources/META-INF/javamail.providers b/imap/src/main/resources/META-INF/javamail.providers new file mode 100644 index 0000000..305dfa5 --- /dev/null +++ b/imap/src/main/resources/META-INF/javamail.providers
@@ -0,0 +1,3 @@ +# JavaMail IMAP provider Oracle +protocol=imap; type=store; class=com.sun.mail.imap.IMAPStore; vendor=Oracle; +protocol=imaps; type=store; class=com.sun.mail.imap.IMAPSSLStore; vendor=Oracle;
diff --git a/imap/src/main/resources/META-INF/services/javax.mail.Provider b/imap/src/main/resources/META-INF/services/javax.mail.Provider new file mode 100644 index 0000000..932e992 --- /dev/null +++ b/imap/src/main/resources/META-INF/services/javax.mail.Provider
@@ -0,0 +1,2 @@ +com.sun.mail.imap.IMAPProvider +com.sun.mail.imap.IMAPSSLProvider
diff --git a/javadoc/pom.xml b/javadoc/pom.xml new file mode 100644 index 0000000..8aeaed3 --- /dev/null +++ b/javadoc/pom.xml
@@ -0,0 +1,205 @@ +<?xml version="1.0" encoding="iso-8859-1"?> +<!-- + + Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 + http://maven.apache.org/maven-v4_0_0.xsd"> + <parent> + <groupId>com.sun.mail</groupId> + <artifactId>all</artifactId> + <version>1.6.3</version> + </parent> + <modelVersion>4.0.0</modelVersion> + <groupId>com.sun.mail</groupId> + <artifactId>javadoc</artifactId> + <packaging>pom</packaging> + <version>1.6.3</version> + <name>JavaMail API javadocs</name> + <description>${project.name}</description> + + <build> + <plugins> + <!-- + To allow us to generate javadocs that only include some + classes in certain packages, we need to copy the sources + to another location and run javadoc against that subset + of the sources. This ant task does the copy. + --> + <plugin> + <artifactId>maven-antrun-plugin</artifactId> + <inherited>false</inherited> + <executions> + <execution> + <phase>package</phase> + <configuration> + <tasks> + + <copy todir="target/javadoc"> + <fileset dir="../mail/src/main/java"> + <include name="**/*.html"/> + <include name="javax/mail/**"/> + </fileset> + <fileset dir="../mail/src/main/java" + includes=" + com/sun/mail/imap/package.html, + com/sun/mail/imap/IMAPFolder.java, + com/sun/mail/imap/IMAPMessage.java, + com/sun/mail/imap/IMAPStore.java, + com/sun/mail/imap/IMAPSSLStore.java + com/sun/mail/imap/ACL.java, + com/sun/mail/imap/Rights.java, + com/sun/mail/imap/Quota.java, + com/sun/mail/imap/SortTerm.java, + com/sun/mail/imap/ResyncData.java, + com/sun/mail/imap/OlderTerm.java, + com/sun/mail/imap/YoungerTerm.java, + com/sun/mail/imap/MessageVanishedEvent.java, + com/sun/mail/imap/ModifiedSinceTerm.java, + com/sun/mail/imap/IdleManager.java, + com/sun/mail/imap/ReferralException.java, + com/sun/mail/pop3/POP3Store.java, + com/sun/mail/pop3/POP3SSLStore.java, + com/sun/mail/pop3/POP3Folder.java, + com/sun/mail/pop3/POP3Message.java, + com/sun/mail/smtp/SMTPMessage.java, + com/sun/mail/smtp/SMTPAddressFailedException.java, + com/sun/mail/smtp/SMTPAddressSucceededException.java, + com/sun/mail/smtp/SMTPSendFailedException.java, + com/sun/mail/smtp/SMTPSenderFailedException.java, + com/sun/mail/smtp/SMTPTransport.java, + com/sun/mail/smtp/SMTPSSLTransport.java, + com/sun/mail/util/MailConnectException.java, + com/sun/mail/util/MailSSLSocketFactory.java, + com/sun/mail/util/ReadableMime.java, + com/sun/mail/util/logging/*.java + "/> + <fileset dir="../dsn/src/main/java" + includes=" + com/sun/mail/dsn/package.html, + com/sun/mail/dsn/DeliveryStatus.java, + com/sun/mail/dsn/DispositionNotification.java, + com/sun/mail/dsn/MessageHeaders.java, + com/sun/mail/dsn/MultipartReport.java, + com/sun/mail/dsn/Report.java + "/> + <fileset dir="../gimap/src/main/java" + includes=" + com/sun/mail/gimap/package.html, + com/sun/mail/gimap/*.java + "/> + </copy> + + </tasks> + </configuration> + <goals> + <goal>run</goal> + </goals> + </execution> + </executions> + </plugin> + + <plugin> + <artifactId>maven-javadoc-plugin</artifactId> + <inherited>false</inherited> + <executions> + <execution> + <phase>package</phase> + <goals> + <goal>javadoc</goal> + </goals> + <configuration> + <additionalJOption>-J-Xmx256m</additionalJOption> + <author>false</author> + <description> + JavaMail API documentation + </description> + <doctitle> + JavaMail API documentation + </doctitle> + <windowtitle> + JavaMail API documentation + </windowtitle> + <splitindex>true</splitindex> + <use>true</use> + <notimestamp>true</notimestamp> + <serialwarn>true</serialwarn> + <quiet>true</quiet> + <overview> + ${basedir}/target/javadoc/overview.html + </overview> + <bottom> +<![CDATA[Copyright © 1996-2018, + <a href="http://www.oracle.com">Oracle</a> + and/or its affiliates. All Rights Reserved. + Use is subject to + <a href="{@docRoot}/doc-files/speclicense.html" target="_top">license terms</a>. +]]> + </bottom> + <groups> + <group> + <title>JavaMail API Packages</title> + <packages>javax.*</packages> + </group> + <group> + <title>RI-specific Packages</title> + <packages>com.sun.*</packages> + </group> + </groups> + <subpackages> +javax:com.sun.mail.imap:com.sun.mail.pop3:com.sun.mail.smtp:com.sun.mail.dsn:com.sun.mail.util:com.sun.mail.gimap + </subpackages> + <excludePackageNames> + com.sun.mail.imap.protocol:com.sun.mail.gimap.protocol + </excludePackageNames> + <sourcepath>${basedir}/target/javadoc</sourcepath> + <!-- + Links to Java SE javadocs. + + XXX - links need to include a trailing "/." + because Maven strips off a trailing "/" + before passing the option to the javadoc + command, which then strips off the last + name if it doesn't end with "/". + --> + <links> + <link>http://docs.oracle.com/javase/7/docs/api/.</link> + </links> + </configuration> + </execution> + </executions> + </plugin> + </plugins> + </build> + + <dependencies> + <dependency> + <groupId>com.sun.mail</groupId> + <artifactId>jakarta.mail</artifactId> + </dependency> + <dependency> + <groupId>com.sun.mail</groupId> + <artifactId>dsn</artifactId> + </dependency> + <dependency> + <groupId>com.sun.mail</groupId> + <artifactId>gimap</artifactId> + </dependency> + </dependencies> +</project>
diff --git a/logging/pom.xml b/logging/pom.xml new file mode 100644 index 0000000..5471b8a --- /dev/null +++ b/logging/pom.xml
@@ -0,0 +1,74 @@ +<?xml version="1.0" encoding="iso-8859-1"?> +<!-- + + Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 + http://maven.apache.org/maven-v4_0_0.xsd"> + <parent> + <groupId>com.sun.mail</groupId> + <artifactId>all</artifactId> + <version>1.6.3</version> + </parent> + <modelVersion>4.0.0</modelVersion> + <groupId>com.sun.mail</groupId> + <artifactId>logging</artifactId> + <packaging>jar</packaging> + <name>JavaMail API logging demo</name> + + <build> + <plugins> + <!-- + Need to disable the maven-bundle-plugin because the + new version doesn't like classes in the default package. + --> + <plugin> + <groupId>org.apache.felix</groupId> + <artifactId>maven-bundle-plugin</artifactId> + <executions> + <execution> + <id>osgi-manifest</id> + <phase>none</phase> + </execution> + </executions> + </plugin> + + <!-- + Because the maven-jar-plugin depends on the manifest file + created by the maven-bundle-plugin, we need to disable it too. + --> + <plugin> + <artifactId>maven-jar-plugin</artifactId> + <executions> + <execution> + <id>default-jar</id> + <phase>none</phase> + </execution> + </executions> + </plugin> + </plugins> + </build> + + <dependencies> + <dependency> + <groupId>com.sun.mail</groupId> + <artifactId>jakarta.mail</artifactId> + </dependency> + </dependencies> +</project>
diff --git a/logging/src/main/java/FileErrorManager.java b/logging/src/main/java/FileErrorManager.java new file mode 100644 index 0000000..e91a246 --- /dev/null +++ b/logging/src/main/java/FileErrorManager.java
@@ -0,0 +1,308 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2009, 2018 Jason Mehrens. All Rights Reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +import java.io.*; +import java.lang.reflect.Constructor; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.logging.ErrorManager; +import java.util.logging.Level; +import java.util.logging.LogManager; + +/** + * An error manager used to store mime messages from the <tt>MailHandler</tt> + * to the file system when the email server is unavailable or unreachable. The + * code to manually setup this error manager can be as simple as the following: + * <pre> + * File dir = new File("path to dir"); + * FileErrorManager em = new FileErrorManager(dir); + * </pre> + * + * <p> + * <b>Configuration:</b> + * The code to setup this error manager via the logging properties can be as + * simple as the following: + * <pre> + * #Default FileErrorManager settings. + * FileErrorManager.pattern = path to directory + * </pre> + * + * If properties are not defined, or contain invalid values, then the specified + * default values are used. + * <ul> + * <li>FileErrorManager.pattern the absolute file path to the directory which + * will store any failed email messages. (defaults to the value of the system + * property <tt>java.io.tmpdir</tt>) + * </ul> + * + * @author Jason Mehrens + */ +public class FileErrorManager extends ErrorManager { + + /** + * Stores the LogManager. + */ + private static final LogManager manager = LogManager.getLogManager(); + /** + * Used to report errors that this error manager fails to report. + */ + private final ErrorManager next = new ErrorManager(); + /** + * Directory of the email store. + */ + private final File emailStore; + + /** + * Creates a new error manager. Files are stored in the users temp + * directory. + * + * @exception SecurityException if unable to access system properties or if + * a security manager is present and unable to read or write to users temp + * directory. + */ + public FileErrorManager() { + this.emailStore = getEmailStore(); + init(); + } + + /** + * Creates a new error manager. + * + * @param dir a directory to store the email files. + * @throws NullPointerException if <tt>dir</tt> is <tt>null</tt> + * @throws IllegalArgumentException if <tt>dir</tt> is a + * <tt>java.io.File</tt> subclass, not a directory, or is not an absolute + * path. + * @throws SecurityException if a security manager is present and unable to + * read or write to a given directory. + */ + public FileErrorManager(File dir) { + this.emailStore = dir; + init(); + } + + /** + * If the message parameter is a raw email, and passes the store term, then + * this method will store the email to the file system. If the message + * parameter is not a raw email then the message is forwarded to the super + * class. If an email is written to the file system without error, then the + * original reported error is ignored. + * + * @param msg String raw email or plain error message. + * @param ex Exception that occurred in the mail handler. + * @param code int error manager code. + */ + @Override + public void error(String msg, Exception ex, int code) { + if (isRawEmail(msg)) { + try { + storeEmail(msg); + } catch (final IOException | RuntimeException IOE) { + next.error(msg, ex, code); + super.error(emailStore.toString(), IOE, ErrorManager.GENERIC_FAILURE); + } + } else { + next.error(msg, ex, code); + } + } + + /** + * Performs the initialization for this object. + */ + private void init() { + if (next == null) { + throw new NullPointerException(ErrorManager.class.getName()); + } + + File dir = this.emailStore; + if (dir.getClass() != File.class) { //For security reasons. + throw new IllegalArgumentException(dir.getClass().getName()); + } + + if (!dir.isDirectory()) { + throw new IllegalArgumentException("File must be a directory."); + } + + if (!dir.canWrite()) { //Can throw under a security manager. + super.error(dir.getAbsolutePath(), + new SecurityException("write"), ErrorManager.OPEN_FAILURE); + } + + //For now, only absolute paths are allowed. + if (!dir.isAbsolute()) { + throw new IllegalArgumentException("Only absolute paths are allowed."); + } + + if (!dir.canRead()) { //Can throw under a security manager. + super.error(dir.getAbsolutePath(), + new SecurityException("read"), ErrorManager.OPEN_FAILURE); + } + } + + /** + * Creates a common temp file prefix. + * + * @return the file prefix. + */ + private String prefixName() { + return "FileErrorManager"; + } + + /** + * Creates a common temp file suffix. + * + * @return the file suffix. + */ + private String suffixName() { + return ".eml"; + } + + /** + * Determines if the given message is a MIME message or just free text. + * + * @param msg the message to examine. + * @return true if MIME message otherwise false. + */ + private boolean isRawEmail(String msg) { + if (msg != null && msg.length() > 0) { + return !msg.startsWith(Level.SEVERE.getName()); + } + return false; + } + + /** + * Stores the given string in a file. + * + * @param email the message to store. + * @throws IOException if there is a problem. + */ + private void storeEmail(String email) throws IOException { + File tmp = null; + FileOutputStream out = null; + for (;;) { + tmp = File.createTempFile(prefixName(), suffixName(), emailStore); + try { + out = new FileOutputStream(tmp); + break; + } catch (FileNotFoundException FNFE) { + if (!tmp.exists()) { //retry if file is locked + throw FNFE; + } + } + } + + try (PrintStream ps = new PrintStream(wrap(out), false, "UTF-8")) { + ps.print(email); + ps.flush(); + tmp = null; //Don't delete 'tmp' if all bytes were written. + } finally { + close(out); + delete(tmp); //Only deletes if not null. + } + } + + /** + * Null safe close method. + * + * @param out closes the given stream. + */ + private void close(OutputStream out) { + if (out != null) { + try { + out.close(); + } catch (IOException IOE) { + super.error(out.toString(), IOE, ErrorManager.CLOSE_FAILURE); + } + } + } + + /** + * Null safe delete method. + * + * @param tmp the file to delete. + */ + private void delete(File tmp) { + if (tmp != null) { + try { + if (!tmp.delete() && tmp.exists()) { + try { + try { + tmp.deleteOnExit(); + } catch (final LinkageError shutdown) { + throw new RuntimeException(shutdown); + } + } catch (final RuntimeException shutdown) { + if (!tmp.delete()) { + super.error(tmp.getAbsolutePath(), shutdown, + ErrorManager.CLOSE_FAILURE); + } + } + } + } catch (SecurityException SE) { + super.error(tmp.toString(), SE, ErrorManager.CLOSE_FAILURE); + } + } + } + + /** + * Gets the location of the email store. + * + * @return the File location. + */ + private File getEmailStore() { + String dir = manager.getProperty( + getClass().getName().concat(".pattern")); + if (dir == null) { + dir = AccessController.doPrivileged(new PrivilegedAction<String>() { + + @Override + public String run() { + return System.getProperty("java.io.tmpdir", "."); + } + }); + } + return new File(dir); + } + + /** + * Wraps the given stream as a NewLineOutputStream. + * + * @param out the stream to wrap. + * @return the original or wrapped output stream. + */ + @SuppressWarnings("UseSpecificCatch") + private OutputStream wrap(OutputStream out) { + assert out != null; + Class<?> k; + try { + k = Class.forName("NewlineOutputStream"); + if (OutputStream.class.isAssignableFrom(k)) { + Constructor<?> c = k.getConstructor(OutputStream.class); + return (OutputStream) c.newInstance(out); + } else { + super.error("Unable to switch newlines", + new ClassNotFoundException(k.getName()), + ErrorManager.GENERIC_FAILURE); + } + } catch (RuntimeException re) { + super.error("Unable to switch newlines", + re, ErrorManager.GENERIC_FAILURE); + } catch (Exception ex) { + super.error("Unable to switch newlines", + ex, ErrorManager.GENERIC_FAILURE); + } catch (LinkageError le) { + super.error("Unable to switch newlines", + new ClassNotFoundException("", le), + ErrorManager.GENERIC_FAILURE); + } + return out; + } +}
diff --git a/logging/src/main/java/MailHandlerDemo.java b/logging/src/main/java/MailHandlerDemo.java new file mode 100644 index 0000000..22c918f --- /dev/null +++ b/logging/src/main/java/MailHandlerDemo.java
@@ -0,0 +1,647 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2009, 2018 Jason Mehrens. All Rights Reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +import com.sun.mail.util.logging.CollectorFormatter; +import com.sun.mail.util.logging.DurationFilter; +import com.sun.mail.util.logging.MailHandler; +import com.sun.mail.util.logging.SeverityComparator; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.PrintStream; +import java.lang.management.ManagementFactory; +import java.util.*; +import java.util.logging.*; +import javax.activation.DataHandler; +import javax.mail.MessagingException; +import javax.mail.Session; +import javax.mail.internet.InternetAddress; + +/** + * Demo for the different configurations for the MailHandler. If the logging + * properties file or class is not specified then this demo will apply some + * default settings to store emails in the user's temp directory. + * + * @author Jason Mehrens + */ +public class MailHandlerDemo { + + /** + * This class name. + */ + private static final String CLASS_NAME = MailHandlerDemo.class.getName(); + /** + * The logger for this class name. + */ + private static final Logger LOGGER = Logger.getLogger(CLASS_NAME); + + /** + * Runs the demo. + * + * @param args the command line arguments + * @throws IOException if there is a problem. + */ + public static void main(String[] args) throws IOException { + List<String> l = Arrays.asList(args); + if (l.contains("/?") || l.contains("-?") || l.contains("-help")) { + LOGGER.info("Usage: java MailHandlerDemo " + + "[[-all] | [-body] | [-custom] | [-debug] | [-low] " + + "| [-simple] | [-pushlevel] | [-pushfilter] " + + "| [-pushnormal] | [-pushonly]] " + + "\n\n" + + "-all\t\t: Execute all demos.\n" + + "-body\t\t: An email with all records and only a body.\n" + + "-custom\t\t: An email with attachments and dynamic names.\n" + + "-debug\t\t: Output basic debug information about the JVM " + + "and log configuration.\n" + + "-low\t\t: Generates multiple emails due to low capacity." + + "\n" + + "-simple\t\t: An email with all records with body and " + + "an attachment.\n" + + "-pushlevel\t: Generates high priority emails when the" + + " push level is triggered and normal priority when " + + "flushed.\n" + + "-pushFilter\t: Generates high priority emails when the " + + "push level and the push filter is triggered and normal " + + "priority emails when flushed.\n" + + "-pushnormal\t: Generates multiple emails when the " + + "MemoryHandler push level is triggered. All generated " + + "email are sent as normal priority.\n" + + "-pushonly\t: Generates multiple emails when the " + + "MemoryHandler push level is triggered. Generates high " + + "priority emails when the push level is triggered and " + + "normal priority when flushed.\n"); + } else { + final boolean debug = init(l); //may create log messages. + try { + LOGGER.log(Level.FINEST, "This is the finest part of the demo.", + new MessagingException("Fake JavaMail issue.")); + LOGGER.log(Level.FINER, "This is the finer part of the demo.", + new NullPointerException("Fake bug.")); + LOGGER.log(Level.FINE, "This is the fine part of the demo."); + LOGGER.log(Level.CONFIG, "Logging config file is {0}.", + getConfigLocation()); + LOGGER.log(Level.INFO, "Your temp directory is {0}, " + + "please wait...", getTempDir()); + + try { //Waste some time for the custom formatter. + Thread.sleep(3L * 1000L); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + + LOGGER.log(Level.WARNING, "This is a warning.", + new FileNotFoundException("Fake file chooser issue.")); + LOGGER.log(Level.SEVERE, "The end of the demo.", + new IOException("Fake access denied issue.")); + } finally { + closeHandlers(); + } + + //Force parse errors. This does have side effects. + if (debug && getConfigLocation() != null) { + LogManager.getLogManager().readConfiguration(); + } + } + } + + /** + * Used debug problems with the logging.properties. The system property + * java.security.debug=access,stack can be used to trace access to the + * LogManager reset. + * + * @param prefix a string to prefix the output. + * @param err any PrintStream or null for System.out. + */ + @SuppressWarnings("UseOfSystemOutOrSystemErr") + private static void checkConfig(String prefix, PrintStream err) { + if (prefix == null || prefix.trim().length() == 0) { + prefix = "DEBUG"; + } + + if (err == null) { + err = System.out; + } + + try { + err.println(prefix + ": java.version=" + + System.getProperty("java.version")); + err.println(prefix + ": LOGGER=" + LOGGER.getLevel()); + err.println(prefix + ": JVM id " + + ManagementFactory.getRuntimeMXBean().getName()); + err.println(prefix + ": java.security.debug=" + + System.getProperty("java.security.debug")); + SecurityManager sm = System.getSecurityManager(); + if (sm != null) { + err.println(prefix + ": SecurityManager.class=" + + sm.getClass().getName()); + err.println(prefix + ": SecurityManager classLoader=" + + toString(sm.getClass().getClassLoader())); + err.println(prefix + ": SecurityManager.toString=" + sm); + } else { + err.println(prefix + ": SecurityManager.class=null"); + err.println(prefix + ": SecurityManager.toString=null"); + err.println(prefix + ": SecurityManager classLoader=null"); + } + + String policy = System.getProperty("java.security.policy"); + if (policy != null) { + File f = new File(policy); + err.println(prefix + ": AbsolutePath=" + f.getAbsolutePath()); + err.println(prefix + ": CanonicalPath=" + f.getCanonicalPath()); + err.println(prefix + ": length=" + f.length()); + err.println(prefix + ": canRead=" + f.canRead()); + err.println(prefix + ": lastModified=" + + new java.util.Date(f.lastModified())); + } + + LogManager manager = LogManager.getLogManager(); + String key = "java.util.logging.config.file"; + String cfg = System.getProperty(key); + if (cfg != null) { + err.println(prefix + ": " + cfg); + File f = new File(cfg); + err.println(prefix + ": AbsolutePath=" + f.getAbsolutePath()); + err.println(prefix + ": CanonicalPath=" + f.getCanonicalPath()); + err.println(prefix + ": length=" + f.length()); + err.println(prefix + ": canRead=" + f.canRead()); + err.println(prefix + ": lastModified=" + + new java.util.Date(f.lastModified())); + } else { + err.println(prefix + ": " + key + + " is not set as a system property."); + } + err.println(prefix + ": LogManager.class=" + + manager.getClass().getName()); + err.println(prefix + ": LogManager classLoader=" + + toString(manager.getClass().getClassLoader())); + err.println(prefix + ": LogManager.toString=" + manager); + err.println(prefix + ": MailHandler classLoader=" + + toString(MailHandler.class.getClassLoader())); + err.println(prefix + ": Context ClassLoader=" + + toString(Thread.currentThread().getContextClassLoader())); + err.println(prefix + ": Session ClassLoader=" + + toString(Session.class.getClassLoader())); + err.println(prefix + ": DataHandler ClassLoader=" + + toString(DataHandler.class.getClassLoader())); + + final String p = MailHandler.class.getName(); + key = p.concat(".mail.to"); + String to = manager.getProperty(key); + err.println(prefix + ": TO=" + to); + if (to != null) { + err.println(prefix + ": TO=" + + Arrays.toString(InternetAddress.parse(to, true))); + } + + key = p.concat(".mail.from"); + String from = manager.getProperty(key); + if (from == null || from.length() == 0) { + Session session = Session.getInstance(new Properties()); + InternetAddress local = InternetAddress.getLocalAddress(session); + err.println(prefix + ": FROM=" + local); + } else { + err.println(prefix + ": FROM=" + + Arrays.asList(InternetAddress.parse(from, false))); + err.println(prefix + ": FROM=" + + Arrays.asList(InternetAddress.parse(from, true))); + } + + synchronized (manager) { + final Enumeration<String> e = manager.getLoggerNames(); + while (e.hasMoreElements()) { + final Logger l = manager.getLogger(e.nextElement()); + if (l != null) { + final Handler[] handlers = l.getHandlers(); + if (handlers.length > 0) { + err.println(prefix + ": " + l.getClass().getName() + + ", " + l.getName()); + for (Handler h : handlers) { + err.println(prefix + ":\t" + toString(prefix, err, h)); + } + } + } + } + } + } catch (Throwable error) { + err.print(prefix + ": "); + error.printStackTrace(err); + } + err.flush(); + } + + /** + * Gets the class loader list. + * + * @param cl the class loader or null. + * @return the class loader list. + */ + private static String toString(ClassLoader cl) { + StringBuilder buf = new StringBuilder(); + buf.append(cl); + while (cl != null) { + cl = cl.getParent(); + buf.append("<-").append(cl); + } + return buf.toString(); + } + + /** + * Gets a formatting string describing the given handler. + * + * @param prefix the output prefix. + * @param err the error stream. + * @param h the handler. + * @return the formatted string. + */ + private static String toString(String prefix, PrintStream err, Handler h) { + StringBuilder buf = new StringBuilder(); + buf.append(h.getClass().getName()); + try { + if (h instanceof MailHandler) { + MailHandler mh = (MailHandler) h; + buf.append(", ").append(mh.getSubject()); + } + } catch (SecurityException error) { + err.print(prefix + ": "); + error.printStackTrace(err); + } + + try { + buf.append(", ").append(h.getFormatter()); + } catch (SecurityException error) { + err.print(prefix + ": "); + error.printStackTrace(err); + } + + try { + if (h instanceof MailHandler) { + MailHandler mh = (MailHandler) h; + buf.append(", ").append(Arrays.toString( + mh.getAttachmentFormatters())); + } + } catch (SecurityException error) { + err.print(prefix + ": "); + error.printStackTrace(err); + } + + try { + buf.append(", ").append(h.getLevel()); + } catch (SecurityException error) { + err.print(prefix + ": "); + error.printStackTrace(err); + } + + try { + buf.append(", ").append(h.getFilter()); + } catch (SecurityException error) { + err.print(prefix + ": "); + error.printStackTrace(err); + } + + try { + buf.append(", ").append(h.getErrorManager()); + } catch (SecurityException error) { + err.print(prefix + ": "); + error.printStackTrace(err); + } + + buf.append(", ").append(toString(h.getClass().getClassLoader())); + return buf.toString(); + } + + /** + * Example for body only messages. On close the remaining messages are sent. <code> + * ##logging.properties + * MailHandlerDemo.handlers=com.sun.mail.util.logging.MailHandler + * com.sun.mail.util.logging.MailHandler.subject=Body only demo + * ## + * </code> + */ + private static void initBodyOnly() { + MailHandler h = new MailHandler(); + h.setSubject("Body only demo"); + LOGGER.addHandler(h); + } + + /** + * Example showing that when the mail handler reaches capacity it will + * format and send the current records. Capacity is used to roughly limit + * the size of an outgoing message. On close any remaining messages are + * sent. <code> + * ##logging.properties + * MailHandlerDemo.handlers=com.sun.mail.util.logging.MailHandler + * com.sun.mail.util.logging.MailHandler.subject=Low capacity demo + * com.sun.mail.util.logging.MailHandler.capacity=5 + * ## + * </code> + */ + private static void initLowCapacity() { + MailHandler h = new MailHandler(5); + h.setSubject("Low capacity demo"); + LOGGER.addHandler(h); + } + + /** + * Example for body only messages. On close any remaining messages are sent. <code> + * ##logging.properties + * MailHandlerDemo.handlers=com.sun.mail.util.logging.MailHandler + * com.sun.mail.util.logging.MailHandler.subject=Body and attachment demo + * com.sun.mail.util.logging.MailHandler.attachment.formatters=java.util.logging.XMLFormatter + * com.sun.mail.util.logging.MailHandler.attachment.names=data.xml + * ## + * </code> + */ + private static void initSimpleAttachment() { + MailHandler h = new MailHandler(); + h.setSubject("Body and attachment demo"); + h.setAttachmentFormatters(new XMLFormatter()); + h.setAttachmentNames("data.xml"); + LOGGER.addHandler(h); + } + + /** + * Example setup for priority messages by level. If the push level is + * triggered the message is high priority. Otherwise, on close any remaining + * messages are sent. <code> + * ##logging.properties + * MailHandlerDemo.handlers=com.sun.mail.util.logging.MailHandler + * com.sun.mail.util.logging.MailHandler.subject=Push level demo + * com.sun.mail.util.logging.MailHandler.pushLevel=WARNING + * ## + * </code> + */ + private static void initWithPushLevel() { + MailHandler h = new MailHandler(); + h.setSubject("Push level demo"); + h.setPushLevel(Level.WARNING); + LOGGER.addHandler(h); + } + + /** + * Example for priority messages by generation rate. If the push filter is + * triggered the message is high priority. Otherwise, on close any remaining + * messages are sent. If the capacity is set to the <code> + * ##logging.properties + * MailHandlerDemo.handlers=com.sun.mail.util.logging.MailHandler + * com.sun.mail.util.logging.MailHandler.subject=Push filter demo + * com.sun.mail.util.logging.MailHandler.pushLevel=ALL + * com.sun.mail.util.logging.MailHandler.pushFilter=com.sun.mail.util.logging.DurationFilter + * com.sun.mail.util.logging.DurationFilter.records=2 + * com.sun.mail.util.logging.DurationFilter.duration=1 * 60 * 1000 + * ## + * </code> + */ + private static void initWithPushFilter() { + MailHandler h = new MailHandler(); + h.setSubject("Push filter demo"); + h.setPushLevel(Level.ALL); + h.setPushFilter(new DurationFilter(2, 1L * 60L * 1000L)); + LOGGER.addHandler(h); + } + + /** + * Example for circular buffer behavior. The level, push level, and capacity + * are set the same so that the memory handler push results in a mail + * handler push. All messages are high priority. On close any remaining + * records are discarded because they never reach the mail handler. <code> + * ##logging.properties + * MailHandlerDemo.handlers=java.util.logging.MemoryHandler + * java.util.logging.MemoryHandler.target=com.sun.mail.util.logging.MailHandler + * com.sun.mail.util.logging.MailHandler.level=ALL + * java.util.logging.MemoryHandler.level=ALL + * java.util.logging.MemoryHandler.push=WARNING + * com.sun.mail.util.logging.MailHandler.subject=Push only demo + * com.sun.mail.util.logging.MailHandler.pushLevel=WARNING + * ## + * </code> + */ + private static void initPushOnly() { + final int capacity = 3; + final Level pushLevel = Level.WARNING; + final MailHandler h = new MailHandler(capacity); + h.setPushLevel(pushLevel); + h.setSubject("Push only demo"); + MemoryHandler m = new MemoryHandler(h, capacity, pushLevel); + h.setLevel(m.getLevel()); + LOGGER.addHandler(m); + pushOnlyHandler = h; + } + + /** + * Holds on to the push only handler. Only declared here to apply fallback + * settings. + */ + private static Handler pushOnlyHandler; + + /** + * Example for circular buffer behavior as normal priority. The push level, + * and capacity are set the same so that the memory handler push results in + * a mail handler push. All messages are normal priority. On close any + * remaining records are discarded because they never reach the mail + * handler. Use the LogManager config option or extend the MemoryHandler to + * emulate this behavior via the logging.properties. + */ + private static void initPushNormal() { + final int capacity = 3; + final MailHandler h = new MailHandler(capacity); + h.setSubject("Push normal demo"); + MemoryHandler m = new MemoryHandler(h, capacity, Level.WARNING) { + + @Override + public void push() { + super.push(); //push to target. + super.flush(); //make the target send the email. + } + }; + LOGGER.addHandler(m); + pushNormalHandler = h; + } + + /** + * Holds on to the push normal handler. Only declared here to apply fallback + * settings. + */ + private static Handler pushNormalHandler; + + /** + * Example for various kinds of custom sorting, formatting, and filtering + * for multiple attachment messages. The subject will contain the most + * severe record and a count of remaining records. The log records are + * ordered from most severe to least severe. The body uses a custom + * formatter that includes a summary by date and time. The attachment use + * XML and plain text formats. Each attachment has a different set of + * filtering. The attachment names are generated from either a fixed name or + * are built using the number and type of the records formatted. On close + * any remaining messages are sent. Use the LogManager config option or + * extend the MemoryHandler to emulate this behavior via the + * logging.properties. + */ + private static void initCustomAttachments() { + MailHandler h = new MailHandler(); + + //Sort records by severity keeping the severe messages at the top. + h.setComparator(Collections.reverseOrder(new SeverityComparator())); + + //Use subject to provide a hint as to what is in the email. + h.setSubject(new CollectorFormatter()); + + //Make the body give a simple summary of what happened. + h.setFormatter(new SummaryFormatter()); + + //Create 3 attachments. + h.setAttachmentFormatters(new XMLFormatter(), + new XMLFormatter(), new SimpleFormatter()); + + //Filter each attachment differently. + h.setAttachmentFilters(null, + new DurationFilter(3L, 1000L), + new DurationFilter(1L, 15L * 60L * 1000L)); + + //Creating the attachment name formatters. + h.setAttachmentNames(new CollectorFormatter("all.xml"), + new CollectorFormatter("{3} records and {5} errors.xml"), + new CollectorFormatter("{5,choice,0#no errors|1#1 error|1<" + + "{5,number,integer} errors}.txt")); + + LOGGER.addHandler(h); + } + + /** + * Sets up the demos that will run. + * + * @param l the list of arguments. + * @return true if debug is on. + */ + private static boolean init(List<String> l) { + l = new ArrayList<String>(l); + Session session = Session.getInstance(System.getProperties()); + boolean all = l.remove("-all") || l.isEmpty(); + if (l.remove("-body") || all) { + initBodyOnly(); + } + + if (l.remove("-custom") || all) { + initCustomAttachments(); + } + + if (l.remove("-low") || all) { + initLowCapacity(); + } + + if (l.remove("-pushfilter") || all) { + initWithPushFilter(); + } + + if (l.remove("-pushlevel") || all) { + initWithPushLevel(); + } + + if (l.remove("-pushnormal") || all) { + initPushNormal(); + } + + if (l.remove("-pushonly") || all) { + initPushOnly(); + } + + if (l.remove("-simple") || all) { + initSimpleAttachment(); + } + + boolean fallback = applyFallbackSettings(); + boolean debug = l.remove("-debug") || session.getDebug(); + if (debug) { + checkConfig(CLASS_NAME, session.getDebugOut()); + } + + if (!l.isEmpty()) { + LOGGER.log(Level.SEVERE, "Unknown commands: {0}", l); + } + + if (fallback) { + LOGGER.info("Check your user temp dir for output."); + } + return debug; + } + + /** + * Close and remove all handlers added to the class logger. + */ + private static void closeHandlers() { + Handler[] handlers = LOGGER.getHandlers(); + for (Handler h : handlers) { + h.close(); + LOGGER.removeHandler(h); + } + } + + /** + * Apply some fallback settings if no configuration file was specified. + * + * @return true if fallback settings were applied. + */ + private static boolean applyFallbackSettings() { + if (getConfigLocation() == null) { + LOGGER.setLevel(Level.ALL); + Handler[] handlers = LOGGER.getHandlers(); + for (Handler h : handlers) { + fallbackSettings(h); + } + fallbackSettings(pushOnlyHandler); + fallbackSettings(pushNormalHandler); + return true; + } + return false; + } + + /** + * Common fallback settings for a single handler. + * + * @param h the handler. + */ + private static void fallbackSettings(Handler h) { + if (h != null) { + h.setErrorManager(new FileErrorManager()); + h.setLevel(Level.ALL); + } + } + + /** + * Gets the system temp directory. + * + * @return the system temp directory. + */ + private static String getTempDir() { + return System.getProperty("java.io.tmpdir"); + } + + /** + * Gets the configuration file or class name. + * + * @return the file name or class name. + */ + private static String getConfigLocation() { + String file = System.getProperty("java.util.logging.config.file"); + if (file == null) { + return System.getProperty("java.util.logging.config.class"); + } + return file; + } + + /** + * No objects are allowed. + * @throws IllegalAccessException always. + */ + private MailHandlerDemo() throws IllegalAccessException { + throw new IllegalAccessException(); + } +}
diff --git a/logging/src/main/java/README.txt b/logging/src/main/java/README.txt new file mode 100644 index 0000000..c6b7add --- /dev/null +++ b/logging/src/main/java/README.txt
@@ -0,0 +1,103 @@ +Logging Demo +------------ + +Notes: +====== + +This should not be taken as a demo of how to use the logging API, but +rather how to use the features of the MailHandler. + +To run the demo: +================ + + 1. The demo requires Java version 1.5 or newer. + We *strongly* encourage you to use the latest version of J2SE, + which you can download from + http://www.oracle.com/technetwork/java/javase/downloads. + + 2. Set your CLASSPATH to include the "mail.jar" and "activation.jar". + + For JDK 1.1 on UNIX: + + export CLASSPATH=/u/me/download/mail.jar:/u/me/download/activation.jar. + + For JDK 1.2 and newer on UNIX: + + export CLASSPATH=/u/me/download/mail.jar:/u/me/download/activation.jar:. + + 3. Go to the demo/logging directory + + 4. Compile all the files using your Java compiler. For example: + + javac *.java + + 5. Not required but, you should edit the maildemo.properties and change the + mail.to address and mail.host to your mail server ip or host name. + + 6. Run the demo. For example: + + java -Dmail.debug=false -Djava.util.logging.config.file=/u/me/download/javamail/demo/maildemo.properties MailHandlerDemo + + + +Overview of the Classes +======================= + +Main Classes: + + MailHandlerDemo = The main method creates log messages + for the MailHander to capture. The + initXXX methods describe some of the + common setup code for different types + of email messages. + + Usage: java MailHandlerDemo [[-all] | [-body] | [-debug] + | [-low] | [-simple] | [-pushlevel] + | [-pushfilter] | [-pushnormal]| [-pushonly]] + + Options: + -all : Execute all demos. + -body : An email with all records and only a body. + -custom : An email with attachments and dynamic names. + -debug : Output basic debug information about the + JVM and log configuration. + -low : Generates multiple emails due to low + capacity. + -simple : An email with all records with body and an + attachment. + -pushlevel : Generates high priority emails when + the push level is triggered and + normal priority when flushed. + -pushFilter : Generates high priority emails when + the push level and the push filter + is triggered and normal priority + emails when flushed. + -pushnormal : Generates multiple emails when the + MemoryHandler push level is + triggered. All generated email are + sent as normal priority. + -pushonly : Generates multiple emails when the + MemoryHandler push level is + triggered. Generates high priority + emails when the push level is + triggered and normal priority when + flushed. + + + FileErrorManager = Used to store email messages to the + local file system when mail transport + fails. This is installed as a + fallback in case the logging config is + not specified. + + SummaryFormatter = An example compact formatter with summary + for use with the body of an email message. + +Support files: + + maildemo.properties = A sample LogManager properties file for + the MailHandlerDemo. + + maildemo.policy = A sample security policy file to use with + the MailHandlerDemo. This can be used to + enable security tracing.
diff --git a/logging/src/main/java/SummaryFormatter.java b/logging/src/main/java/SummaryFormatter.java new file mode 100644 index 0000000..b821364 --- /dev/null +++ b/logging/src/main/java/SummaryFormatter.java
@@ -0,0 +1,80 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2009, 2018 Jason Mehrens. All Rights Reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +import com.sun.mail.util.logging.CollectorFormatter; +import com.sun.mail.util.logging.CompactFormatter; +import java.util.logging.Formatter; +import java.util.logging.Handler; +import java.util.logging.LogRecord; + +/** + * A compact formatter used to summarize an error report. + * + * @author Jason Mehrens + */ +public final class SummaryFormatter extends Formatter { + + /** + * The line formatter. + */ + private final CompactFormatter format; + /** + * The footer formatter. + */ + private final CollectorFormatter footer; + + /** + * Creates the formatter. + */ + public SummaryFormatter() { + format = new CompactFormatter("[%4$s]\t%5$s %6$s%n"); + footer = new CollectorFormatter("\nThese {3} messages occurred between " + + "{7,time,EEE, MMM dd HH:mm:ss:S ZZZ yyyy} and " + + "{8,time,EEE, MMM dd HH:mm:ss:S ZZZ yyyy}\n", format, null); + } + + /** + * Gets the header information. + * + * @param h the handler or null. + * @return the header. + */ + @Override + public String getHead(Handler h) { + footer.getHead(h); + return format.getHead(h); + } + + /** + * Formats the given record. + * + * @param record the log record. + * @return the formatted record. + * @throws NullPointerException if record is null. + */ + public String format(LogRecord record) { + String data = format.format(record); + footer.format(record); //Track record times for footer. + return data; + } + + /** + * Gets and resets the footer information. + * + * @param h the handler or null. + * @return the footer. + */ + @Override + public String getTail(Handler h) { + format.getTail(h); + return footer.getTail(h); + } +}
diff --git a/logging/src/main/java/maildemo.policy b/logging/src/main/java/maildemo.policy new file mode 100644 index 0000000..d7d7980 --- /dev/null +++ b/logging/src/main/java/maildemo.policy
@@ -0,0 +1,19 @@ +/* + * Copyright (c) 2014, 2018 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2014, 2018 Jason Mehrens. All Rights Reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +grant { + permission java.util.logging.LoggingPermission "control"; + permission java.util.PropertyPermission "*", "read, write"; + permission java.net.SocketPermission "*", "resolve, connect"; + permission java.lang.RuntimePermission "accessClassInPackage.sun.util.logging.resources"; + permission java.io.FilePermission "<<ALL FILES>>", "read, write"; + permission java.lang.RuntimePermission "getClassLoader"; +}; +
diff --git a/logging/src/main/java/maildemo.properties b/logging/src/main/java/maildemo.properties new file mode 100644 index 0000000..314a05a --- /dev/null +++ b/logging/src/main/java/maildemo.properties
@@ -0,0 +1,51 @@ +# +# Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2009, 2018 Jason Mehrens. All Rights Reserved. +# +# This program and the accompanying materials are made available under the +# terms of the Eclipse Distribution License v. 1.0, which is available at +# http://www.eclipse.org/org/documents/edl-v10.php. +# +# SPDX-License-Identifier: BSD-3-Clause + + + + +# This can be used by setting the system property +# -Djava.util.logging.config.file=path to this file + +# Taken from the JDK defaults. +handlers= java.util.logging.ConsoleHandler +.level= INFO +java.util.logging.ConsoleHandler.level = INFO +java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter + + +# Set the mail handler demo logger level +MailHandlerDemo.level = ALL + +# Configure the MailHandler. +com.sun.mail.util.logging.MailHandler.level = ALL +com.sun.mail.util.logging.MailHandler.mail.host = my-mail-server +com.sun.mail.util.logging.MailHandler.mail.from = me@example.com +com.sun.mail.util.logging.MailHandler.mail.to = me@example.com +com.sun.mail.util.logging.MailHandler.verify = local + +# Add attachments if needed. +#com.sun.mail.util.logging.MailHandler.attachment.formatters = java.util.logging.SimpleFormatter, java.util.logging.XMLFormatter + +# No filters. +#com.sun.mail.util.logging.MailHandler.attachment.filters = null, null + +# Formatter class name or strings. +#com.sun.mail.util.logging.MailHandler.attachment.names = simple.txt, error.xml + + +# Store messages on error by installing the FileErrorManager (demo code). +com.sun.mail.util.logging.MailHandler.errorManager = FileErrorManager + +# Configure the FileErrorManager for demo (not required). +# FileErrorManager.pattern = path-to-dir + +# Debug mail transport issues. +mail.debug = false
diff --git a/mail.sig b/mail.sig new file mode 100644 index 0000000..fdddb33 --- /dev/null +++ b/mail.sig
@@ -0,0 +1,1451 @@ +#Signature file v4.3 +#Version + +CLSS public abstract javax.mail.Address +cons public <init>() +intf java.io.Serializable +meth public abstract boolean equals(java.lang.Object) +meth public abstract java.lang.String getType() +meth public abstract java.lang.String toString() +supr java.lang.Object +hfds serialVersionUID + +CLSS public javax.mail.AuthenticationFailedException +cons public <init>() +cons public <init>(java.lang.String) +cons public <init>(java.lang.String,java.lang.Exception) +supr javax.mail.MessagingException +hfds serialVersionUID + +CLSS public abstract javax.mail.Authenticator +cons public <init>() +meth protected final int getRequestingPort() +meth protected final java.lang.String getDefaultUserName() +meth protected final java.lang.String getRequestingPrompt() +meth protected final java.lang.String getRequestingProtocol() +meth protected final java.net.InetAddress getRequestingSite() +meth protected javax.mail.PasswordAuthentication getPasswordAuthentication() +supr java.lang.Object +hfds requestingPort,requestingPrompt,requestingProtocol,requestingSite,requestingUserName + +CLSS public abstract javax.mail.BodyPart +cons public <init>() +fld protected javax.mail.Multipart parent +intf javax.mail.Part +meth public javax.mail.Multipart getParent() +supr java.lang.Object + +CLSS public abstract interface javax.mail.EncodingAware +meth public abstract java.lang.String getEncoding() + +CLSS public javax.mail.FetchProfile +cons public <init>() +innr public static Item +meth public boolean contains(java.lang.String) +meth public boolean contains(javax.mail.FetchProfile$Item) +meth public java.lang.String[] getHeaderNames() +meth public javax.mail.FetchProfile$Item[] getItems() +meth public void add(java.lang.String) +meth public void add(javax.mail.FetchProfile$Item) +supr java.lang.Object +hfds headers,specials + +CLSS public static javax.mail.FetchProfile$Item + outer javax.mail.FetchProfile +cons protected <init>(java.lang.String) +fld public final static javax.mail.FetchProfile$Item CONTENT_INFO +fld public final static javax.mail.FetchProfile$Item ENVELOPE +fld public final static javax.mail.FetchProfile$Item FLAGS +fld public final static javax.mail.FetchProfile$Item SIZE +meth public java.lang.String toString() +supr java.lang.Object +hfds name + +CLSS public javax.mail.Flags +cons public <init>() +cons public <init>(java.lang.String) +cons public <init>(javax.mail.Flags$Flag) +cons public <init>(javax.mail.Flags) +innr public final static Flag +intf java.io.Serializable +intf java.lang.Cloneable +meth public boolean contains(java.lang.String) +meth public boolean contains(javax.mail.Flags$Flag) +meth public boolean contains(javax.mail.Flags) +meth public boolean equals(java.lang.Object) +meth public boolean retainAll(javax.mail.Flags) +meth public int hashCode() +meth public java.lang.Object clone() +meth public java.lang.String toString() +meth public java.lang.String[] getUserFlags() +meth public javax.mail.Flags$Flag[] getSystemFlags() +meth public void add(java.lang.String) +meth public void add(javax.mail.Flags$Flag) +meth public void add(javax.mail.Flags) +meth public void clearSystemFlags() +meth public void clearUserFlags() +meth public void remove(java.lang.String) +meth public void remove(javax.mail.Flags$Flag) +meth public void remove(javax.mail.Flags) +supr java.lang.Object +hfds ANSWERED_BIT,DELETED_BIT,DRAFT_BIT,FLAGGED_BIT,RECENT_BIT,SEEN_BIT,USER_BIT,serialVersionUID,system_flags,user_flags + +CLSS public final static javax.mail.Flags$Flag + outer javax.mail.Flags +fld public final static javax.mail.Flags$Flag ANSWERED +fld public final static javax.mail.Flags$Flag DELETED +fld public final static javax.mail.Flags$Flag DRAFT +fld public final static javax.mail.Flags$Flag FLAGGED +fld public final static javax.mail.Flags$Flag RECENT +fld public final static javax.mail.Flags$Flag SEEN +fld public final static javax.mail.Flags$Flag USER +supr java.lang.Object +hfds bit + +CLSS public abstract javax.mail.Folder +cons protected <init>(javax.mail.Store) +fld protected int mode +fld protected javax.mail.Store store +fld public final static int HOLDS_FOLDERS = 2 +fld public final static int HOLDS_MESSAGES = 1 +fld public final static int READ_ONLY = 1 +fld public final static int READ_WRITE = 2 +intf java.lang.AutoCloseable +meth protected void finalize() throws java.lang.Throwable +meth protected void notifyConnectionListeners(int) +meth protected void notifyFolderListeners(int) +meth protected void notifyFolderRenamedListeners(javax.mail.Folder) +meth protected void notifyMessageAddedListeners(javax.mail.Message[]) +meth protected void notifyMessageChangedListeners(int,javax.mail.Message) +meth protected void notifyMessageRemovedListeners(boolean,javax.mail.Message[]) +meth public abstract boolean create(int) throws javax.mail.MessagingException +meth public abstract boolean delete(boolean) throws javax.mail.MessagingException +meth public abstract boolean exists() throws javax.mail.MessagingException +meth public abstract boolean hasNewMessages() throws javax.mail.MessagingException +meth public abstract boolean isOpen() +meth public abstract boolean renameTo(javax.mail.Folder) throws javax.mail.MessagingException +meth public abstract char getSeparator() throws javax.mail.MessagingException +meth public abstract int getMessageCount() throws javax.mail.MessagingException +meth public abstract int getType() throws javax.mail.MessagingException +meth public abstract java.lang.String getFullName() +meth public abstract java.lang.String getName() +meth public abstract javax.mail.Flags getPermanentFlags() +meth public abstract javax.mail.Folder getFolder(java.lang.String) throws javax.mail.MessagingException +meth public abstract javax.mail.Folder getParent() throws javax.mail.MessagingException +meth public abstract javax.mail.Folder[] list(java.lang.String) throws javax.mail.MessagingException +meth public abstract javax.mail.Message getMessage(int) throws javax.mail.MessagingException +meth public abstract javax.mail.Message[] expunge() throws javax.mail.MessagingException +meth public abstract void appendMessages(javax.mail.Message[]) throws javax.mail.MessagingException +meth public abstract void close(boolean) throws javax.mail.MessagingException +meth public abstract void open(int) throws javax.mail.MessagingException +meth public boolean isSubscribed() +meth public int getDeletedMessageCount() throws javax.mail.MessagingException +meth public int getMode() +meth public int getNewMessageCount() throws javax.mail.MessagingException +meth public int getUnreadMessageCount() throws javax.mail.MessagingException +meth public java.lang.String toString() +meth public javax.mail.Folder[] list() throws javax.mail.MessagingException +meth public javax.mail.Folder[] listSubscribed() throws javax.mail.MessagingException +meth public javax.mail.Folder[] listSubscribed(java.lang.String) throws javax.mail.MessagingException +meth public javax.mail.Message[] getMessages() throws javax.mail.MessagingException +meth public javax.mail.Message[] getMessages(int,int) throws javax.mail.MessagingException +meth public javax.mail.Message[] getMessages(int[]) throws javax.mail.MessagingException +meth public javax.mail.Message[] search(javax.mail.search.SearchTerm) throws javax.mail.MessagingException +meth public javax.mail.Message[] search(javax.mail.search.SearchTerm,javax.mail.Message[]) throws javax.mail.MessagingException +meth public javax.mail.Store getStore() +meth public javax.mail.URLName getURLName() throws javax.mail.MessagingException +meth public void addConnectionListener(javax.mail.event.ConnectionListener) +meth public void addFolderListener(javax.mail.event.FolderListener) +meth public void addMessageChangedListener(javax.mail.event.MessageChangedListener) +meth public void addMessageCountListener(javax.mail.event.MessageCountListener) +meth public void close() throws javax.mail.MessagingException +meth public void copyMessages(javax.mail.Message[],javax.mail.Folder) throws javax.mail.MessagingException +meth public void fetch(javax.mail.Message[],javax.mail.FetchProfile) throws javax.mail.MessagingException +meth public void removeConnectionListener(javax.mail.event.ConnectionListener) +meth public void removeFolderListener(javax.mail.event.FolderListener) +meth public void removeMessageChangedListener(javax.mail.event.MessageChangedListener) +meth public void removeMessageCountListener(javax.mail.event.MessageCountListener) +meth public void setFlags(int,int,javax.mail.Flags,boolean) throws javax.mail.MessagingException +meth public void setFlags(int[],javax.mail.Flags,boolean) throws javax.mail.MessagingException +meth public void setFlags(javax.mail.Message[],javax.mail.Flags,boolean) throws javax.mail.MessagingException +meth public void setSubscribed(boolean) throws javax.mail.MessagingException +supr java.lang.Object +hfds connectionListeners,folderListeners,messageChangedListeners,messageCountListeners,q + +CLSS public javax.mail.FolderClosedException +cons public <init>(javax.mail.Folder) +cons public <init>(javax.mail.Folder,java.lang.String) +cons public <init>(javax.mail.Folder,java.lang.String,java.lang.Exception) +meth public javax.mail.Folder getFolder() +supr javax.mail.MessagingException +hfds folder,serialVersionUID + +CLSS public javax.mail.FolderNotFoundException +cons public <init>() +cons public <init>(java.lang.String,javax.mail.Folder) +cons public <init>(javax.mail.Folder) +cons public <init>(javax.mail.Folder,java.lang.String) +cons public <init>(javax.mail.Folder,java.lang.String,java.lang.Exception) +meth public javax.mail.Folder getFolder() +supr javax.mail.MessagingException +hfds folder,serialVersionUID + +CLSS public javax.mail.Header +cons public <init>(java.lang.String,java.lang.String) +fld protected java.lang.String name +fld protected java.lang.String value +meth public java.lang.String getName() +meth public java.lang.String getValue() +supr java.lang.Object + +CLSS public javax.mail.IllegalWriteException +cons public <init>() +cons public <init>(java.lang.String) +cons public <init>(java.lang.String,java.lang.Exception) +supr javax.mail.MessagingException +hfds serialVersionUID + +CLSS public abstract interface !annotation javax.mail.MailSessionDefinition + anno 0 java.lang.annotation.Repeatable(java.lang.Class<? extends java.lang.annotation.Annotation> value=class javax.mail.MailSessionDefinitions) + anno 0 java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy value=RUNTIME) + anno 0 java.lang.annotation.Target(java.lang.annotation.ElementType[] value=[TYPE]) +intf java.lang.annotation.Annotation +meth public abstract !hasdefault java.lang.String description() value= "" +meth public abstract !hasdefault java.lang.String from() value= "" +meth public abstract !hasdefault java.lang.String host() value= "" +meth public abstract !hasdefault java.lang.String password() value= "" +meth public abstract !hasdefault java.lang.String storeProtocol() value= "" +meth public abstract !hasdefault java.lang.String transportProtocol() value= "" +meth public abstract !hasdefault java.lang.String user() value= "" +meth public abstract !hasdefault java.lang.String[] properties() value= [] +meth public abstract java.lang.String name() + +CLSS public abstract interface !annotation javax.mail.MailSessionDefinitions + anno 0 java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy value=RUNTIME) + anno 0 java.lang.annotation.Target(java.lang.annotation.ElementType[] value=[TYPE]) +intf java.lang.annotation.Annotation +meth public abstract javax.mail.MailSessionDefinition[] value() + +CLSS public abstract javax.mail.Message +cons protected <init>() +cons protected <init>(javax.mail.Folder,int) +cons protected <init>(javax.mail.Session) +fld protected boolean expunged +fld protected int msgnum +fld protected javax.mail.Folder folder +fld protected javax.mail.Session session +innr public static RecipientType +intf javax.mail.Part +meth protected void setExpunged(boolean) +meth protected void setMessageNumber(int) +meth public abstract java.lang.String getSubject() throws javax.mail.MessagingException +meth public abstract java.util.Date getReceivedDate() throws javax.mail.MessagingException +meth public abstract java.util.Date getSentDate() throws javax.mail.MessagingException +meth public abstract javax.mail.Address[] getFrom() throws javax.mail.MessagingException +meth public abstract javax.mail.Address[] getRecipients(javax.mail.Message$RecipientType) throws javax.mail.MessagingException +meth public abstract javax.mail.Flags getFlags() throws javax.mail.MessagingException +meth public abstract javax.mail.Message reply(boolean) throws javax.mail.MessagingException +meth public abstract void addFrom(javax.mail.Address[]) throws javax.mail.MessagingException +meth public abstract void addRecipients(javax.mail.Message$RecipientType,javax.mail.Address[]) throws javax.mail.MessagingException +meth public abstract void saveChanges() throws javax.mail.MessagingException +meth public abstract void setFlags(javax.mail.Flags,boolean) throws javax.mail.MessagingException +meth public abstract void setFrom() throws javax.mail.MessagingException +meth public abstract void setFrom(javax.mail.Address) throws javax.mail.MessagingException +meth public abstract void setRecipients(javax.mail.Message$RecipientType,javax.mail.Address[]) throws javax.mail.MessagingException +meth public abstract void setSentDate(java.util.Date) throws javax.mail.MessagingException +meth public abstract void setSubject(java.lang.String) throws javax.mail.MessagingException +meth public boolean isExpunged() +meth public boolean isSet(javax.mail.Flags$Flag) throws javax.mail.MessagingException +meth public boolean match(javax.mail.search.SearchTerm) throws javax.mail.MessagingException +meth public int getMessageNumber() +meth public javax.mail.Address[] getAllRecipients() throws javax.mail.MessagingException +meth public javax.mail.Address[] getReplyTo() throws javax.mail.MessagingException +meth public javax.mail.Folder getFolder() +meth public javax.mail.Session getSession() +meth public void addRecipient(javax.mail.Message$RecipientType,javax.mail.Address) throws javax.mail.MessagingException +meth public void setFlag(javax.mail.Flags$Flag,boolean) throws javax.mail.MessagingException +meth public void setRecipient(javax.mail.Message$RecipientType,javax.mail.Address) throws javax.mail.MessagingException +meth public void setReplyTo(javax.mail.Address[]) throws javax.mail.MessagingException +supr java.lang.Object + +CLSS public static javax.mail.Message$RecipientType + outer javax.mail.Message +cons protected <init>(java.lang.String) +fld protected java.lang.String type +fld public final static javax.mail.Message$RecipientType BCC +fld public final static javax.mail.Message$RecipientType CC +fld public final static javax.mail.Message$RecipientType TO +intf java.io.Serializable +meth protected java.lang.Object readResolve() throws java.io.ObjectStreamException +meth public java.lang.String toString() +supr java.lang.Object +hfds serialVersionUID + +CLSS public abstract interface javax.mail.MessageAware +meth public abstract javax.mail.MessageContext getMessageContext() + +CLSS public javax.mail.MessageContext +cons public <init>(javax.mail.Part) +meth public javax.mail.Message getMessage() +meth public javax.mail.Part getPart() +meth public javax.mail.Session getSession() +supr java.lang.Object +hfds part + +CLSS public javax.mail.MessageRemovedException +cons public <init>() +cons public <init>(java.lang.String) +cons public <init>(java.lang.String,java.lang.Exception) +supr javax.mail.MessagingException +hfds serialVersionUID + +CLSS public javax.mail.MessagingException +cons public <init>() +cons public <init>(java.lang.String) +cons public <init>(java.lang.String,java.lang.Exception) +meth public boolean setNextException(java.lang.Exception) +meth public java.lang.Exception getNextException() +meth public java.lang.String toString() +meth public java.lang.Throwable getCause() +supr java.lang.Exception +hfds next,serialVersionUID + +CLSS public javax.mail.MethodNotSupportedException +cons public <init>() +cons public <init>(java.lang.String) +cons public <init>(java.lang.String,java.lang.Exception) +supr javax.mail.MessagingException +hfds serialVersionUID + +CLSS public abstract javax.mail.Multipart +cons protected <init>() +fld protected java.lang.String contentType +fld protected java.util.Vector<javax.mail.BodyPart> parts +fld protected javax.mail.Part parent +meth protected void setMultipartDataSource(javax.mail.MultipartDataSource) throws javax.mail.MessagingException +meth public abstract void writeTo(java.io.OutputStream) throws java.io.IOException,javax.mail.MessagingException +meth public boolean removeBodyPart(javax.mail.BodyPart) throws javax.mail.MessagingException +meth public int getCount() throws javax.mail.MessagingException +meth public java.lang.String getContentType() +meth public javax.mail.BodyPart getBodyPart(int) throws javax.mail.MessagingException +meth public javax.mail.Part getParent() +meth public void addBodyPart(javax.mail.BodyPart) throws javax.mail.MessagingException +meth public void addBodyPart(javax.mail.BodyPart,int) throws javax.mail.MessagingException +meth public void removeBodyPart(int) throws javax.mail.MessagingException +meth public void setParent(javax.mail.Part) +supr java.lang.Object + +CLSS public abstract interface javax.mail.MultipartDataSource +intf javax.activation.DataSource +meth public abstract int getCount() +meth public abstract javax.mail.BodyPart getBodyPart(int) throws javax.mail.MessagingException + +CLSS public javax.mail.NoSuchProviderException +cons public <init>() +cons public <init>(java.lang.String) +cons public <init>(java.lang.String,java.lang.Exception) +supr javax.mail.MessagingException +hfds serialVersionUID + +CLSS public abstract interface javax.mail.Part +fld public final static java.lang.String ATTACHMENT = "attachment" +fld public final static java.lang.String INLINE = "inline" +meth public abstract boolean isMimeType(java.lang.String) throws javax.mail.MessagingException +meth public abstract int getLineCount() throws javax.mail.MessagingException +meth public abstract int getSize() throws javax.mail.MessagingException +meth public abstract java.io.InputStream getInputStream() throws java.io.IOException,javax.mail.MessagingException +meth public abstract java.lang.Object getContent() throws java.io.IOException,javax.mail.MessagingException +meth public abstract java.lang.String getContentType() throws javax.mail.MessagingException +meth public abstract java.lang.String getDescription() throws javax.mail.MessagingException +meth public abstract java.lang.String getDisposition() throws javax.mail.MessagingException +meth public abstract java.lang.String getFileName() throws javax.mail.MessagingException +meth public abstract java.lang.String[] getHeader(java.lang.String) throws javax.mail.MessagingException +meth public abstract java.util.Enumeration<javax.mail.Header> getAllHeaders() throws javax.mail.MessagingException +meth public abstract java.util.Enumeration<javax.mail.Header> getMatchingHeaders(java.lang.String[]) throws javax.mail.MessagingException +meth public abstract java.util.Enumeration<javax.mail.Header> getNonMatchingHeaders(java.lang.String[]) throws javax.mail.MessagingException +meth public abstract javax.activation.DataHandler getDataHandler() throws javax.mail.MessagingException +meth public abstract void addHeader(java.lang.String,java.lang.String) throws javax.mail.MessagingException +meth public abstract void removeHeader(java.lang.String) throws javax.mail.MessagingException +meth public abstract void setContent(java.lang.Object,java.lang.String) throws javax.mail.MessagingException +meth public abstract void setContent(javax.mail.Multipart) throws javax.mail.MessagingException +meth public abstract void setDataHandler(javax.activation.DataHandler) throws javax.mail.MessagingException +meth public abstract void setDescription(java.lang.String) throws javax.mail.MessagingException +meth public abstract void setDisposition(java.lang.String) throws javax.mail.MessagingException +meth public abstract void setFileName(java.lang.String) throws javax.mail.MessagingException +meth public abstract void setHeader(java.lang.String,java.lang.String) throws javax.mail.MessagingException +meth public abstract void setText(java.lang.String) throws javax.mail.MessagingException +meth public abstract void writeTo(java.io.OutputStream) throws java.io.IOException,javax.mail.MessagingException + +CLSS public final javax.mail.PasswordAuthentication +cons public <init>(java.lang.String,java.lang.String) +meth public java.lang.String getPassword() +meth public java.lang.String getUserName() +supr java.lang.Object +hfds password,userName + +CLSS public javax.mail.Provider +cons public <init>(javax.mail.Provider$Type,java.lang.String,java.lang.String,java.lang.String,java.lang.String) +innr public static Type +meth public java.lang.String getClassName() +meth public java.lang.String getProtocol() +meth public java.lang.String getVendor() +meth public java.lang.String getVersion() +meth public java.lang.String toString() +meth public javax.mail.Provider$Type getType() +supr java.lang.Object +hfds className,protocol,type,vendor,version + +CLSS public static javax.mail.Provider$Type + outer javax.mail.Provider +fld public final static javax.mail.Provider$Type STORE +fld public final static javax.mail.Provider$Type TRANSPORT +meth public java.lang.String toString() +supr java.lang.Object +hfds type + +CLSS public javax.mail.Quota +cons public <init>(java.lang.String) +fld public java.lang.String quotaRoot +fld public javax.mail.Quota$Resource[] resources +innr public static Resource +meth public void setResourceLimit(java.lang.String,long) +supr java.lang.Object + +CLSS public static javax.mail.Quota$Resource + outer javax.mail.Quota +cons public <init>(java.lang.String,long,long) +fld public java.lang.String name +fld public long limit +fld public long usage +supr java.lang.Object + +CLSS public abstract interface javax.mail.QuotaAwareStore +meth public abstract javax.mail.Quota[] getQuota(java.lang.String) throws javax.mail.MessagingException +meth public abstract void setQuota(javax.mail.Quota) throws javax.mail.MessagingException + +CLSS public javax.mail.ReadOnlyFolderException +cons public <init>(javax.mail.Folder) +cons public <init>(javax.mail.Folder,java.lang.String) +cons public <init>(javax.mail.Folder,java.lang.String,java.lang.Exception) +meth public javax.mail.Folder getFolder() +supr javax.mail.MessagingException +hfds folder,serialVersionUID + +CLSS public javax.mail.SendFailedException +cons public <init>() +cons public <init>(java.lang.String) +cons public <init>(java.lang.String,java.lang.Exception) +cons public <init>(java.lang.String,java.lang.Exception,javax.mail.Address[],javax.mail.Address[],javax.mail.Address[]) +fld protected javax.mail.Address[] invalid +fld protected javax.mail.Address[] validSent +fld protected javax.mail.Address[] validUnsent +meth public javax.mail.Address[] getInvalidAddresses() +meth public javax.mail.Address[] getValidSentAddresses() +meth public javax.mail.Address[] getValidUnsentAddresses() +supr javax.mail.MessagingException +hfds serialVersionUID + +CLSS public abstract javax.mail.Service +cons protected <init>(javax.mail.Session,javax.mail.URLName) +fld protected boolean debug +fld protected javax.mail.Session session +fld protected volatile javax.mail.URLName url +intf java.lang.AutoCloseable +meth protected boolean protocolConnect(java.lang.String,int,java.lang.String,java.lang.String) throws javax.mail.MessagingException +meth protected void finalize() throws java.lang.Throwable +meth protected void notifyConnectionListeners(int) +meth protected void queueEvent(javax.mail.event.MailEvent,java.util.Vector<? extends java.util.EventListener>) +meth protected void setConnected(boolean) +meth protected void setURLName(javax.mail.URLName) +meth public boolean isConnected() +meth public java.lang.String toString() +meth public javax.mail.URLName getURLName() +meth public void addConnectionListener(javax.mail.event.ConnectionListener) +meth public void close() throws javax.mail.MessagingException +meth public void connect() throws javax.mail.MessagingException +meth public void connect(java.lang.String,int,java.lang.String,java.lang.String) throws javax.mail.MessagingException +meth public void connect(java.lang.String,java.lang.String) throws javax.mail.MessagingException +meth public void connect(java.lang.String,java.lang.String,java.lang.String) throws javax.mail.MessagingException +meth public void removeConnectionListener(javax.mail.event.ConnectionListener) +supr java.lang.Object +hfds connected,connectionListeners,q + +CLSS public final javax.mail.Session +meth public boolean getDebug() +meth public java.io.PrintStream getDebugOut() +meth public java.lang.String getProperty(java.lang.String) +meth public java.util.Properties getProperties() +meth public javax.mail.Folder getFolder(javax.mail.URLName) throws javax.mail.MessagingException +meth public javax.mail.PasswordAuthentication getPasswordAuthentication(javax.mail.URLName) +meth public javax.mail.PasswordAuthentication requestPasswordAuthentication(java.net.InetAddress,int,java.lang.String,java.lang.String,java.lang.String) +meth public javax.mail.Provider getProvider(java.lang.String) throws javax.mail.NoSuchProviderException +meth public javax.mail.Provider[] getProviders() +meth public javax.mail.Store getStore() throws javax.mail.NoSuchProviderException +meth public javax.mail.Store getStore(java.lang.String) throws javax.mail.NoSuchProviderException +meth public javax.mail.Store getStore(javax.mail.Provider) throws javax.mail.NoSuchProviderException +meth public javax.mail.Store getStore(javax.mail.URLName) throws javax.mail.NoSuchProviderException +meth public javax.mail.Transport getTransport() throws javax.mail.NoSuchProviderException +meth public javax.mail.Transport getTransport(java.lang.String) throws javax.mail.NoSuchProviderException +meth public javax.mail.Transport getTransport(javax.mail.Address) throws javax.mail.NoSuchProviderException +meth public javax.mail.Transport getTransport(javax.mail.Provider) throws javax.mail.NoSuchProviderException +meth public javax.mail.Transport getTransport(javax.mail.URLName) throws javax.mail.NoSuchProviderException +meth public static javax.mail.Session getDefaultInstance(java.util.Properties) +meth public static javax.mail.Session getDefaultInstance(java.util.Properties,javax.mail.Authenticator) +meth public static javax.mail.Session getInstance(java.util.Properties) +meth public static javax.mail.Session getInstance(java.util.Properties,javax.mail.Authenticator) +meth public void addProvider(javax.mail.Provider) +meth public void setDebug(boolean) +meth public void setDebugOut(java.io.PrintStream) +meth public void setPasswordAuthentication(javax.mail.URLName,javax.mail.PasswordAuthentication) +meth public void setProtocolForAddress(java.lang.String,java.lang.String) +meth public void setProvider(javax.mail.Provider) throws javax.mail.NoSuchProviderException +supr java.lang.Object +hfds addressMap,authTable,authenticator,confDir,debug,defaultSession,logger,out,props,providers,providersByClassName,providersByProtocol,q + +CLSS public abstract javax.mail.Store +cons protected <init>(javax.mail.Session,javax.mail.URLName) +meth protected void notifyFolderListeners(int,javax.mail.Folder) +meth protected void notifyFolderRenamedListeners(javax.mail.Folder,javax.mail.Folder) +meth protected void notifyStoreListeners(int,java.lang.String) +meth public abstract javax.mail.Folder getDefaultFolder() throws javax.mail.MessagingException +meth public abstract javax.mail.Folder getFolder(java.lang.String) throws javax.mail.MessagingException +meth public abstract javax.mail.Folder getFolder(javax.mail.URLName) throws javax.mail.MessagingException +meth public javax.mail.Folder[] getPersonalNamespaces() throws javax.mail.MessagingException +meth public javax.mail.Folder[] getSharedNamespaces() throws javax.mail.MessagingException +meth public javax.mail.Folder[] getUserNamespaces(java.lang.String) throws javax.mail.MessagingException +meth public void addFolderListener(javax.mail.event.FolderListener) +meth public void addStoreListener(javax.mail.event.StoreListener) +meth public void removeFolderListener(javax.mail.event.FolderListener) +meth public void removeStoreListener(javax.mail.event.StoreListener) +supr javax.mail.Service +hfds folderListeners,storeListeners + +CLSS public javax.mail.StoreClosedException +cons public <init>(javax.mail.Store) +cons public <init>(javax.mail.Store,java.lang.String) +cons public <init>(javax.mail.Store,java.lang.String,java.lang.Exception) +meth public javax.mail.Store getStore() +supr javax.mail.MessagingException +hfds serialVersionUID,store + +CLSS public abstract javax.mail.Transport +cons public <init>(javax.mail.Session,javax.mail.URLName) +meth protected void notifyTransportListeners(int,javax.mail.Address[],javax.mail.Address[],javax.mail.Address[],javax.mail.Message) +meth public abstract void sendMessage(javax.mail.Message,javax.mail.Address[]) throws javax.mail.MessagingException +meth public static void send(javax.mail.Message) throws javax.mail.MessagingException +meth public static void send(javax.mail.Message,java.lang.String,java.lang.String) throws javax.mail.MessagingException +meth public static void send(javax.mail.Message,javax.mail.Address[]) throws javax.mail.MessagingException +meth public static void send(javax.mail.Message,javax.mail.Address[],java.lang.String,java.lang.String) throws javax.mail.MessagingException +meth public void addTransportListener(javax.mail.event.TransportListener) +meth public void removeTransportListener(javax.mail.event.TransportListener) +supr javax.mail.Service +hfds transportListeners + +CLSS public abstract interface javax.mail.UIDFolder +fld public final static long LASTUID = -1 +fld public final static long MAXUID = 4294967295 +innr public static FetchProfileItem +meth public abstract javax.mail.Message getMessageByUID(long) throws javax.mail.MessagingException +meth public abstract javax.mail.Message[] getMessagesByUID(long,long) throws javax.mail.MessagingException +meth public abstract javax.mail.Message[] getMessagesByUID(long[]) throws javax.mail.MessagingException +meth public abstract long getUID(javax.mail.Message) throws javax.mail.MessagingException +meth public abstract long getUIDNext() throws javax.mail.MessagingException +meth public abstract long getUIDValidity() throws javax.mail.MessagingException + +CLSS public static javax.mail.UIDFolder$FetchProfileItem + outer javax.mail.UIDFolder +cons protected <init>(java.lang.String) +fld public final static javax.mail.UIDFolder$FetchProfileItem UID +supr javax.mail.FetchProfile$Item + +CLSS public javax.mail.URLName +cons public <init>(java.lang.String) +cons public <init>(java.lang.String,java.lang.String,int,java.lang.String,java.lang.String,java.lang.String) +cons public <init>(java.net.URL) +fld protected java.lang.String fullURL +meth protected void parseString(java.lang.String) +meth public boolean equals(java.lang.Object) +meth public int getPort() +meth public int hashCode() +meth public java.lang.String getFile() +meth public java.lang.String getHost() +meth public java.lang.String getPassword() +meth public java.lang.String getProtocol() +meth public java.lang.String getRef() +meth public java.lang.String getUsername() +meth public java.lang.String toString() +meth public java.net.URL getURL() throws java.net.MalformedURLException +supr java.lang.Object +hfds caseDiff,doEncode,dontNeedEncoding,file,hashCode,host,hostAddress,hostAddressKnown,password,port,protocol,ref,username + +CLSS public abstract javax.mail.event.ConnectionAdapter +cons public <init>() +intf javax.mail.event.ConnectionListener +meth public void closed(javax.mail.event.ConnectionEvent) +meth public void disconnected(javax.mail.event.ConnectionEvent) +meth public void opened(javax.mail.event.ConnectionEvent) +supr java.lang.Object + +CLSS public javax.mail.event.ConnectionEvent +cons public <init>(java.lang.Object,int) +fld protected int type +fld public final static int CLOSED = 3 +fld public final static int DISCONNECTED = 2 +fld public final static int OPENED = 1 +meth public int getType() +meth public void dispatch(java.lang.Object) +supr javax.mail.event.MailEvent +hfds serialVersionUID + +CLSS public abstract interface javax.mail.event.ConnectionListener +intf java.util.EventListener +meth public abstract void closed(javax.mail.event.ConnectionEvent) +meth public abstract void disconnected(javax.mail.event.ConnectionEvent) +meth public abstract void opened(javax.mail.event.ConnectionEvent) + +CLSS public abstract javax.mail.event.FolderAdapter +cons public <init>() +intf javax.mail.event.FolderListener +meth public void folderCreated(javax.mail.event.FolderEvent) +meth public void folderDeleted(javax.mail.event.FolderEvent) +meth public void folderRenamed(javax.mail.event.FolderEvent) +supr java.lang.Object + +CLSS public javax.mail.event.FolderEvent +cons public <init>(java.lang.Object,javax.mail.Folder,int) +cons public <init>(java.lang.Object,javax.mail.Folder,javax.mail.Folder,int) +fld protected int type +fld protected javax.mail.Folder folder +fld protected javax.mail.Folder newFolder +fld public final static int CREATED = 1 +fld public final static int DELETED = 2 +fld public final static int RENAMED = 3 +meth public int getType() +meth public javax.mail.Folder getFolder() +meth public javax.mail.Folder getNewFolder() +meth public void dispatch(java.lang.Object) +supr javax.mail.event.MailEvent +hfds serialVersionUID + +CLSS public abstract interface javax.mail.event.FolderListener +intf java.util.EventListener +meth public abstract void folderCreated(javax.mail.event.FolderEvent) +meth public abstract void folderDeleted(javax.mail.event.FolderEvent) +meth public abstract void folderRenamed(javax.mail.event.FolderEvent) + +CLSS public abstract javax.mail.event.MailEvent +cons public <init>(java.lang.Object) +meth public abstract void dispatch(java.lang.Object) +supr java.util.EventObject +hfds serialVersionUID + +CLSS public javax.mail.event.MessageChangedEvent +cons public <init>(java.lang.Object,int,javax.mail.Message) +fld protected int type +fld protected javax.mail.Message msg +fld public final static int ENVELOPE_CHANGED = 2 +fld public final static int FLAGS_CHANGED = 1 +meth public int getMessageChangeType() +meth public javax.mail.Message getMessage() +meth public void dispatch(java.lang.Object) +supr javax.mail.event.MailEvent +hfds serialVersionUID + +CLSS public abstract interface javax.mail.event.MessageChangedListener +intf java.util.EventListener +meth public abstract void messageChanged(javax.mail.event.MessageChangedEvent) + +CLSS public abstract javax.mail.event.MessageCountAdapter +cons public <init>() +intf javax.mail.event.MessageCountListener +meth public void messagesAdded(javax.mail.event.MessageCountEvent) +meth public void messagesRemoved(javax.mail.event.MessageCountEvent) +supr java.lang.Object + +CLSS public javax.mail.event.MessageCountEvent +cons public <init>(javax.mail.Folder,int,boolean,javax.mail.Message[]) +fld protected boolean removed +fld protected int type +fld protected javax.mail.Message[] msgs +fld public final static int ADDED = 1 +fld public final static int REMOVED = 2 +meth public boolean isRemoved() +meth public int getType() +meth public javax.mail.Message[] getMessages() +meth public void dispatch(java.lang.Object) +supr javax.mail.event.MailEvent +hfds serialVersionUID + +CLSS public abstract interface javax.mail.event.MessageCountListener +intf java.util.EventListener +meth public abstract void messagesAdded(javax.mail.event.MessageCountEvent) +meth public abstract void messagesRemoved(javax.mail.event.MessageCountEvent) + +CLSS public javax.mail.event.StoreEvent +cons public <init>(javax.mail.Store,int,java.lang.String) +fld protected int type +fld protected java.lang.String message +fld public final static int ALERT = 1 +fld public final static int NOTICE = 2 +meth public int getMessageType() +meth public java.lang.String getMessage() +meth public void dispatch(java.lang.Object) +supr javax.mail.event.MailEvent +hfds serialVersionUID + +CLSS public abstract interface javax.mail.event.StoreListener +intf java.util.EventListener +meth public abstract void notification(javax.mail.event.StoreEvent) + +CLSS public abstract javax.mail.event.TransportAdapter +cons public <init>() +intf javax.mail.event.TransportListener +meth public void messageDelivered(javax.mail.event.TransportEvent) +meth public void messageNotDelivered(javax.mail.event.TransportEvent) +meth public void messagePartiallyDelivered(javax.mail.event.TransportEvent) +supr java.lang.Object + +CLSS public javax.mail.event.TransportEvent +cons public <init>(javax.mail.Transport,int,javax.mail.Address[],javax.mail.Address[],javax.mail.Address[],javax.mail.Message) +fld protected int type +fld protected javax.mail.Address[] invalid +fld protected javax.mail.Address[] validSent +fld protected javax.mail.Address[] validUnsent +fld protected javax.mail.Message msg +fld public final static int MESSAGE_DELIVERED = 1 +fld public final static int MESSAGE_NOT_DELIVERED = 2 +fld public final static int MESSAGE_PARTIALLY_DELIVERED = 3 +meth public int getType() +meth public javax.mail.Address[] getInvalidAddresses() +meth public javax.mail.Address[] getValidSentAddresses() +meth public javax.mail.Address[] getValidUnsentAddresses() +meth public javax.mail.Message getMessage() +meth public void dispatch(java.lang.Object) +supr javax.mail.event.MailEvent +hfds serialVersionUID + +CLSS public abstract interface javax.mail.event.TransportListener +intf java.util.EventListener +meth public abstract void messageDelivered(javax.mail.event.TransportEvent) +meth public abstract void messageNotDelivered(javax.mail.event.TransportEvent) +meth public abstract void messagePartiallyDelivered(javax.mail.event.TransportEvent) + +CLSS public javax.mail.internet.AddressException +cons public <init>() +cons public <init>(java.lang.String) +cons public <init>(java.lang.String,java.lang.String) +cons public <init>(java.lang.String,java.lang.String,int) +fld protected int pos +fld protected java.lang.String ref +meth public int getPos() +meth public java.lang.String getRef() +meth public java.lang.String toString() +supr javax.mail.internet.ParseException +hfds serialVersionUID + +CLSS public javax.mail.internet.ContentDisposition +cons public <init>() +cons public <init>(java.lang.String) throws javax.mail.internet.ParseException +cons public <init>(java.lang.String,javax.mail.internet.ParameterList) +meth public java.lang.String getDisposition() +meth public java.lang.String getParameter(java.lang.String) +meth public java.lang.String toString() +meth public javax.mail.internet.ParameterList getParameterList() +meth public void setDisposition(java.lang.String) +meth public void setParameter(java.lang.String,java.lang.String) +meth public void setParameterList(javax.mail.internet.ParameterList) +supr java.lang.Object +hfds disposition,list + +CLSS public javax.mail.internet.ContentType +cons public <init>() +cons public <init>(java.lang.String) throws javax.mail.internet.ParseException +cons public <init>(java.lang.String,java.lang.String,javax.mail.internet.ParameterList) +meth public boolean match(java.lang.String) +meth public boolean match(javax.mail.internet.ContentType) +meth public java.lang.String getBaseType() +meth public java.lang.String getParameter(java.lang.String) +meth public java.lang.String getPrimaryType() +meth public java.lang.String getSubType() +meth public java.lang.String toString() +meth public javax.mail.internet.ParameterList getParameterList() +meth public void setParameter(java.lang.String,java.lang.String) +meth public void setParameterList(javax.mail.internet.ParameterList) +meth public void setPrimaryType(java.lang.String) +meth public void setSubType(java.lang.String) +supr java.lang.Object +hfds list,primaryType,subType + +CLSS public javax.mail.internet.HeaderTokenizer +cons public <init>(java.lang.String) +cons public <init>(java.lang.String,java.lang.String) +cons public <init>(java.lang.String,java.lang.String,boolean) +fld public final static java.lang.String MIME = "()<>@,;:\u005c\u0022\u0009 []/?=" +fld public final static java.lang.String RFC822 = "()<>@,;:\u005c\u0022\u0009 .[]" +innr public static Token +meth public java.lang.String getRemainder() +meth public javax.mail.internet.HeaderTokenizer$Token next() throws javax.mail.internet.ParseException +meth public javax.mail.internet.HeaderTokenizer$Token next(char) throws javax.mail.internet.ParseException +meth public javax.mail.internet.HeaderTokenizer$Token next(char,boolean) throws javax.mail.internet.ParseException +meth public javax.mail.internet.HeaderTokenizer$Token peek() throws javax.mail.internet.ParseException +supr java.lang.Object +hfds EOFToken,currentPos,delimiters,maxPos,nextPos,peekPos,skipComments,string + +CLSS public static javax.mail.internet.HeaderTokenizer$Token + outer javax.mail.internet.HeaderTokenizer +cons public <init>(int,java.lang.String) +fld public final static int ATOM = -1 +fld public final static int COMMENT = -3 +fld public final static int EOF = -4 +fld public final static int QUOTEDSTRING = -2 +meth public int getType() +meth public java.lang.String getValue() +supr java.lang.Object +hfds type,value + +CLSS public javax.mail.internet.InternetAddress +cons public <init>() +cons public <init>(java.lang.String) throws javax.mail.internet.AddressException +cons public <init>(java.lang.String,boolean) throws javax.mail.internet.AddressException +cons public <init>(java.lang.String,java.lang.String) throws java.io.UnsupportedEncodingException +cons public <init>(java.lang.String,java.lang.String,java.lang.String) throws java.io.UnsupportedEncodingException +fld protected java.lang.String address +fld protected java.lang.String encodedPersonal +fld protected java.lang.String personal +intf java.lang.Cloneable +meth public boolean equals(java.lang.Object) +meth public boolean isGroup() +meth public int hashCode() +meth public java.lang.Object clone() +meth public java.lang.String getAddress() +meth public java.lang.String getPersonal() +meth public java.lang.String getType() +meth public java.lang.String toString() +meth public java.lang.String toUnicodeString() +meth public javax.mail.internet.InternetAddress[] getGroup(boolean) throws javax.mail.internet.AddressException +meth public static java.lang.String toString(javax.mail.Address[]) +meth public static java.lang.String toString(javax.mail.Address[],int) +meth public static java.lang.String toUnicodeString(javax.mail.Address[]) +meth public static java.lang.String toUnicodeString(javax.mail.Address[],int) +meth public static javax.mail.internet.InternetAddress getLocalAddress(javax.mail.Session) +meth public static javax.mail.internet.InternetAddress[] parse(java.lang.String) throws javax.mail.internet.AddressException +meth public static javax.mail.internet.InternetAddress[] parse(java.lang.String,boolean) throws javax.mail.internet.AddressException +meth public static javax.mail.internet.InternetAddress[] parseHeader(java.lang.String,boolean) throws javax.mail.internet.AddressException +meth public void setAddress(java.lang.String) +meth public void setPersonal(java.lang.String) throws java.io.UnsupportedEncodingException +meth public void setPersonal(java.lang.String,java.lang.String) throws java.io.UnsupportedEncodingException +meth public void validate() throws javax.mail.internet.AddressException +supr javax.mail.Address +hfds allowUtf8,ignoreBogusGroupName,rfc822phrase,serialVersionUID,specialsNoDot,specialsNoDotNoAt,useCanonicalHostName + +CLSS public javax.mail.internet.InternetHeaders +cons public <init>() +cons public <init>(java.io.InputStream) throws javax.mail.MessagingException +cons public <init>(java.io.InputStream,boolean) throws javax.mail.MessagingException +fld protected java.util.List<javax.mail.internet.InternetHeaders$InternetHeader> headers +innr protected final static InternetHeader +meth public java.lang.String getHeader(java.lang.String,java.lang.String) +meth public java.lang.String[] getHeader(java.lang.String) +meth public java.util.Enumeration<java.lang.String> getAllHeaderLines() +meth public java.util.Enumeration<java.lang.String> getMatchingHeaderLines(java.lang.String[]) +meth public java.util.Enumeration<java.lang.String> getNonMatchingHeaderLines(java.lang.String[]) +meth public java.util.Enumeration<javax.mail.Header> getAllHeaders() +meth public java.util.Enumeration<javax.mail.Header> getMatchingHeaders(java.lang.String[]) +meth public java.util.Enumeration<javax.mail.Header> getNonMatchingHeaders(java.lang.String[]) +meth public void addHeader(java.lang.String,java.lang.String) +meth public void addHeaderLine(java.lang.String) +meth public void load(java.io.InputStream) throws javax.mail.MessagingException +meth public void load(java.io.InputStream,boolean) throws javax.mail.MessagingException +meth public void removeHeader(java.lang.String) +meth public void setHeader(java.lang.String,java.lang.String) +supr java.lang.Object +hfds ignoreWhitespaceLines +hcls MatchEnum,MatchHeaderEnum,MatchStringEnum + +CLSS protected final static javax.mail.internet.InternetHeaders$InternetHeader + outer javax.mail.internet.InternetHeaders +cons public <init>(java.lang.String) +cons public <init>(java.lang.String,java.lang.String) +meth public java.lang.String getValue() +supr javax.mail.Header +hfds line + +CLSS public javax.mail.internet.MailDateFormat +cons public <init>() +meth public java.lang.StringBuffer format(java.util.Date,java.lang.StringBuffer,java.text.FieldPosition) +meth public java.util.Date get2DigitYearStart() +meth public java.util.Date parse(java.lang.String,java.text.ParsePosition) +meth public javax.mail.internet.MailDateFormat clone() +meth public void applyLocalizedPattern(java.lang.String) +meth public void applyPattern(java.lang.String) +meth public void set2DigitYearStart(java.util.Date) +meth public void setCalendar(java.util.Calendar) +meth public void setDateFormatSymbols(java.text.DateFormatSymbols) +meth public void setNumberFormat(java.text.NumberFormat) +supr java.text.SimpleDateFormat +hfds LEAP_SECOND,LOGGER,PATTERN,UNKNOWN_DAY_NAME,UTC,serialVersionUID +hcls AbstractDateParser,Rfc2822LenientParser,Rfc2822StrictParser + +CLSS public javax.mail.internet.MimeBodyPart +cons public <init>() +cons public <init>(java.io.InputStream) throws javax.mail.MessagingException +cons public <init>(javax.mail.internet.InternetHeaders,byte[]) throws javax.mail.MessagingException +fld protected byte[] content +fld protected java.io.InputStream contentStream +fld protected java.lang.Object cachedContent +fld protected javax.activation.DataHandler dh +fld protected javax.mail.internet.InternetHeaders headers +intf javax.mail.internet.MimePart +meth protected java.io.InputStream getContentStream() throws javax.mail.MessagingException +meth protected void updateHeaders() throws javax.mail.MessagingException +meth public boolean isMimeType(java.lang.String) throws javax.mail.MessagingException +meth public int getLineCount() throws javax.mail.MessagingException +meth public int getSize() throws javax.mail.MessagingException +meth public java.io.InputStream getInputStream() throws java.io.IOException,javax.mail.MessagingException +meth public java.io.InputStream getRawInputStream() throws javax.mail.MessagingException +meth public java.lang.Object getContent() throws java.io.IOException,javax.mail.MessagingException +meth public java.lang.String getContentID() throws javax.mail.MessagingException +meth public java.lang.String getContentMD5() throws javax.mail.MessagingException +meth public java.lang.String getContentType() throws javax.mail.MessagingException +meth public java.lang.String getDescription() throws javax.mail.MessagingException +meth public java.lang.String getDisposition() throws javax.mail.MessagingException +meth public java.lang.String getEncoding() throws javax.mail.MessagingException +meth public java.lang.String getFileName() throws javax.mail.MessagingException +meth public java.lang.String getHeader(java.lang.String,java.lang.String) throws javax.mail.MessagingException +meth public java.lang.String[] getContentLanguage() throws javax.mail.MessagingException +meth public java.lang.String[] getHeader(java.lang.String) throws javax.mail.MessagingException +meth public java.util.Enumeration<java.lang.String> getAllHeaderLines() throws javax.mail.MessagingException +meth public java.util.Enumeration<java.lang.String> getMatchingHeaderLines(java.lang.String[]) throws javax.mail.MessagingException +meth public java.util.Enumeration<java.lang.String> getNonMatchingHeaderLines(java.lang.String[]) throws javax.mail.MessagingException +meth public java.util.Enumeration<javax.mail.Header> getAllHeaders() throws javax.mail.MessagingException +meth public java.util.Enumeration<javax.mail.Header> getMatchingHeaders(java.lang.String[]) throws javax.mail.MessagingException +meth public java.util.Enumeration<javax.mail.Header> getNonMatchingHeaders(java.lang.String[]) throws javax.mail.MessagingException +meth public javax.activation.DataHandler getDataHandler() throws javax.mail.MessagingException +meth public void addHeader(java.lang.String,java.lang.String) throws javax.mail.MessagingException +meth public void addHeaderLine(java.lang.String) throws javax.mail.MessagingException +meth public void attachFile(java.io.File) throws java.io.IOException,javax.mail.MessagingException +meth public void attachFile(java.io.File,java.lang.String,java.lang.String) throws java.io.IOException,javax.mail.MessagingException +meth public void attachFile(java.lang.String) throws java.io.IOException,javax.mail.MessagingException +meth public void attachFile(java.lang.String,java.lang.String,java.lang.String) throws java.io.IOException,javax.mail.MessagingException +meth public void removeHeader(java.lang.String) throws javax.mail.MessagingException +meth public void saveFile(java.io.File) throws java.io.IOException,javax.mail.MessagingException +meth public void saveFile(java.lang.String) throws java.io.IOException,javax.mail.MessagingException +meth public void setContent(java.lang.Object,java.lang.String) throws javax.mail.MessagingException +meth public void setContent(javax.mail.Multipart) throws javax.mail.MessagingException +meth public void setContentID(java.lang.String) throws javax.mail.MessagingException +meth public void setContentLanguage(java.lang.String[]) throws javax.mail.MessagingException +meth public void setContentMD5(java.lang.String) throws javax.mail.MessagingException +meth public void setDataHandler(javax.activation.DataHandler) throws javax.mail.MessagingException +meth public void setDescription(java.lang.String) throws javax.mail.MessagingException +meth public void setDescription(java.lang.String,java.lang.String) throws javax.mail.MessagingException +meth public void setDisposition(java.lang.String) throws javax.mail.MessagingException +meth public void setFileName(java.lang.String) throws javax.mail.MessagingException +meth public void setHeader(java.lang.String,java.lang.String) throws javax.mail.MessagingException +meth public void setText(java.lang.String) throws javax.mail.MessagingException +meth public void setText(java.lang.String,java.lang.String) throws javax.mail.MessagingException +meth public void setText(java.lang.String,java.lang.String,java.lang.String) throws javax.mail.MessagingException +meth public void writeTo(java.io.OutputStream) throws java.io.IOException,javax.mail.MessagingException +supr javax.mail.BodyPart +hfds allowutf8,cacheMultipart,decodeFileName,encodeFileName,ignoreMultipartEncoding,setContentTypeFileName,setDefaultTextCharset +hcls EncodedFileDataSource,MimePartDataHandler + +CLSS public javax.mail.internet.MimeMessage +cons protected <init>(javax.mail.Folder,int) +cons protected <init>(javax.mail.Folder,java.io.InputStream,int) throws javax.mail.MessagingException +cons protected <init>(javax.mail.Folder,javax.mail.internet.InternetHeaders,byte[],int) throws javax.mail.MessagingException +cons public <init>(javax.mail.Session) +cons public <init>(javax.mail.Session,java.io.InputStream) throws javax.mail.MessagingException +cons public <init>(javax.mail.internet.MimeMessage) throws javax.mail.MessagingException +fld protected boolean modified +fld protected boolean saved +fld protected byte[] content +fld protected java.io.InputStream contentStream +fld protected java.lang.Object cachedContent +fld protected javax.activation.DataHandler dh +fld protected javax.mail.Flags flags +fld protected javax.mail.internet.InternetHeaders headers +innr public static RecipientType +intf javax.mail.internet.MimePart +meth protected java.io.InputStream getContentStream() throws javax.mail.MessagingException +meth protected javax.mail.internet.InternetHeaders createInternetHeaders(java.io.InputStream) throws javax.mail.MessagingException +meth protected javax.mail.internet.MimeMessage createMimeMessage(javax.mail.Session) throws javax.mail.MessagingException +meth protected void parse(java.io.InputStream) throws javax.mail.MessagingException +meth protected void updateHeaders() throws javax.mail.MessagingException +meth protected void updateMessageID() throws javax.mail.MessagingException +meth public boolean isMimeType(java.lang.String) throws javax.mail.MessagingException +meth public boolean isSet(javax.mail.Flags$Flag) throws javax.mail.MessagingException +meth public int getLineCount() throws javax.mail.MessagingException +meth public int getSize() throws javax.mail.MessagingException +meth public java.io.InputStream getInputStream() throws java.io.IOException,javax.mail.MessagingException +meth public java.io.InputStream getRawInputStream() throws javax.mail.MessagingException +meth public java.lang.Object getContent() throws java.io.IOException,javax.mail.MessagingException +meth public java.lang.String getContentID() throws javax.mail.MessagingException +meth public java.lang.String getContentMD5() throws javax.mail.MessagingException +meth public java.lang.String getContentType() throws javax.mail.MessagingException +meth public java.lang.String getDescription() throws javax.mail.MessagingException +meth public java.lang.String getDisposition() throws javax.mail.MessagingException +meth public java.lang.String getEncoding() throws javax.mail.MessagingException +meth public java.lang.String getFileName() throws javax.mail.MessagingException +meth public java.lang.String getHeader(java.lang.String,java.lang.String) throws javax.mail.MessagingException +meth public java.lang.String getMessageID() throws javax.mail.MessagingException +meth public java.lang.String getSubject() throws javax.mail.MessagingException +meth public java.lang.String[] getContentLanguage() throws javax.mail.MessagingException +meth public java.lang.String[] getHeader(java.lang.String) throws javax.mail.MessagingException +meth public java.util.Date getReceivedDate() throws javax.mail.MessagingException +meth public java.util.Date getSentDate() throws javax.mail.MessagingException +meth public java.util.Enumeration<java.lang.String> getAllHeaderLines() throws javax.mail.MessagingException +meth public java.util.Enumeration<java.lang.String> getMatchingHeaderLines(java.lang.String[]) throws javax.mail.MessagingException +meth public java.util.Enumeration<java.lang.String> getNonMatchingHeaderLines(java.lang.String[]) throws javax.mail.MessagingException +meth public java.util.Enumeration<javax.mail.Header> getAllHeaders() throws javax.mail.MessagingException +meth public java.util.Enumeration<javax.mail.Header> getMatchingHeaders(java.lang.String[]) throws javax.mail.MessagingException +meth public java.util.Enumeration<javax.mail.Header> getNonMatchingHeaders(java.lang.String[]) throws javax.mail.MessagingException +meth public javax.activation.DataHandler getDataHandler() throws javax.mail.MessagingException +meth public javax.mail.Address getSender() throws javax.mail.MessagingException +meth public javax.mail.Address[] getAllRecipients() throws javax.mail.MessagingException +meth public javax.mail.Address[] getFrom() throws javax.mail.MessagingException +meth public javax.mail.Address[] getRecipients(javax.mail.Message$RecipientType) throws javax.mail.MessagingException +meth public javax.mail.Address[] getReplyTo() throws javax.mail.MessagingException +meth public javax.mail.Flags getFlags() throws javax.mail.MessagingException +meth public javax.mail.Message reply(boolean) throws javax.mail.MessagingException +meth public javax.mail.Message reply(boolean,boolean) throws javax.mail.MessagingException +meth public void addFrom(javax.mail.Address[]) throws javax.mail.MessagingException +meth public void addHeader(java.lang.String,java.lang.String) throws javax.mail.MessagingException +meth public void addHeaderLine(java.lang.String) throws javax.mail.MessagingException +meth public void addRecipients(javax.mail.Message$RecipientType,java.lang.String) throws javax.mail.MessagingException +meth public void addRecipients(javax.mail.Message$RecipientType,javax.mail.Address[]) throws javax.mail.MessagingException +meth public void removeHeader(java.lang.String) throws javax.mail.MessagingException +meth public void saveChanges() throws javax.mail.MessagingException +meth public void setContent(java.lang.Object,java.lang.String) throws javax.mail.MessagingException +meth public void setContent(javax.mail.Multipart) throws javax.mail.MessagingException +meth public void setContentID(java.lang.String) throws javax.mail.MessagingException +meth public void setContentLanguage(java.lang.String[]) throws javax.mail.MessagingException +meth public void setContentMD5(java.lang.String) throws javax.mail.MessagingException +meth public void setDataHandler(javax.activation.DataHandler) throws javax.mail.MessagingException +meth public void setDescription(java.lang.String) throws javax.mail.MessagingException +meth public void setDescription(java.lang.String,java.lang.String) throws javax.mail.MessagingException +meth public void setDisposition(java.lang.String) throws javax.mail.MessagingException +meth public void setFileName(java.lang.String) throws javax.mail.MessagingException +meth public void setFlags(javax.mail.Flags,boolean) throws javax.mail.MessagingException +meth public void setFrom() throws javax.mail.MessagingException +meth public void setFrom(java.lang.String) throws javax.mail.MessagingException +meth public void setFrom(javax.mail.Address) throws javax.mail.MessagingException +meth public void setHeader(java.lang.String,java.lang.String) throws javax.mail.MessagingException +meth public void setRecipients(javax.mail.Message$RecipientType,java.lang.String) throws javax.mail.MessagingException +meth public void setRecipients(javax.mail.Message$RecipientType,javax.mail.Address[]) throws javax.mail.MessagingException +meth public void setReplyTo(javax.mail.Address[]) throws javax.mail.MessagingException +meth public void setSender(javax.mail.Address) throws javax.mail.MessagingException +meth public void setSentDate(java.util.Date) throws javax.mail.MessagingException +meth public void setSubject(java.lang.String) throws javax.mail.MessagingException +meth public void setSubject(java.lang.String,java.lang.String) throws javax.mail.MessagingException +meth public void setText(java.lang.String) throws javax.mail.MessagingException +meth public void setText(java.lang.String,java.lang.String) throws javax.mail.MessagingException +meth public void setText(java.lang.String,java.lang.String,java.lang.String) throws javax.mail.MessagingException +meth public void writeTo(java.io.OutputStream) throws java.io.IOException,javax.mail.MessagingException +meth public void writeTo(java.io.OutputStream,java.lang.String[]) throws java.io.IOException,javax.mail.MessagingException +supr javax.mail.Message +hfds allowutf8,answeredFlag,mailDateFormat,strict + +CLSS public static javax.mail.internet.MimeMessage$RecipientType + outer javax.mail.internet.MimeMessage +cons protected <init>(java.lang.String) +fld public final static javax.mail.internet.MimeMessage$RecipientType NEWSGROUPS +meth protected java.lang.Object readResolve() throws java.io.ObjectStreamException +supr javax.mail.Message$RecipientType +hfds serialVersionUID + +CLSS public javax.mail.internet.MimeMultipart +cons public !varargs <init>(java.lang.String,javax.mail.BodyPart[]) throws javax.mail.MessagingException +cons public !varargs <init>(javax.mail.BodyPart[]) throws javax.mail.MessagingException +cons public <init>() +cons public <init>(java.lang.String) +cons public <init>(javax.activation.DataSource) throws javax.mail.MessagingException +fld protected boolean allowEmpty +fld protected boolean complete +fld protected boolean ignoreExistingBoundaryParameter +fld protected boolean ignoreMissingBoundaryParameter +fld protected boolean ignoreMissingEndBoundary +fld protected boolean parsed +fld protected java.lang.String preamble +fld protected javax.activation.DataSource ds +meth protected javax.mail.internet.InternetHeaders createInternetHeaders(java.io.InputStream) throws javax.mail.MessagingException +meth protected javax.mail.internet.MimeBodyPart createMimeBodyPart(java.io.InputStream) throws javax.mail.MessagingException +meth protected javax.mail.internet.MimeBodyPart createMimeBodyPart(javax.mail.internet.InternetHeaders,byte[]) throws javax.mail.MessagingException +meth protected void initializeProperties() +meth protected void parse() throws javax.mail.MessagingException +meth protected void updateHeaders() throws javax.mail.MessagingException +meth public boolean isComplete() throws javax.mail.MessagingException +meth public boolean removeBodyPart(javax.mail.BodyPart) throws javax.mail.MessagingException +meth public int getCount() throws javax.mail.MessagingException +meth public java.lang.String getPreamble() throws javax.mail.MessagingException +meth public javax.mail.BodyPart getBodyPart(int) throws javax.mail.MessagingException +meth public javax.mail.BodyPart getBodyPart(java.lang.String) throws javax.mail.MessagingException +meth public void addBodyPart(javax.mail.BodyPart) throws javax.mail.MessagingException +meth public void addBodyPart(javax.mail.BodyPart,int) throws javax.mail.MessagingException +meth public void removeBodyPart(int) throws javax.mail.MessagingException +meth public void setPreamble(java.lang.String) throws javax.mail.MessagingException +meth public void setSubType(java.lang.String) throws javax.mail.MessagingException +meth public void writeTo(java.io.OutputStream) throws java.io.IOException,javax.mail.MessagingException +supr javax.mail.Multipart + +CLSS public abstract interface javax.mail.internet.MimePart +intf javax.mail.Part +meth public abstract java.lang.String getContentID() throws javax.mail.MessagingException +meth public abstract java.lang.String getContentMD5() throws javax.mail.MessagingException +meth public abstract java.lang.String getEncoding() throws javax.mail.MessagingException +meth public abstract java.lang.String getHeader(java.lang.String,java.lang.String) throws javax.mail.MessagingException +meth public abstract java.lang.String[] getContentLanguage() throws javax.mail.MessagingException +meth public abstract java.util.Enumeration<java.lang.String> getAllHeaderLines() throws javax.mail.MessagingException +meth public abstract java.util.Enumeration<java.lang.String> getMatchingHeaderLines(java.lang.String[]) throws javax.mail.MessagingException +meth public abstract java.util.Enumeration<java.lang.String> getNonMatchingHeaderLines(java.lang.String[]) throws javax.mail.MessagingException +meth public abstract void addHeaderLine(java.lang.String) throws javax.mail.MessagingException +meth public abstract void setContentLanguage(java.lang.String[]) throws javax.mail.MessagingException +meth public abstract void setContentMD5(java.lang.String) throws javax.mail.MessagingException +meth public abstract void setText(java.lang.String) throws javax.mail.MessagingException +meth public abstract void setText(java.lang.String,java.lang.String) throws javax.mail.MessagingException +meth public abstract void setText(java.lang.String,java.lang.String,java.lang.String) throws javax.mail.MessagingException + +CLSS public javax.mail.internet.MimePartDataSource +cons public <init>(javax.mail.internet.MimePart) +fld protected javax.mail.internet.MimePart part +intf javax.activation.DataSource +intf javax.mail.MessageAware +meth public java.io.InputStream getInputStream() throws java.io.IOException +meth public java.io.OutputStream getOutputStream() throws java.io.IOException +meth public java.lang.String getContentType() +meth public java.lang.String getName() +meth public javax.mail.MessageContext getMessageContext() +supr java.lang.Object +hfds context + +CLSS public javax.mail.internet.MimeUtility +fld public final static int ALL = -1 +meth public static java.io.InputStream decode(java.io.InputStream,java.lang.String) throws javax.mail.MessagingException +meth public static java.io.OutputStream encode(java.io.OutputStream,java.lang.String) throws javax.mail.MessagingException +meth public static java.io.OutputStream encode(java.io.OutputStream,java.lang.String,java.lang.String) throws javax.mail.MessagingException +meth public static java.lang.String decodeText(java.lang.String) throws java.io.UnsupportedEncodingException +meth public static java.lang.String decodeWord(java.lang.String) throws java.io.UnsupportedEncodingException,javax.mail.internet.ParseException +meth public static java.lang.String encodeText(java.lang.String) throws java.io.UnsupportedEncodingException +meth public static java.lang.String encodeText(java.lang.String,java.lang.String,java.lang.String) throws java.io.UnsupportedEncodingException +meth public static java.lang.String encodeWord(java.lang.String) throws java.io.UnsupportedEncodingException +meth public static java.lang.String encodeWord(java.lang.String,java.lang.String,java.lang.String) throws java.io.UnsupportedEncodingException +meth public static java.lang.String fold(int,java.lang.String) +meth public static java.lang.String getDefaultJavaCharset() +meth public static java.lang.String getEncoding(javax.activation.DataHandler) +meth public static java.lang.String getEncoding(javax.activation.DataSource) +meth public static java.lang.String javaCharset(java.lang.String) +meth public static java.lang.String mimeCharset(java.lang.String) +meth public static java.lang.String quote(java.lang.String,java.lang.String) +meth public static java.lang.String unfold(java.lang.String) +supr java.lang.Object +hfds ALL_ASCII,MOSTLY_ASCII,MOSTLY_NONASCII,allowUtf8,decodeStrict,defaultJavaCharset,defaultMIMECharset,encodeEolStrict,foldEncodedWords,foldText,ignoreUnknownEncoding,java2mime,mime2java,nonAsciiCharsetMap + +CLSS public javax.mail.internet.NewsAddress +cons public <init>() +cons public <init>(java.lang.String) +cons public <init>(java.lang.String,java.lang.String) +fld protected java.lang.String host +fld protected java.lang.String newsgroup +meth public boolean equals(java.lang.Object) +meth public int hashCode() +meth public java.lang.String getHost() +meth public java.lang.String getNewsgroup() +meth public java.lang.String getType() +meth public java.lang.String toString() +meth public static java.lang.String toString(javax.mail.Address[]) +meth public static javax.mail.internet.NewsAddress[] parse(java.lang.String) throws javax.mail.internet.AddressException +meth public void setHost(java.lang.String) +meth public void setNewsgroup(java.lang.String) +supr javax.mail.Address +hfds serialVersionUID + +CLSS public javax.mail.internet.ParameterList +cons public <init>() +cons public <init>(java.lang.String) throws javax.mail.internet.ParseException +meth public int size() +meth public java.lang.String get(java.lang.String) +meth public java.lang.String toString() +meth public java.lang.String toString(int) +meth public java.util.Enumeration<java.lang.String> getNames() +meth public void combineSegments() +meth public void remove(java.lang.String) +meth public void set(java.lang.String,java.lang.String) +meth public void set(java.lang.String,java.lang.String,java.lang.String) +supr java.lang.Object +hfds applehack,decodeParameters,decodeParametersStrict,encodeParameters,hex,lastName,list,multisegmentNames,parametersStrict,slist,splitLongParameters,windowshack +hcls LiteralValue,MultiValue,ParamEnum,ToStringBuffer,Value + +CLSS public javax.mail.internet.ParseException +cons public <init>() +cons public <init>(java.lang.String) +supr javax.mail.MessagingException +hfds serialVersionUID + +CLSS public javax.mail.internet.PreencodedMimeBodyPart +cons public <init>(java.lang.String) +meth protected void updateHeaders() throws javax.mail.MessagingException +meth public java.lang.String getEncoding() throws javax.mail.MessagingException +meth public void writeTo(java.io.OutputStream) throws java.io.IOException,javax.mail.MessagingException +supr javax.mail.internet.MimeBodyPart +hfds encoding + +CLSS public abstract interface javax.mail.internet.SharedInputStream +meth public abstract java.io.InputStream newStream(long,long) +meth public abstract long getPosition() + +CLSS public abstract javax.mail.search.AddressStringTerm +cons protected <init>(java.lang.String) +meth protected boolean match(javax.mail.Address) +meth public boolean equals(java.lang.Object) +supr javax.mail.search.StringTerm +hfds serialVersionUID + +CLSS public abstract javax.mail.search.AddressTerm +cons protected <init>(javax.mail.Address) +fld protected javax.mail.Address address +meth protected boolean match(javax.mail.Address) +meth public boolean equals(java.lang.Object) +meth public int hashCode() +meth public javax.mail.Address getAddress() +supr javax.mail.search.SearchTerm +hfds serialVersionUID + +CLSS public final javax.mail.search.AndTerm +cons public <init>(javax.mail.search.SearchTerm,javax.mail.search.SearchTerm) +cons public <init>(javax.mail.search.SearchTerm[]) +meth public boolean equals(java.lang.Object) +meth public boolean match(javax.mail.Message) +meth public int hashCode() +meth public javax.mail.search.SearchTerm[] getTerms() +supr javax.mail.search.SearchTerm +hfds serialVersionUID,terms + +CLSS public final javax.mail.search.BodyTerm +cons public <init>(java.lang.String) +meth public boolean equals(java.lang.Object) +meth public boolean match(javax.mail.Message) +supr javax.mail.search.StringTerm +hfds serialVersionUID + +CLSS public abstract javax.mail.search.ComparisonTerm +cons public <init>() +fld protected int comparison +fld public final static int EQ = 3 +fld public final static int GE = 6 +fld public final static int GT = 5 +fld public final static int LE = 1 +fld public final static int LT = 2 +fld public final static int NE = 4 +meth public boolean equals(java.lang.Object) +meth public int hashCode() +supr javax.mail.search.SearchTerm +hfds serialVersionUID + +CLSS public abstract javax.mail.search.DateTerm +cons protected <init>(int,java.util.Date) +fld protected java.util.Date date +meth protected boolean match(java.util.Date) +meth public boolean equals(java.lang.Object) +meth public int getComparison() +meth public int hashCode() +meth public java.util.Date getDate() +supr javax.mail.search.ComparisonTerm +hfds serialVersionUID + +CLSS public final javax.mail.search.FlagTerm +cons public <init>(javax.mail.Flags,boolean) +meth public boolean equals(java.lang.Object) +meth public boolean getTestSet() +meth public boolean match(javax.mail.Message) +meth public int hashCode() +meth public javax.mail.Flags getFlags() +supr javax.mail.search.SearchTerm +hfds flags,serialVersionUID,set + +CLSS public final javax.mail.search.FromStringTerm +cons public <init>(java.lang.String) +meth public boolean equals(java.lang.Object) +meth public boolean match(javax.mail.Message) +supr javax.mail.search.AddressStringTerm +hfds serialVersionUID + +CLSS public final javax.mail.search.FromTerm +cons public <init>(javax.mail.Address) +meth public boolean equals(java.lang.Object) +meth public boolean match(javax.mail.Message) +supr javax.mail.search.AddressTerm +hfds serialVersionUID + +CLSS public final javax.mail.search.HeaderTerm +cons public <init>(java.lang.String,java.lang.String) +meth public boolean equals(java.lang.Object) +meth public boolean match(javax.mail.Message) +meth public int hashCode() +meth public java.lang.String getHeaderName() +supr javax.mail.search.StringTerm +hfds headerName,serialVersionUID + +CLSS public abstract javax.mail.search.IntegerComparisonTerm +cons protected <init>(int,int) +fld protected int number +meth protected boolean match(int) +meth public boolean equals(java.lang.Object) +meth public int getComparison() +meth public int getNumber() +meth public int hashCode() +supr javax.mail.search.ComparisonTerm +hfds serialVersionUID + +CLSS public final javax.mail.search.MessageIDTerm +cons public <init>(java.lang.String) +meth public boolean equals(java.lang.Object) +meth public boolean match(javax.mail.Message) +supr javax.mail.search.StringTerm +hfds serialVersionUID + +CLSS public final javax.mail.search.MessageNumberTerm +cons public <init>(int) +meth public boolean equals(java.lang.Object) +meth public boolean match(javax.mail.Message) +supr javax.mail.search.IntegerComparisonTerm +hfds serialVersionUID + +CLSS public final javax.mail.search.NotTerm +cons public <init>(javax.mail.search.SearchTerm) +meth public boolean equals(java.lang.Object) +meth public boolean match(javax.mail.Message) +meth public int hashCode() +meth public javax.mail.search.SearchTerm getTerm() +supr javax.mail.search.SearchTerm +hfds serialVersionUID,term + +CLSS public final javax.mail.search.OrTerm +cons public <init>(javax.mail.search.SearchTerm,javax.mail.search.SearchTerm) +cons public <init>(javax.mail.search.SearchTerm[]) +meth public boolean equals(java.lang.Object) +meth public boolean match(javax.mail.Message) +meth public int hashCode() +meth public javax.mail.search.SearchTerm[] getTerms() +supr javax.mail.search.SearchTerm +hfds serialVersionUID,terms + +CLSS public final javax.mail.search.ReceivedDateTerm +cons public <init>(int,java.util.Date) +meth public boolean equals(java.lang.Object) +meth public boolean match(javax.mail.Message) +supr javax.mail.search.DateTerm +hfds serialVersionUID + +CLSS public final javax.mail.search.RecipientStringTerm +cons public <init>(javax.mail.Message$RecipientType,java.lang.String) +meth public boolean equals(java.lang.Object) +meth public boolean match(javax.mail.Message) +meth public int hashCode() +meth public javax.mail.Message$RecipientType getRecipientType() +supr javax.mail.search.AddressStringTerm +hfds serialVersionUID,type + +CLSS public final javax.mail.search.RecipientTerm +cons public <init>(javax.mail.Message$RecipientType,javax.mail.Address) +meth public boolean equals(java.lang.Object) +meth public boolean match(javax.mail.Message) +meth public int hashCode() +meth public javax.mail.Message$RecipientType getRecipientType() +supr javax.mail.search.AddressTerm +hfds serialVersionUID,type + +CLSS public javax.mail.search.SearchException +cons public <init>() +cons public <init>(java.lang.String) +supr javax.mail.MessagingException +hfds serialVersionUID + +CLSS public abstract javax.mail.search.SearchTerm +cons public <init>() +intf java.io.Serializable +meth public abstract boolean match(javax.mail.Message) +supr java.lang.Object +hfds serialVersionUID + +CLSS public final javax.mail.search.SentDateTerm +cons public <init>(int,java.util.Date) +meth public boolean equals(java.lang.Object) +meth public boolean match(javax.mail.Message) +supr javax.mail.search.DateTerm +hfds serialVersionUID + +CLSS public final javax.mail.search.SizeTerm +cons public <init>(int,int) +meth public boolean equals(java.lang.Object) +meth public boolean match(javax.mail.Message) +supr javax.mail.search.IntegerComparisonTerm +hfds serialVersionUID + +CLSS public abstract javax.mail.search.StringTerm +cons protected <init>(java.lang.String) +cons protected <init>(java.lang.String,boolean) +fld protected boolean ignoreCase +fld protected java.lang.String pattern +meth protected boolean match(java.lang.String) +meth public boolean equals(java.lang.Object) +meth public boolean getIgnoreCase() +meth public int hashCode() +meth public java.lang.String getPattern() +supr javax.mail.search.SearchTerm +hfds serialVersionUID + +CLSS public final javax.mail.search.SubjectTerm +cons public <init>(java.lang.String) +meth public boolean equals(java.lang.Object) +meth public boolean match(javax.mail.Message) +supr javax.mail.search.StringTerm +hfds serialVersionUID + +CLSS public javax.mail.util.ByteArrayDataSource +cons public <init>(byte[],java.lang.String) +cons public <init>(java.io.InputStream,java.lang.String) throws java.io.IOException +cons public <init>(java.lang.String,java.lang.String) throws java.io.IOException +intf javax.activation.DataSource +meth public java.io.InputStream getInputStream() throws java.io.IOException +meth public java.io.OutputStream getOutputStream() throws java.io.IOException +meth public java.lang.String getContentType() +meth public java.lang.String getName() +meth public void setName(java.lang.String) +supr java.lang.Object +hfds data,len,name,type +hcls DSByteArrayOutputStream + +CLSS public javax.mail.util.SharedByteArrayInputStream +cons public <init>(byte[]) +cons public <init>(byte[],int,int) +fld protected int start +intf javax.mail.internet.SharedInputStream +meth public java.io.InputStream newStream(long,long) +meth public long getPosition() +supr java.io.ByteArrayInputStream + +CLSS public javax.mail.util.SharedFileInputStream +cons public <init>(java.io.File) throws java.io.IOException +cons public <init>(java.io.File,int) throws java.io.IOException +cons public <init>(java.lang.String) throws java.io.IOException +cons public <init>(java.lang.String,int) throws java.io.IOException +fld protected int bufsize +fld protected java.io.RandomAccessFile in +fld protected long bufpos +fld protected long datalen +fld protected long start +intf javax.mail.internet.SharedInputStream +meth protected void finalize() throws java.lang.Throwable +meth public boolean markSupported() +meth public int available() throws java.io.IOException +meth public int read() throws java.io.IOException +meth public int read(byte[],int,int) throws java.io.IOException +meth public java.io.InputStream newStream(long,long) +meth public long getPosition() +meth public long skip(long) throws java.io.IOException +meth public void close() throws java.io.IOException +meth public void mark(int) +meth public void reset() throws java.io.IOException +supr java.io.BufferedInputStream +hfds defaultBufferSize,master,sf +hcls SharedFile +
diff --git a/mail/exclude.xml b/mail/exclude.xml new file mode 100644 index 0000000..b5fa45c --- /dev/null +++ b/mail/exclude.xml
@@ -0,0 +1,727 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<!-- FindBugs exclude list for JavaMail --> + +<FindBugsFilter> + <!-- + FindBugs *really* wants us to use zero length arrays instead + of null. Unfortunately, there's a bunch of places we can't + do this for various reasons: + - The API specifies null and we can't make an incompatible change. + - We use null to indicate no header and an empty array to indicate + the header exists but has no entries. + - We use null to indicate NIL in the protocol and an empty array + to indicate an empty list "()" in the protocol. + This error occurs often enough that we just ignore it everywhere. + --> + <Match> + <Bug pattern="PZLA_PREFER_ZERO_LENGTH_ARRAYS"/> + </Match> + <!-- + There are a bunch of places where FindBugs complains about + exposing internal representations. We exclude cases where + this only happens in internal classes that are never visible + through the public JavaMail API, or cases where the user + passes in an object (usually an array) and can only hurt + themselves by modifying the array while a method is in progress, + or where the implementation is passing the data back to the user + (e.g., in an Exception) and never uses it again. + --> + <Match> + <Or> + <Class name="javax.mail.SendFailedException"/> + <Class name="javax.mail.event.MessageCountEvent"/> + <Class name="javax.mail.event.TransportEvent"/> + <Class name="com.sun.mail.iap.ByteArray"/> + <Class name="com.sun.mail.imap.MessageVanishedEvent"/> + <Class name="com.sun.mail.imap.protocol.FetchResponse"/> + </Or> + <Or> + <Bug pattern="EI_EXPOSE_REP"/> + <Bug pattern="EI_EXPOSE_REP2"/> + </Or> + </Match> + <Match> + <Class name="com.sun.mail.smtp.SMTPTransport"/> + <Method name="sendMessage"/> + <!-- passed in Address array --> + <Bug pattern="EI_EXPOSE_REP2"/> + </Match> + <Match> + <Class name="javax.mail.internet.MimeBodyPart"/> + <Method name="<init>"/> <!-- match constructor --> + <!-- passed in byte array --> + <Bug pattern="EI_EXPOSE_REP2"/> + </Match> + <Match> + <Class name="javax.mail.util.ByteArrayDataSource"/> + <Method name="<init>"/> <!-- match constructor --> + <!-- passed in byte array --> + <Bug pattern="EI_EXPOSE_REP2"/> + </Match> + <Match> + <Class name="com.sun.mail.util.MailSSLSocketFactory"/> + <Method name="setTrustManagers"/> + <!-- passed in TrustManager array --> + <Bug pattern="EI_EXPOSE_REP2"/> + </Match> + <Match> + <Class name="com.sun.mail.util.MailSSLSocketFactory"/> + <Method name="getTrustManagers"/> + <!-- returned TrustManager array --> + <Bug pattern="EI_EXPOSE_REP"/> + </Match> + <Match> + <Class name="com.sun.mail.imap.protocol.INTERNALDATE"/> + <Method name="getDate"/> + <!-- + Returned Date object; it's only ever stored in + IMAPMessage.receivedDate and is always used to + construct a new Date object before returning to + users. + --> + <Bug pattern="EI_EXPOSE_REP"/> + </Match> + <Match> + <Class name="com.sun.mail.imap.CopyUID"/> + <Method name="<init>"/> <!-- match constructor --> + <Bug pattern="EI_EXPOSE_REP2"/> + </Match> + + <!-- + A few places where it complains about wait not being in a loop. + This purposely doesn't loop so that the application + calling idle can check whether the idle should continue. + --> + <Match> + <!-- an anonymous inner class of the idle method --> + <Class name="~com\.sun\.mail\.imap\.IMAPFolder.*"/> + <Method name="doCommand"/> + <Bug pattern="WA_NOT_IN_LOOP"/> + </Match> + <Match> + <Class name="com.sun.mail.imap.IMAPStore"/> + <Method name="idle"/> + <Bug pattern="WA_NOT_IN_LOOP"/> + </Match> + + <!-- + A few places where we catch Exception even though it's not + explicitly thrown. We need to make sure that if anything + goes wrong we clean things up. Perhaps these should be + converted to a finally block and a boolean "success" flag? + Most of these are related to reflection or ClassLoader + operations, which can fail for all sorts of unexpected reasons. + Some of these could be converted to use multi-catch with JDK 1.7. + --> + <Match> + <Class name="com.sun.mail.imap.IMAPStore"/> + <Or> + <Method name="<init>"/> <!-- match constructor --> + <Method name="getProtocol"/> + <Method name="getStoreProtocol"/> + <Method name="newIMAPFolder"/> + </Or> + <Bug pattern="REC_CATCH_EXCEPTION"/> + </Match> + <Match> + <Class name="com.sun.mail.imap.protocol.IMAPProtocol"/> + <Or> + <Method name="authlogin"/> + <Method name="authntlm"/> + <Method name="authplain"/> + <Method name="authoauth2"/> + <Method name="sasllogin"/> + </Or> + <Bug pattern="REC_CATCH_EXCEPTION"/> + </Match> + <Match> + <Class name="com.sun.mail.imap.protocol.IMAPSaslAuthenticator"/> + <Method name="authenticate"/> + <Bug pattern="REC_CATCH_EXCEPTION"/> + </Match> + <Match> + <Class name="com.sun.mail.pop3.POP3Store"/> + <Method name="<init>"/> <!-- match constructor --> + <Bug pattern="REC_CATCH_EXCEPTION"/> + </Match> + <Match> + <Class name="com.sun.mail.smtp.SMTPTransport"/> + <Or> + <Method name="isConnected"/> + <Method name="sasllogin"/> + </Or> + <Bug pattern="REC_CATCH_EXCEPTION"/> + </Match> + <Match> + <Class name="com.sun.mail.pop3.POP3Folder"/> + <Method name="createMessage"/> + <Or> + <Bug pattern="REC_CATCH_EXCEPTION"/> + <Bug pattern="DE_MIGHT_IGNORE"/> + </Or> + </Match> + <Match> + <Class name="com.sun.mail.util.SocketFetcher"/> + <Or> + <Method name="getSocket"/> + <Method name="startTLS"/> + <Method name="matchCert"/> + </Or> + <Bug pattern="REC_CATCH_EXCEPTION"/> + </Match> + <Match> + <Class name="com.sun.mail.util.MimeUtil"/> + <Method name="cleanContentType"/> + <Bug pattern="REC_CATCH_EXCEPTION"/> + </Match> + <Match> + <Class name="javax.mail.Session"/> + <Or> + <Method name="getService"/> + <Method name="loadAllResources"/> + </Or> + <Bug pattern="REC_CATCH_EXCEPTION"/> + </Match> + <Match> + <Class name="javax.mail.internet.MimeUtility"/> + <Or> + <Method name="<clinit>"/> <!-- match static initializer --> + <Method name="getEncoding"/> + </Or> + <Bug pattern="REC_CATCH_EXCEPTION"/> + </Match> + <Match> + <Class name="com.sun.mail.imap.IMAPFolder"/> + <Method name="handleIdle"/> + <Bug pattern="DE_MIGHT_IGNORE"/> + </Match> + + <!-- + FindBugs complains about a possible double check of headersLoaded, + but it seems to be just wrong; I don't see it. + --> + <Match> + <Class name="com.sun.mail.imap.IMAPMessage"/> + <Method name="loadHeaders"/> + <Bug pattern="DC_DOUBLECHECK"/> + </Match> + + <!-- + These IMAP-specific subclasses of standard classes don't override + equals because all they add are constructors and optimized access + methods; everything else, including the way to test for equality, + is the same. + --> + <Match> + <Class name="com.sun.mail.imap.protocol.FLAGS"/> + <Bug pattern="EQ_DOESNT_OVERRIDE_EQUALS"/> + </Match> + <Match> + <Class name="com.sun.mail.imap.protocol.IMAPAddress"/> + <!-- defined in ENVELOPE.java --> + <Bug pattern="EQ_DOESNT_OVERRIDE_EQUALS"/> + </Match> + + <!-- + FindBugs complains of an unitialized read of the "capabilities" + field. Since the superclass might initialize the field (via a + call into the overridden processGreeting method), any initialization + done in the IMAPProtocol class will undo any initialization done + as a side effect of calling the superclass constructor. Thus, we + need to depend on the field being initialized to the default value. + --> + <Match> + <Class name="com.sun.mail.imap.protocol.IMAPProtocol"/> + <Method name="<init>"/> <!-- match constructor --> + <Bug pattern="UR_UNINIT_READ"/> + </Match> + + <!-- + This use of string concatenation only occurs when creating a + string for an error message in an exception. The simpler code + is better here; performance is not an issue. + --> + <Match> + <Class name="com.sun.mail.util.BASE64DecoderStream"/> + <Method name="recentChars"/> + <Bug pattern="SBSC_USE_STRINGBUFFER_CONCATENATION"/> + </Match> + + <!-- + Yes, the "next" element in my linked list isn't ever actually + used, but it feels weird to only have a "prev" element. + --> + <Match> + <Class name="javax.mail.EventQueue$QueueElement"/> + <Field name="next"/> + <Bug pattern="URF_UNREAD_FIELD"/> + </Match> + + <!-- + Stupid Serializable EventObject class causes FindBugs to complain + about transient fields in subclasses. I don't know why it's + complaining about these fields but not others, but since I don't + really expect anyone to serialize these events I'm just ignoring + this complaint. Ditto Exception fields. + --> + <Match> + <Class name="javax.mail.event.TransportEvent"/> + <Or> + <Field name="invalid"/> + <Field name="validSent"/> + <Field name="validUnsent"/> + </Or> + <Bug pattern="SE_TRANSIENT_FIELD_NOT_RESTORED"/> + </Match> + <Match> + <Class name="javax.mail.event.FolderEvent"/> + <Or> + <Field name="folder"/> + <Field name="newFolder"/> + </Or> + <Bug pattern="SE_TRANSIENT_FIELD_NOT_RESTORED"/> + </Match> + <Match> + <Class name="javax.mail.event.MessageChangedEvent"/> + <Field name="msg"/> + <Bug pattern="SE_TRANSIENT_FIELD_NOT_RESTORED"/> + </Match> + <Match> + <Class name="javax.mail.event.MessageCountEvent"/> + <Field name="msgs"/> + <Bug pattern="SE_TRANSIENT_FIELD_NOT_RESTORED"/> + </Match> + <Match> + <Class name="javax.mail.event.TransportEvent"/> + <Field name="msg"/> + <Bug pattern="SE_TRANSIENT_FIELD_NOT_RESTORED"/> + </Match> + <Match> + <Class name="javax.mail.SendFailedException"/> + <Or> + <Field name="invalid"/> + <Field name="validSent"/> + <Field name="validUnsent"/> + </Or> + <Bug pattern="SE_TRANSIENT_FIELD_NOT_RESTORED"/> + </Match> + + <!-- + These string comparisons are just optimizations. + --> + <Match> + <Or> + <Class name="javax.mail.URLName"/> + <Class name="javax.mail.internet.InternetAddress"/> + </Or> + <Method name="equals"/> + <Bug pattern="ES_COMPARING_STRINGS_WITH_EQ"/> + </Match> + + <!-- + This string comparison using == is to determine whether the + String object is a different String object. + --> + <Match> + <Class name="javax.mail.internet.MimeUtility"/> + <Method name="decodeText"/> + <Bug pattern="ES_COMPARING_STRINGS_WITH_EQ"/> + </Match> + + <!-- + This string comparison using == is to determine whether the + String object is the original default String object. + --> + <Match> + <Class name="com.sun.mail.smtp.SMTPTransport"/> + <Method name="authenticate"/> + <Bug pattern="ES_COMPARING_STRINGS_WITH_EQ"/> + </Match> + + <!-- + ByteArrayInputStream.available guarantees to return the full number + of bytes left in the buffer, and ByteArrayInputStream.read guarantees + to read all the bytes, so we don't really need to check the return + value. Ignore this complaint. + --> + <Match> + <Class name="com.sun.mail.util.ASCIIUtility"/> + <Method name="getBytes"/> + <Bug pattern="DLS_DEAD_LOCAL_STORE"/> + </Match> + + <!-- + We extract the "lang" field of an encoded string but we don't + currently do anything with it. Ignore this complaint. + --> + <Match> + <Class name="javax.mail.internet.ParameterList"/> + <Method name="decodeValue"/> + <Bug pattern="DLS_DEAD_LOCAL_STORE"/> + </Match> + + <!-- + The call ParameterList.set(null, "DONE") is a kludge used by the + IMAP provider to indicate that it's done setting parameters. + In other cases we *want* a null name to cause a NullPointerException. + --> + <Match> + <Class name="javax.mail.internet.ParameterList"/> + <Method name="set"/> + <Bug pattern="NP_NULL_ON_SOME_PATH"/> + </Match> + + <!-- + We purposely don't close these streams, which are just wrappers + around the original stream that needs to remain open. + --> + <Match> + <Class name="javax.mail.internet.MimeMultipart"/> + <Or> + <Method name="parse"/> + <Method name="parsebm"/> + </Or> + <Bug pattern="OS_OPEN_STREAM"/> + </Match> + + <!-- + When deleting the temp file fails, there's really nothing to be done. + --> + <Match> + <Class name="com.sun.mail.pop3.TempFile"/> + <Method name="close"/> + <Bug pattern="RV_RETURN_VALUE_IGNORED_BAD_PRACTICE"/> + </Match> + + <!-- + In IMAPFolder.close, I believe the protocol field can be set to null + as a result of a protocol error that invokes a callback that calls + cleanup, thus I don't believe these null checks are redundant. + --> + <Match> + <Class name="com.sun.mail.imap.IMAPFolder"/> + <Method name="close"/> + <Bug pattern="RCN_REDUNDANT_NULLCHECK_WOULD_HAVE_BEEN_A_NPE"/> + </Match> + + <!-- + Can't fix these errors in toString until JavaMail 1.5 because + it's part of the spec. Sigh. + --> + <Match> + <Or> + <Class name="javax.mail.internet.ContentDisposition"/> + <Class name="javax.mail.internet.ContentType"/> + </Or> + <Method name="toString"/> + <Bug pattern="NP_TOSTRING_COULD_RETURN_NULL"/> + </Match> + + <!-- + IMAPFolder.uidTable is only ever manipulated when the + messageCacheLock is held, but FindBugs can't figure that out. + --> + <Match> + <Class name="com.sun.mail.imap.IMAPFolder"/> + <Field name="uidTable"/> + <Bug pattern="IS2_INCONSISTENT_SYNC"/> + </Match> + + <!-- + IMAPFolder.doExpungeNotification is only ever manipulated when the + messageCacheLock is held, but FindBugs can't figure that out. + --> + <Match> + <Class name="com.sun.mail.imap.IMAPFolder"/> + <Field name="doExpungeNotification"/> + <Bug pattern="IS2_INCONSISTENT_SYNC"/> + </Match> + + <!-- + The static mailDateFormat field is only used to access the parse + method, which is known to be thread safe, so we can ignore this error. + --> + <Match> + <Class name="com.sun.mail.imap.protocol.INTERNALDATE"/> + <Bug pattern="STCAL_INVOKE_ON_STATIC_DATE_FORMAT_INSTANCE"/> + </Match> + + <!-- + These errors are in code imported from the JDK, where it seems + to be purposely using the platform default encoding. It's safer + to just leave this alone and ignore the errors. + --> + <Match> + <Class name="javax.mail.URLName"/> + <Bug pattern="DM_DEFAULT_ENCODING"/> + </Match> + + <!-- + We're purposely using the platform default encoding because + we're trying to discover what the platform default encoding *is*! + --> + <Match> + <Class name="javax.mail.internet.MimeUtility"/> + <Method name="getDefaultJavaCharset"/> + <Bug pattern="DM_DEFAULT_ENCODING"/> + </Match> + + <!-- + We're purposely using the platform default encoding when a + charset hasn't been specified. + --> + <Match> + <Class name="javax.mail.internet.ParameterList"/> + <Method name="combineMultisegmentNames"/> + <Bug pattern="DM_DEFAULT_ENCODING"/> + </Match> + + <!-- + We're purposely using the platform default encoding because + we're converting bytes that were written using the platform + default encoding back to a String. + --> + <Match> + <Class name="com.sun.mail.util.LogOutputStream"/> + <Method name="logBuf"/> + <Bug pattern="DM_DEFAULT_ENCODING"/> + </Match> + + <!-- + MimeBodyPart.headers is set in the constructor. IMAPBodyPart has + perhaps paranoid defensive code to set it if it's not set. That + code is protected by the object lock, but all of the reads of the + field are not, thus FindBugs complains. For now I think it's + better to leave the defensive code in and ignore this error. + --> + <Match> + <Class name="javax.mail.internet.MimeBodyPart"/> + <Field name="headers"/> + <Bug pattern="IS2_INCONSISTENT_SYNC"/> + </Match> + + <!-- + Refactoring these anonymous inner classes to be static classes + makes the code less readable. Too bad there's no way for an + anonymous inner class to be static. + XXX - these class names aren't going to be stable as the code changes + --> + <Match> + <Or> + <Class name="com.sun.mail.imap.DefaultFolder$1"/> + <Class name="com.sun.mail.imap.DefaultFolder$2"/> + <Class name="com.sun.mail.imap.IMAPFolder$1"/> + <Class name="com.sun.mail.imap.IMAPFolder$4"/> + <Class name="com.sun.mail.imap.IMAPFolder$7"/> + <Class name="com.sun.mail.imap.IMAPFolder$13"/> + <Class name="com.sun.mail.imap.IMAPFolder$20"/> + <Class name="com.sun.mail.imap.IdleManager$2"/> + </Or> + <Bug pattern="SIC_INNER_SHOULD_BE_STATIC_ANON"/> + </Match> + + <!-- + SpotBugs doesn't recognize the anonymous inner classes above. + https://github.com/spotbugs/discuss/issues/22 + This regular expression version has the same effect, and + avoids the potential issue with the class names changing. + --> + <Match> + <Or> + <Class name="~com\.sun\.mail\.imap\.DefaultFolder.*"/> + <Class name="~com\.sun\.mail\.imap\.IMAPFolder.*"/> + <Class name="~com\.sun\.mail\.imap\.IdleManager.*"/> + </Or> + <Bug pattern="SIC_INNER_SHOULD_BE_STATIC_ANON"/> + </Match> + + <!-- + Even though only this array reference is volatile, the array is + used in such a way that it's safe. The aray elements are created + and initialized before the array is assigned to this array + reference, and after being assigned neither the array nor the + array elements are changed. + --> + <Match> + <Class name="com.sun.mail.imap.IMAPFolder"/> + <Field name="attributes"/> + <Bug pattern="VO_VOLATILE_REFERENCE_TO_ARRAY"/> + </Match> + + <!-- + This code is easier to understand with empty "if" clauses; + the "match" method is being called for its side effects. + --> + <Match> + <Class name="com.sun.mail.imap.protocol.FetchResponse"/> + <Method name="parseItem"/> + <Bug pattern="UCF_USELESS_CONTROL_FLOW"/> + </Match> + + <!-- + Even though these fields are unread for now, we'll keep them + since they represent data in the protocol message being parsed. + --> + <Match> + <Or> + <Class name="com.sun.mail.imap.protocol.ENVELOPE"/> + <Class name="com.sun.mail.imap.protocol.INTERNALDATE"/> + <Class name="com.sun.mail.imap.protocol.RFC822SIZE"/> + </Or> + <Field name="msgno"/> + <Bug pattern="URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD"/> + </Match> + <Match> + <Class name="com.sun.mail.imap.protocol.MailboxInfo"/> + <Field name="first"/> + <Bug pattern="URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD"/> + </Match> + <!-- + These unread fields are part of the public API. + --> + <Match> + <Class name="javax.mail.Service"/> + <Field name="debug"/> + <Bug pattern="URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD"/> + </Match> + <Match> + <Class name="javax.mail.Quota$Resource"/> + <Field name="usage"/> + <Bug pattern="URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD"/> + </Match> + + <!-- + We use a static MailDateFormat instance, which uses a static + GregorianCalendar instance. The MailDateFormat.parse method + uses only local data except for the GregorianCalendar instance, + which is invoked through a static synchronized method. + --> + <Match> + <Class name="com.sun.mail.imap.protocol.ENVELOPE"/> + <Bug pattern="STCAL_INVOKE_ON_STATIC_DATE_FORMAT_INSTANCE"/> + </Match> + + <!-- + A special String instance is used to indicate that the POP3 + UID is unknown. The uid field is initialized to this String + instance, which we then compare with later to see if the uid + has been set. + --> + <Match> + <Class name="com.sun.mail.pop3.POP3Folder"/> + <Method name="getUID"/> + <Bug pattern="ES_COMPARING_STRINGS_WITH_EQ"/> + </Match> + <!-- + As above, a special String instance is used to indicate whether + certain fields have been set. + --> + <Match> + <Class name="com.sun.mail.smtp.SMTPTransport"/> + <Or> + <Method name="getAuthorizationId"/> + <Method name="getNTLMDomain"/> + <Method name="getSASLRealm"/> + </Or> + <Bug pattern="ES_COMPARING_STRINGS_WITH_EQ"/> + </Match> + + <!-- + I know Exception.toString is never supposed to return null, + but this extra check is cheap insurance. + --> + <Match> + <Class name="javax.mail.MessagingException"/> + <Method name="toString"/> + <Bug pattern="RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE"/> + </Match> + <!-- + I know ClassLoader.getResources is never supposed to return null, + but this extra check is cheap insurance. + --> + <Match> + <Class name="~javax\.mail\.Session\$.*"/> + <Method name="run"/> + <Bug pattern="RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE"/> + </Match> + + <!-- + These names may be confusing, but we're stuck with them. + --> + <Match> + <Class name="javax.mail.PasswordAuthentication"/> + <Method name="getUserName"/> + <Bug pattern="NM_CONFUSING"/> + </Match> + + <!-- + This trivial inner class extends ArrayList, but is never serialized, + so it doesn't really need a serialVersionUID. + --> + <Match> + <Class name="javax.mail.internet.ParameterList$MultiValue"/> + <Bug pattern="SE_NO_SERIALVERSIONID"/> + </Match> + + <!-- + These array references are declared volatile because they're + treated as copy-on-write. + --> + <Match> + <Class name="com.sun.mail.util.logging.LogManagerProperties"/> + <Field name="REFLECT_NAMES"/> + <Bug pattern="VO_VOLATILE_REFERENCE_TO_ARRAY"/> + </Match> + <Match> + <Class name="com.sun.mail.util.logging.MailHandler"/> + <Field name="attachmentFilters"/> + <Bug pattern="VO_VOLATILE_REFERENCE_TO_ARRAY"/> + </Match> + + <!-- + The Authenticator fields are only accessible by subclasses, + which are only called through the one synchronized public + method, assuming the subclass doesn't do something crazy and + expose them itself. + --> + <Match> + <Class name="javax.mail.Authenticator"/> + <Bug pattern="IS2_INCONSISTENT_SYNC"/> + </Match> + + <!-- + Ignore failures to close connection. + --> + <Match> + <Class name="com.sun.mail.smtp.SMTPTransport"/> + <Method name="protocolConnect"/> + <Bug pattern="DE_MIGHT_IGNORE"/> + </Match> + + <!-- + This inner class doesn't need "this", but it creates another + inner class that *does* need "this". + --> + <Match> + <Class name="com.sun.mail.smtp.SMTPTransport$BDATOutputStream"/> + <Bug pattern="SIC_INNER_SHOULD_BE_STATIC_NEEDS_THIS"/> + </Match> +</FindBugsFilter>
diff --git a/mail/pom.xml b/mail/pom.xml new file mode 100644 index 0000000..1020ed0 --- /dev/null +++ b/mail/pom.xml
@@ -0,0 +1,214 @@ +<?xml version="1.0" encoding="iso-8859-1"?> +<!-- + + Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 + http://maven.apache.org/maven-v4_0_0.xsd"> + <parent> + <groupId>com.sun.mail</groupId> + <artifactId>all</artifactId> + <version>1.6.3</version> + </parent> + <modelVersion>4.0.0</modelVersion> + <groupId>com.sun.mail</groupId> + <artifactId>jakarta.mail</artifactId> + <packaging>jar</packaging> + <name>JavaMail API</name> + + <properties> + <mail.extensionName> + jakarta.mail + </mail.extensionName> + <mail.moduleName> + jakarta.mail + </mail.moduleName> + <mail.specificationTitle> + JavaMail(TM) API Design Specification + </mail.specificationTitle> + <mail.implementationTitle> + javax.mail + </mail.implementationTitle> + <mail.packages.export> + javax.mail.*; version=${mail.spec.version}, + com.sun.mail.imap; version=${mail.osgiversion}, + com.sun.mail.imap.protocol; version=${mail.osgiversion}, + com.sun.mail.iap; version=${mail.osgiversion}, + com.sun.mail.pop3; version=${mail.osgiversion}, + com.sun.mail.smtp; version=${mail.osgiversion}, + com.sun.mail.util; version=${mail.osgiversion}, + com.sun.mail.util.logging; version=${mail.osgiversion}, + com.sun.mail.handlers; version=${mail.osgiversion} + </mail.packages.export> + <mail.probeFile> + META-INF/gfprobe-provider.xml + </mail.probeFile> + <findbugs.skip> + false + </findbugs.skip> + <findbugs.exclude> + ${project.basedir}/exclude.xml + </findbugs.exclude> + </properties> + + <profiles> + <!-- + A special profile for compiling with the real JDK 1.7 compiler. + Exclude MailSessionDefinition and MailSessionDefinitions since + the former requires JDK 1.8 and the latter depends on the former. + --> + <profile> + <id>1.7</id> + <build> + <plugins> + <plugin> + <artifactId>maven-compiler-plugin</artifactId> + <executions> + <execution> + <id>default-compile</id> + <configuration> + <excludes> + <exclude> + javax/mail/MailSessionDefinition.java + </exclude> + <exclude> + javax/mail/MailSessionDefinitions.java + </exclude> + <exclude> + module-info.java + </exclude> + </excludes> + </configuration> + </execution> + </executions> + </plugin> + </plugins> + </build> + </profile> + + <!-- + A special profile used when compiling with the real JDK 9 compiler. + Override the release setting from the parent's "9" profile so + that we produce JDK 1.7 compatible class files. + --> + <profile> + <id>9</id> + <activation> + <jdk>9</jdk> + </activation> + <build> + <plugins> + <plugin> + <artifactId>maven-compiler-plugin</artifactId> + <executions> + <execution> + <id>default-compile</id> + <configuration> + <release combine.self="override"></release> + </configuration> + </execution> + </executions> + </plugin> + </plugins> + </build> + </profile> + </profiles> + + <build> + <resources> + <resource> + <directory>src/main/resources</directory> + <filtering>true</filtering> + </resource> + </resources> + <plugins> + <!-- + Configure compiler plugin to print lint warnings. + Need to exclude options warning because bootstrap + classpath isn't set (on purpose). + Need to exclude path warnings because Maven includes + source directories that don't exist (e.g., generated-sources). + --> + <plugin> + <artifactId>maven-compiler-plugin</artifactId> + <configuration> + <source>1.7</source> + <target>1.7</target> + <compilerArgs> + <arg>-Xlint</arg> + <arg>-Xlint:-options</arg> + <arg>-Xlint:-path</arg> + <arg>-Werror</arg> + </compilerArgs> + <showDeprecation>true</showDeprecation> + <showWarnings>true</showWarnings> + <excludes> + <exclude> + module-info.java + </exclude> + </excludes> + </configuration> + </plugin> + + <!-- + Configure test plugin to find *TestSuite classes. + --> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-surefire-plugin</artifactId> + <configuration> + <includes> + <include>**/*Test.java</include> + <include>**/*TestSuite.java</include> + </includes> + </configuration> + </plugin> + + <!-- + Add the Automatic-Module-Name manifest header for JDK 9. + --> + <plugin> + <artifactId>maven-jar-plugin</artifactId> + <configuration> + <archive> + <manifestEntries> + <Automatic-Module-Name> + ${mail.moduleName} + </Automatic-Module-Name> + </manifestEntries> + </archive> + </configuration> + </plugin> + </plugins> + </build> + + <dependencies> + <dependency> + <groupId>com.sun.activation</groupId> + <artifactId>jakarta.activation</artifactId> + </dependency> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <version>4.12</version> + <scope>test</scope> + <optional>true</optional> + </dependency> + </dependencies> +</project>
diff --git a/mail/src/main/java/com/sun/mail/auth/MD4.java b/mail/src/main/java/com/sun/mail/auth/MD4.java new file mode 100644 index 0000000..4b98b90 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/auth/MD4.java
@@ -0,0 +1,276 @@ +/* + * Copyright (c) 2005, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +/* + * Copied from OpenJDK with permission. + */ + +package com.sun.mail.auth; + +import java.security.*; + +//import static sun.security.provider.ByteArrayAccess.*; + +/** + * The MD4 class is used to compute an MD4 message digest over a given + * buffer of bytes. It is an implementation of the RSA Data Security Inc + * MD4 algorithim as described in internet RFC 1320. + * + * @author Andreas Sterbenz + * @author Bill Shannon (adapted for JavaMail) + */ +public final class MD4 { + + // state of this object + private final int[] state; + // temporary buffer, used by implCompress() + private final int[] x; + + // size of the input to the compression function in bytes + private static final int blockSize = 64; + + // buffer to store partial blocks, blockSize bytes large + private final byte[] buffer = new byte[blockSize]; + // offset into buffer + private int bufOfs; + + // number of bytes processed so far. + // also used as a flag to indicate reset status + // -1: need to call engineReset() before next call to update() + // 0: is already reset + private long bytesProcessed; + + // rotation constants + private static final int S11 = 3; + private static final int S12 = 7; + private static final int S13 = 11; + private static final int S14 = 19; + private static final int S21 = 3; + private static final int S22 = 5; + private static final int S23 = 9; + private static final int S24 = 13; + private static final int S31 = 3; + private static final int S32 = 9; + private static final int S33 = 11; + private static final int S34 = 15; + + private static final byte[] padding; + + static { + padding = new byte[136]; + padding[0] = (byte)0x80; + } + + /** + * Standard constructor, creates a new MD4 instance. + */ + public MD4() { + state = new int[4]; + x = new int[16]; + implReset(); + } + + /** + * Compute and return the message digest of the input byte array. + * + * @param in the input byte array + * @return the message digest byte array + */ + public byte[] digest(byte[] in) { + implReset(); + engineUpdate(in, 0, in.length); + byte[] out = new byte[16]; + implDigest(out, 0); + return out; + } + + /** + * Reset the state of this object. + */ + private void implReset() { + // Load magic initialization constants. + state[0] = 0x67452301; + state[1] = 0xefcdab89; + state[2] = 0x98badcfe; + state[3] = 0x10325476; + bufOfs = 0; + bytesProcessed = 0; + } + + /** + * Perform the final computations, any buffered bytes are added + * to the digest, the count is added to the digest, and the resulting + * digest is stored. + */ + private void implDigest(byte[] out, int ofs) { + long bitsProcessed = bytesProcessed << 3; + + int index = (int)bytesProcessed & 0x3f; + int padLen = (index < 56) ? (56 - index) : (120 - index); + engineUpdate(padding, 0, padLen); + + //i2bLittle4((int)bitsProcessed, buffer, 56); + //i2bLittle4((int)(bitsProcessed >>> 32), buffer, 60); + buffer[56] = (byte)bitsProcessed; + buffer[57] = (byte)(bitsProcessed>>8); + buffer[58] = (byte)(bitsProcessed>>16); + buffer[59] = (byte)(bitsProcessed>>24); + buffer[60] = (byte)(bitsProcessed>>32); + buffer[61] = (byte)(bitsProcessed>>40); + buffer[62] = (byte)(bitsProcessed>>48); + buffer[63] = (byte)(bitsProcessed>>56); + implCompress(buffer, 0); + + //i2bLittle(state, 0, out, ofs, 16); + for (int i = 0; i < state.length; i++) { + int x = state[i]; + out[ofs++] = (byte)x; + out[ofs++] = (byte)(x>>8); + out[ofs++] = (byte)(x>>16); + out[ofs++] = (byte)(x>>24); + } + } + + private void engineUpdate(byte[] b, int ofs, int len) { + if (len == 0) { + return; + } + if ((ofs < 0) || (len < 0) || (ofs > b.length - len)) { + throw new ArrayIndexOutOfBoundsException(); + } + if (bytesProcessed < 0) { + implReset(); + } + bytesProcessed += len; + // if buffer is not empty, we need to fill it before proceeding + if (bufOfs != 0) { + int n = Math.min(len, blockSize - bufOfs); + System.arraycopy(b, ofs, buffer, bufOfs, n); + bufOfs += n; + ofs += n; + len -= n; + if (bufOfs >= blockSize) { + // compress completed block now + implCompress(buffer, 0); + bufOfs = 0; + } + } + // compress complete blocks + while (len >= blockSize) { + implCompress(b, ofs); + len -= blockSize; + ofs += blockSize; + } + // copy remainder to buffer + if (len > 0) { + System.arraycopy(b, ofs, buffer, 0, len); + bufOfs = len; + } + } + + private static int FF(int a, int b, int c, int d, int x, int s) { + a += ((b & c) | ((~b) & d)) + x; + return ((a << s) | (a >>> (32 - s))); + } + + private static int GG(int a, int b, int c, int d, int x, int s) { + a += ((b & c) | (b & d) | (c & d)) + x + 0x5a827999; + return ((a << s) | (a >>> (32 - s))); + } + + private static int HH(int a, int b, int c, int d, int x, int s) { + a += ((b ^ c) ^ d) + x + 0x6ed9eba1; + return ((a << s) | (a >>> (32 - s))); + } + + /** + * This is where the functions come together as the generic MD4 + * transformation operation. It consumes 64 + * bytes from the buffer, beginning at the specified offset. + */ + private void implCompress(byte[] buf, int ofs) { + //b2iLittle64(buf, ofs, x); + for (int xfs = 0; xfs < x.length; xfs++) { + x[xfs] = (buf[ofs] & 0xff) | ((buf[ofs+1] & 0xff) << 8) | + ((buf[ofs+2] & 0xff) << 16) | ((buf[ofs+3] & 0xff) << 24); + ofs += 4; + } + + int a = state[0]; + int b = state[1]; + int c = state[2]; + int d = state[3]; + + /* Round 1 */ + a = FF (a, b, c, d, x[ 0], S11); /* 1 */ + d = FF (d, a, b, c, x[ 1], S12); /* 2 */ + c = FF (c, d, a, b, x[ 2], S13); /* 3 */ + b = FF (b, c, d, a, x[ 3], S14); /* 4 */ + a = FF (a, b, c, d, x[ 4], S11); /* 5 */ + d = FF (d, a, b, c, x[ 5], S12); /* 6 */ + c = FF (c, d, a, b, x[ 6], S13); /* 7 */ + b = FF (b, c, d, a, x[ 7], S14); /* 8 */ + a = FF (a, b, c, d, x[ 8], S11); /* 9 */ + d = FF (d, a, b, c, x[ 9], S12); /* 10 */ + c = FF (c, d, a, b, x[10], S13); /* 11 */ + b = FF (b, c, d, a, x[11], S14); /* 12 */ + a = FF (a, b, c, d, x[12], S11); /* 13 */ + d = FF (d, a, b, c, x[13], S12); /* 14 */ + c = FF (c, d, a, b, x[14], S13); /* 15 */ + b = FF (b, c, d, a, x[15], S14); /* 16 */ + + /* Round 2 */ + a = GG (a, b, c, d, x[ 0], S21); /* 17 */ + d = GG (d, a, b, c, x[ 4], S22); /* 18 */ + c = GG (c, d, a, b, x[ 8], S23); /* 19 */ + b = GG (b, c, d, a, x[12], S24); /* 20 */ + a = GG (a, b, c, d, x[ 1], S21); /* 21 */ + d = GG (d, a, b, c, x[ 5], S22); /* 22 */ + c = GG (c, d, a, b, x[ 9], S23); /* 23 */ + b = GG (b, c, d, a, x[13], S24); /* 24 */ + a = GG (a, b, c, d, x[ 2], S21); /* 25 */ + d = GG (d, a, b, c, x[ 6], S22); /* 26 */ + c = GG (c, d, a, b, x[10], S23); /* 27 */ + b = GG (b, c, d, a, x[14], S24); /* 28 */ + a = GG (a, b, c, d, x[ 3], S21); /* 29 */ + d = GG (d, a, b, c, x[ 7], S22); /* 30 */ + c = GG (c, d, a, b, x[11], S23); /* 31 */ + b = GG (b, c, d, a, x[15], S24); /* 32 */ + + /* Round 3 */ + a = HH (a, b, c, d, x[ 0], S31); /* 33 */ + d = HH (d, a, b, c, x[ 8], S32); /* 34 */ + c = HH (c, d, a, b, x[ 4], S33); /* 35 */ + b = HH (b, c, d, a, x[12], S34); /* 36 */ + a = HH (a, b, c, d, x[ 2], S31); /* 37 */ + d = HH (d, a, b, c, x[10], S32); /* 38 */ + c = HH (c, d, a, b, x[ 6], S33); /* 39 */ + b = HH (b, c, d, a, x[14], S34); /* 40 */ + a = HH (a, b, c, d, x[ 1], S31); /* 41 */ + d = HH (d, a, b, c, x[ 9], S32); /* 42 */ + c = HH (c, d, a, b, x[ 5], S33); /* 43 */ + b = HH (b, c, d, a, x[13], S34); /* 44 */ + a = HH (a, b, c, d, x[ 3], S31); /* 45 */ + d = HH (d, a, b, c, x[11], S32); /* 46 */ + c = HH (c, d, a, b, x[ 7], S33); /* 47 */ + b = HH (b, c, d, a, x[15], S34); /* 48 */ + + state[0] += a; + state[1] += b; + state[2] += c; + state[3] += d; + } +}
diff --git a/mail/src/main/java/com/sun/mail/auth/Ntlm.java b/mail/src/main/java/com/sun/mail/auth/Ntlm.java new file mode 100644 index 0000000..46a05a4 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/auth/Ntlm.java
@@ -0,0 +1,343 @@ +/* + * Copyright (c) 2005, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +/* + * Copied from OpenJDK with permission. + */ + +package com.sun.mail.auth; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.io.PrintStream; +import java.security.GeneralSecurityException; +import java.security.NoSuchAlgorithmException; +import java.util.Locale; +import java.util.logging.Level; +import javax.crypto.Cipher; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.DESKeySpec; + +import com.sun.mail.util.BASE64DecoderStream; +import com.sun.mail.util.BASE64EncoderStream; +import com.sun.mail.util.MailLogger; + + +/** + * NTLMAuthentication: + * + * @author Michael McMahon + * @author Bill Shannon (adapted for JavaMail) + */ +public class Ntlm { + + private byte[] type1; + private byte[] type3; + + private SecretKeyFactory fac; + private Cipher cipher; + private MD4 md4; + private String hostname; + private String ntdomain; + private String username; + private String password; + + private MailLogger logger; + + private void init0() { + type1 = new byte[256]; + type3 = new byte[256]; + System.arraycopy(new byte[] {'N','T','L','M','S','S','P',0,1}, 0, + type1, 0, 9); + type1[12] = (byte) 3; + type1[13] = (byte) 0xb2; + type1[28] = (byte) 0x20; + System.arraycopy(new byte[] {'N','T','L','M','S','S','P',0,3}, 0, + type3, 0, 9); + type3[12] = (byte) 0x18; + type3[14] = (byte) 0x18; + type3[20] = (byte) 0x18; + type3[22] = (byte) 0x18; + type3[32] = (byte) 0x40; + type3[60] = (byte) 1; + type3[61] = (byte) 0x82; + + try { + fac = SecretKeyFactory.getInstance("DES"); + cipher = Cipher.getInstance("DES/ECB/NoPadding"); + md4 = new MD4(); + } catch (NoSuchPaddingException e) { + assert false; + } catch (NoSuchAlgorithmException e) { + assert false; + } + }; + + /** + * Create an NTLM authenticator. + * Username may be specified as domain\\username in the Authenticator. + * If this notation is not used, then the domain will be taken + * from the ntdomain parameter. + * + * @param ntdomain the NT domain + * @param hostname the host name + * @param username the user name + * @param password the password + * @param logger the MailLogger + */ + public Ntlm(String ntdomain, String hostname, String username, + String password, MailLogger logger) { + int i = hostname.indexOf('.'); + if (i != -1) { + hostname = hostname.substring(0, i); + } + i = username.indexOf('\\'); + if (i != -1) { + ntdomain = username.substring(0, i).toUpperCase(Locale.ENGLISH); + username = username.substring(i+1); + } else if (ntdomain == null) { + ntdomain = ""; + } + this.ntdomain = ntdomain; + this.hostname = hostname; + this.username = username; + this.password = password; + this.logger = logger.getLogger(this.getClass(), "DEBUG NTLM"); + init0(); + } + + private void copybytes(byte[] dest, int destpos, String src, String enc) { + try { + byte[] x = src.getBytes(enc); + System.arraycopy(x, 0, dest, destpos, x.length); + } catch (UnsupportedEncodingException e) { + assert false; + } + } + + public String generateType1Msg(int flags) { + // XXX - should set "flags" in generated message + int dlen = ntdomain.length(); + type1[16]= (byte) (dlen % 256); + type1[17]= (byte) (dlen / 256); + type1[18] = type1[16]; + type1[19] = type1[17]; + if (dlen == 0) + type1[13] &= ~0x10; + + int hlen = hostname.length(); + type1[24]= (byte) (hlen % 256); + type1[25]= (byte) (hlen / 256); + type1[26] = type1[24]; + type1[27] = type1[25]; + + copybytes(type1, 32, hostname, "iso-8859-1"); + copybytes(type1, hlen+32, ntdomain, "iso-8859-1"); + type1[20] = (byte) ((hlen+32) % 256); + type1[21] = (byte) ((hlen+32) / 256); + + byte[] msg = new byte[32 + hlen + dlen]; + System.arraycopy(type1, 0, msg, 0, 32 + hlen + dlen); + if (logger.isLoggable(Level.FINE)) + logger.fine("type 1 message: " + toHex(msg)); + + String result = null; + try { + result = new String(BASE64EncoderStream.encode(msg), "iso-8859-1"); + } catch (UnsupportedEncodingException e) { + assert false; + } + return result; + } + + /** + * Convert a 7 byte array to an 8 byte array (for a des key with parity). + * Input starts at offset off. + */ + private byte[] makeDesKey(byte[] input, int off) { + int[] in = new int[input.length]; + for (int i = 0; i < in.length; i++) { + in[i] = input[i] < 0 ? input[i] + 256: input[i]; + } + byte[] out = new byte[8]; + out[0] = (byte)in[off+0]; + out[1] = (byte)(((in[off+0] << 7) & 0xFF) | (in[off+1] >> 1)); + out[2] = (byte)(((in[off+1] << 6) & 0xFF) | (in[off+2] >> 2)); + out[3] = (byte)(((in[off+2] << 5) & 0xFF) | (in[off+3] >> 3)); + out[4] = (byte)(((in[off+3] << 4) & 0xFF) | (in[off+4] >> 4)); + out[5] = (byte)(((in[off+4] << 3) & 0xFF) | (in[off+5] >> 5)); + out[6] = (byte)(((in[off+5] << 2) & 0xFF) | (in[off+6] >> 6)); + out[7] = (byte)((in[off+6] << 1) & 0xFF); + return out; + } + + private byte[] calcLMHash() throws GeneralSecurityException { + byte[] magic = {0x4b, 0x47, 0x53, 0x21, 0x40, 0x23, 0x24, 0x25}; + byte[] pwb = null; + try { + pwb = password.toUpperCase(Locale.ENGLISH).getBytes("iso-8859-1"); + } catch (UnsupportedEncodingException ex) { + // should never happen + assert false; + } + byte[] pwb1 = new byte[14]; + int len = password.length(); + if (len > 14) + len = 14; + System.arraycopy(pwb, 0, pwb1, 0, len); /* Zero padded */ + + DESKeySpec dks1 = new DESKeySpec(makeDesKey(pwb1, 0)); + DESKeySpec dks2 = new DESKeySpec(makeDesKey(pwb1, 7)); + + SecretKey key1 = fac.generateSecret(dks1); + SecretKey key2 = fac.generateSecret(dks2); + cipher.init(Cipher.ENCRYPT_MODE, key1); + byte[] out1 = cipher.doFinal(magic, 0, 8); + cipher.init(Cipher.ENCRYPT_MODE, key2); + byte[] out2 = cipher.doFinal(magic, 0, 8); + + byte[] result = new byte [21]; + System.arraycopy(out1, 0, result, 0, 8); + System.arraycopy(out2, 0, result, 8, 8); + return result; + } + + private byte[] calcNTHash() throws GeneralSecurityException { + byte[] pw = null; + try { + pw = password.getBytes("UnicodeLittleUnmarked"); + } catch (UnsupportedEncodingException e) { + assert false; + } + byte[] out = md4.digest(pw); + byte[] result = new byte[21]; + System.arraycopy(out, 0, result, 0, 16); + return result; + } + + /* + * Key is a 21 byte array. Split it into 3 7 byte chunks, + * convert each to 8 byte DES keys, encrypt the text arg with + * each key and return the three results in a sequential []. + */ + private byte[] calcResponse(byte[] key, byte[] text) + throws GeneralSecurityException { + assert key.length == 21; + DESKeySpec dks1 = new DESKeySpec(makeDesKey(key, 0)); + DESKeySpec dks2 = new DESKeySpec(makeDesKey(key, 7)); + DESKeySpec dks3 = new DESKeySpec(makeDesKey(key, 14)); + SecretKey key1 = fac.generateSecret(dks1); + SecretKey key2 = fac.generateSecret(dks2); + SecretKey key3 = fac.generateSecret(dks3); + cipher.init(Cipher.ENCRYPT_MODE, key1); + byte[] out1 = cipher.doFinal(text, 0, 8); + cipher.init(Cipher.ENCRYPT_MODE, key2); + byte[] out2 = cipher.doFinal(text, 0, 8); + cipher.init(Cipher.ENCRYPT_MODE, key3); + byte[] out3 = cipher.doFinal(text, 0, 8); + byte[] result = new byte[24]; + System.arraycopy(out1, 0, result, 0, 8); + System.arraycopy(out2, 0, result, 8, 8); + System.arraycopy(out3, 0, result, 16, 8); + return result; + } + + public String generateType3Msg(String challenge) { + try { + /* First decode the type2 message to get the server nonce */ + /* nonce is located at type2[24] for 8 bytes */ + + byte[] type2 = null; + try { + type2 = BASE64DecoderStream.decode(challenge.getBytes("us-ascii")); + } catch (UnsupportedEncodingException ex) { + // should never happen + assert false; + } + byte[] nonce = new byte[8]; + System.arraycopy(type2, 24, nonce, 0, 8); + + int ulen = username.length()*2; + type3[36] = type3[38] = (byte) (ulen % 256); + type3[37] = type3[39] = (byte) (ulen / 256); + int dlen = ntdomain.length()*2; + type3[28] = type3[30] = (byte) (dlen % 256); + type3[29] = type3[31] = (byte) (dlen / 256); + int hlen = hostname.length()*2; + type3[44] = type3[46] = (byte) (hlen % 256); + type3[45] = type3[47] = (byte) (hlen / 256); + + int l = 64; + copybytes(type3, l, ntdomain, "UnicodeLittleUnmarked"); + type3[32] = (byte) (l % 256); + type3[33] = (byte) (l / 256); + l += dlen; + copybytes(type3, l, username, "UnicodeLittleUnmarked"); + type3[40] = (byte) (l % 256); + type3[41] = (byte) (l / 256); + l += ulen; + copybytes(type3, l, hostname, "UnicodeLittleUnmarked"); + type3[48] = (byte) (l % 256); + type3[49] = (byte) (l / 256); + l += hlen; + + byte[] lmhash = calcLMHash(); + byte[] lmresponse = calcResponse(lmhash, nonce); + byte[] nthash = calcNTHash(); + byte[] ntresponse = calcResponse(nthash, nonce); + System.arraycopy(lmresponse, 0, type3, l, 24); + type3[16] = (byte) (l % 256); + type3[17] = (byte) (l / 256); + l += 24; + System.arraycopy(ntresponse, 0, type3, l, 24); + type3[24] = (byte) (l % 256); + type3[25] = (byte) (l / 256); + l += 24; + type3[56] = (byte) (l % 256); + type3[57] = (byte) (l / 256); + + byte[] msg = new byte[l]; + System.arraycopy(type3, 0, msg, 0, l); + if (logger.isLoggable(Level.FINE)) + logger.fine("type 3 message: " + toHex(msg)); + + String result = null; + try { + result = new String(BASE64EncoderStream.encode(msg), "iso-8859-1"); + } catch (UnsupportedEncodingException e) { + assert false; + } + return result; + + } catch (GeneralSecurityException ex) { + // should never happen + logger.log(Level.FINE, "GeneralSecurityException", ex); + return ""; // will fail later + } + } + + private static char[] hex = + { '0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F' }; + + private static String toHex(byte[] b) { + StringBuilder sb = new StringBuilder(b.length * 3); + for (int i = 0; i < b.length; i++) + sb.append(hex[(b[i]>>4)&0xF]).append(hex[b[i]&0xF]).append(' '); + return sb.toString(); + } +}
diff --git a/mail/src/main/java/com/sun/mail/auth/OAuth2SaslClient.java b/mail/src/main/java/com/sun/mail/auth/OAuth2SaslClient.java new file mode 100644 index 0000000..2ec95d4 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/auth/OAuth2SaslClient.java
@@ -0,0 +1,120 @@ +/* + * Copyright (c) 2014, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.auth; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.util.Map; +import java.security.Provider; +import java.security.Security; +import javax.security.sasl.*; +import javax.security.auth.callback.*; + +import com.sun.mail.util.ASCIIUtility; + +/** + * JavaMail SASL client for OAUTH2. + * + * @see <a href="http://tools.ietf.org/html/rfc6749"> + * RFC 6749 - OAuth 2.0 Authorization Framework</a> + * @see <a href="http://tools.ietf.org/html/rfc6750"> + * RFC 6750 - OAuth 2.0 Authorization Framework: Bearer Token Usage</a> + * @author Bill Shannon + */ +public class OAuth2SaslClient implements SaslClient { + private CallbackHandler cbh; + //private Map<String,?> props; // XXX - not currently used + private boolean complete = false; + + public OAuth2SaslClient(Map<String,?> props, CallbackHandler cbh) { + //this.props = props; + this.cbh = cbh; + } + + @Override + public String getMechanismName() { + return "XOAUTH2"; + } + + @Override + public boolean hasInitialResponse() { + return true; + } + + @Override + public byte[] evaluateChallenge(byte[] challenge) throws SaslException { + if (complete) + return new byte[0]; + + NameCallback ncb = new NameCallback("User name:"); + PasswordCallback pcb = new PasswordCallback("OAuth token:", false); + try { + cbh.handle(new Callback[] { ncb, pcb }); + } catch (UnsupportedCallbackException ex) { + throw new SaslException("Unsupported callback", ex); + } catch (IOException ex) { + throw new SaslException("Callback handler failed", ex); + } + + /* + * The OAuth token isn't really a password, and JavaMail doesn't + * use char[] for passwords, so we don't worry about storing the + * token in strings. + */ + String user = ncb.getName(); + String token = new String(pcb.getPassword()); + pcb.clearPassword(); + String resp = "user=" + user + "\001auth=Bearer " + token + "\001\001"; + byte[] response; + try { + response = resp.getBytes("utf-8"); + } catch (UnsupportedEncodingException ex) { + // fall back to ASCII + response = ASCIIUtility.getBytes(resp); + } + complete = true; + return response; + } + + @Override + public boolean isComplete() { + return complete; + } + + @Override + public byte[] unwrap(byte[] incoming, int offset, int len) + throws SaslException { + throw new IllegalStateException("OAUTH2 unwrap not supported"); + } + + @Override + public byte[] wrap(byte[] outgoing, int offset, int len) + throws SaslException { + throw new IllegalStateException("OAUTH2 wrap not supported"); + } + + @Override + public Object getNegotiatedProperty(String propName) { + if (!complete) + throw new IllegalStateException("OAUTH2 getNegotiatedProperty"); + return null; + } + + @Override + public void dispose() throws SaslException { + } +}
diff --git a/mail/src/main/java/com/sun/mail/auth/OAuth2SaslClientFactory.java b/mail/src/main/java/com/sun/mail/auth/OAuth2SaslClientFactory.java new file mode 100644 index 0000000..8450391 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/auth/OAuth2SaslClientFactory.java
@@ -0,0 +1,73 @@ +/* + * Copyright (c) 2014, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.auth; + +import java.util.*; +import java.security.Provider; +import java.security.Security; +import javax.security.sasl.*; +import javax.security.auth.callback.*; + +/** + * JavaMail SASL client factory for OAUTH2. + * + * @author Bill Shannon + */ +public class OAuth2SaslClientFactory implements SaslClientFactory { + + private static final String PROVIDER_NAME = "JavaMail-OAuth2"; + private static final String MECHANISM_NAME = "SaslClientFactory.XOAUTH2"; + + static class OAuth2Provider extends Provider { + private static final long serialVersionUID = -5371795551562287059L; + + public OAuth2Provider() { + super(PROVIDER_NAME, 1.0, "XOAUTH2 SASL Mechanism"); + put(MECHANISM_NAME, OAuth2SaslClientFactory.class.getName()); + } + } + + @Override + public SaslClient createSaslClient(String[] mechanisms, + String authorizationId, String protocol, + String serverName, Map<String,?> props, + CallbackHandler cbh) throws SaslException { + for (String m : mechanisms) { + if (m.equals("XOAUTH2")) + return new OAuth2SaslClient(props, cbh); + } + return null; + } + + @Override + public String[] getMechanismNames(Map<String,?> props) { + return new String[] { "XOAUTH2" }; + } + + /** + * Initialize this OAUTH2 provider, but only if there isn't one already. + * If we're not allowed to add this provider, just give up silently. + */ + public static void init() { + try { + if (Security.getProvider(PROVIDER_NAME) == null) + Security.addProvider(new OAuth2Provider()); + } catch (SecurityException ex) { + // oh well... + } + } +}
diff --git a/mail/src/main/java/com/sun/mail/auth/package.html b/mail/src/main/java/com/sun/mail/auth/package.html new file mode 100644 index 0000000..476f2ee --- /dev/null +++ b/mail/src/main/java/com/sun/mail/auth/package.html
@@ -0,0 +1,33 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> +<HTML> +<HEAD> +<meta http-equiv="Content-Type" content="text/html; charset=utf-8"> +<!-- + + Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<TITLE>com.sun.mail.auth package</TITLE> +</HEAD> +<BODY BGCOLOR="white"> + +<P> +This package includes internal authentication support classes and +<strong>SHOULD NOT BE USED DIRECTLY BY APPLICATIONS</strong>. +</P> + +</BODY> +</HTML>
diff --git a/mail/src/main/java/com/sun/mail/handlers/handler_base.java b/mail/src/main/java/com/sun/mail/handlers/handler_base.java new file mode 100644 index 0000000..5b4edae --- /dev/null +++ b/mail/src/main/java/com/sun/mail/handlers/handler_base.java
@@ -0,0 +1,85 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.handlers; + +import java.io.IOException; +import java.awt.datatransfer.DataFlavor; +import javax.activation.*; + +/** + * Base class for other DataContentHandlers. + */ +public abstract class handler_base implements DataContentHandler { + + /** + * Return an array of ActivationDataFlavors that we support. + * Usually there will be only one. + * + * @return array of ActivationDataFlavors that we support + */ + protected abstract ActivationDataFlavor[] getDataFlavors(); + + /** + * Given the flavor that matched, return the appropriate type of object. + * Usually there's only one flavor so just call getContent. + * + * @param aFlavor the ActivationDataFlavor + * @param ds DataSource containing the data + * @return the object + * @exception IOException for errors reading the data + */ + protected Object getData(ActivationDataFlavor aFlavor, DataSource ds) + throws IOException { + return getContent(ds); + } + + /** + * Return the DataFlavors for this <code>DataContentHandler</code>. + * + * @return The DataFlavors + */ + @Override + public DataFlavor[] getTransferDataFlavors() { + ActivationDataFlavor[] adf = getDataFlavors(); + if (adf.length == 1) // the common case + return new DataFlavor[] { adf[0] }; + DataFlavor[] df = new DataFlavor[adf.length]; + System.arraycopy(adf, 0, df, 0, adf.length); + return df; + } + + /** + * Return the Transfer Data of type DataFlavor from InputStream. + * + * @param df The DataFlavor + * @param ds The DataSource corresponding to the data + * @return the object + * @exception IOException for errors reading the data + */ + @Override + public Object getTransferData(DataFlavor df, DataSource ds) + throws IOException { + ActivationDataFlavor[] adf = getDataFlavors(); + for (int i = 0; i < adf.length; i++) { + // use ActivationDataFlavor.equals, which properly + // ignores Content-Type parameters in comparison + if (adf[i].equals(df)) + return getData(adf[i], ds); + } + return null; + } +}
diff --git a/mail/src/main/java/com/sun/mail/handlers/image_gif.java b/mail/src/main/java/com/sun/mail/handlers/image_gif.java new file mode 100644 index 0000000..0c75c8b --- /dev/null +++ b/mail/src/main/java/com/sun/mail/handlers/image_gif.java
@@ -0,0 +1,74 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.handlers; + +import java.io.*; +import java.awt.*; +import javax.activation.*; + +/** + * DataContentHandler for image/gif. + */ +public class image_gif extends handler_base { + private static ActivationDataFlavor[] myDF = { + new ActivationDataFlavor(Image.class, "image/gif", "GIF Image") + }; + + @Override + protected ActivationDataFlavor[] getDataFlavors() { + return myDF; + } + + @Override + public Object getContent(DataSource ds) throws IOException { + InputStream is = ds.getInputStream(); + int pos = 0; + int count; + byte buf[] = new byte[1024]; + + while ((count = is.read(buf, pos, buf.length - pos)) != -1) { + pos += count; + if (pos >= buf.length) { + int size = buf.length; + if (size < 256*1024) + size += size; + else + size += 256*1024; + byte tbuf[] = new byte[size]; + System.arraycopy(buf, 0, tbuf, 0, pos); + buf = tbuf; + } + } + Toolkit tk = Toolkit.getDefaultToolkit(); + return tk.createImage(buf, 0, pos); + } + + /** + * Write the object to the output stream, using the specified MIME type. + */ + @Override + public void writeTo(Object obj, String type, OutputStream os) + throws IOException { + if (!(obj instanceof Image)) + throw new IOException("\"" + getDataFlavors()[0].getMimeType() + + "\" DataContentHandler requires Image object, " + + "was given object of type " + obj.getClass().toString()); + + throw new IOException(getDataFlavors()[0].getMimeType() + + " encoding not supported"); + } +}
diff --git a/mail/src/main/java/com/sun/mail/handlers/image_jpeg.java b/mail/src/main/java/com/sun/mail/handlers/image_jpeg.java new file mode 100644 index 0000000..149d129 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/handlers/image_jpeg.java
@@ -0,0 +1,34 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.handlers; + +import java.awt.Image; +import javax.activation.ActivationDataFlavor; + +/** + * DataContentHandler for image/jpeg. + */ +public class image_jpeg extends image_gif { + private static ActivationDataFlavor[] myDF = { + new ActivationDataFlavor(Image.class, "image/jpeg", "JPEG Image") + }; + + @Override + protected ActivationDataFlavor[] getDataFlavors() { + return myDF; + } +}
diff --git a/mail/src/main/java/com/sun/mail/handlers/message_rfc822.java b/mail/src/main/java/com/sun/mail/handlers/message_rfc822.java new file mode 100644 index 0000000..07b4c81 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/handlers/message_rfc822.java
@@ -0,0 +1,90 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.handlers; + +import java.io.*; +import java.util.Properties; +import javax.activation.*; +import javax.mail.*; +import javax.mail.internet.*; + + +/** + * @author Christopher Cotton + */ + + +public class message_rfc822 extends handler_base { + + private static ActivationDataFlavor[] ourDataFlavor = { + new ActivationDataFlavor(Message.class, "message/rfc822", "Message") + }; + + @Override + protected ActivationDataFlavor[] getDataFlavors() { + return ourDataFlavor; + } + + /** + * Return the content. + */ + @Override + public Object getContent(DataSource ds) throws IOException { + // create a new MimeMessage + try { + Session session; + if (ds instanceof MessageAware) { + MessageContext mc = ((MessageAware)ds).getMessageContext(); + session = mc.getSession(); + } else { + // Hopefully a rare case. Also hopefully the application + // has created a default Session that can just be returned + // here. If not, the one we create here is better than + // nothing, but overall not a really good answer. + session = Session.getDefaultInstance(new Properties(), null); + } + return new MimeMessage(session, ds.getInputStream()); + } catch (MessagingException me) { + IOException ioex = + new IOException("Exception creating MimeMessage in " + + "message/rfc822 DataContentHandler"); + ioex.initCause(me); + throw ioex; + } + } + + /** + * Write the object as a byte stream. + */ + @Override + public void writeTo(Object obj, String mimeType, OutputStream os) + throws IOException { + // if the object is a message, we know how to write that out + if (obj instanceof Message) { + Message m = (Message)obj; + try { + m.writeTo(os); + } catch (MessagingException me) { + IOException ioex = new IOException("Exception writing message"); + ioex.initCause(me); + throw ioex; + } + } else { + throw new IOException("unsupported object"); + } + } +}
diff --git a/mail/src/main/java/com/sun/mail/handlers/multipart_mixed.java b/mail/src/main/java/com/sun/mail/handlers/multipart_mixed.java new file mode 100644 index 0000000..2e2bd2e --- /dev/null +++ b/mail/src/main/java/com/sun/mail/handlers/multipart_mixed.java
@@ -0,0 +1,69 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.handlers; + +import java.io.*; +import javax.activation.*; +import javax.mail.MessagingException; +import javax.mail.Multipart; +import javax.mail.internet.MimeMultipart; + + +public class multipart_mixed extends handler_base { + private static ActivationDataFlavor[] myDF = { + new ActivationDataFlavor(Multipart.class, + "multipart/mixed", "Multipart") + }; + + @Override + protected ActivationDataFlavor[] getDataFlavors() { + return myDF; + } + + /** + * Return the content. + */ + @Override + public Object getContent(DataSource ds) throws IOException { + try { + return new MimeMultipart(ds); + } catch (MessagingException e) { + IOException ioex = + new IOException("Exception while constructing MimeMultipart"); + ioex.initCause(e); + throw ioex; + } + } + + /** + * Write the object to the output stream, using the specific MIME type. + */ + @Override + public void writeTo(Object obj, String mimeType, OutputStream os) + throws IOException { + if (obj instanceof Multipart) { + try { + ((Multipart)obj).writeTo(os); + } catch (MessagingException e) { + IOException ioex = + new IOException("Exception writing Multipart"); + ioex.initCause(e); + throw ioex; + } + } + } +}
diff --git a/mail/src/main/java/com/sun/mail/handlers/package.html b/mail/src/main/java/com/sun/mail/handlers/package.html new file mode 100644 index 0000000..0aafa21 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/handlers/package.html
@@ -0,0 +1,33 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> +<HTML> +<HEAD> +<meta http-equiv="Content-Type" content="text/html; charset=utf-8"> +<!-- + + Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<TITLE>com.sun.mail.handlers package</TITLE> +</HEAD> +<BODY BGCOLOR="white"> + +<P> +This package includes internal data handler support classes and +<strong>SHOULD NOT BE USED DIRECTLY BY APPLICATIONS</strong>. +</P> + +</BODY> +</HTML>
diff --git a/mail/src/main/java/com/sun/mail/handlers/text_html.java b/mail/src/main/java/com/sun/mail/handlers/text_html.java new file mode 100644 index 0000000..24ef058 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/handlers/text_html.java
@@ -0,0 +1,34 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.handlers; + +import javax.activation.ActivationDataFlavor; + +/** + * DataContentHandler for text/html. + * + */ +public class text_html extends text_plain { + private static ActivationDataFlavor[] myDF = { + new ActivationDataFlavor(String.class, "text/html", "HTML String") + }; + + @Override + protected ActivationDataFlavor[] getDataFlavors() { + return myDF; + } +}
diff --git a/mail/src/main/java/com/sun/mail/handlers/text_plain.java b/mail/src/main/java/com/sun/mail/handlers/text_plain.java new file mode 100644 index 0000000..da0beab --- /dev/null +++ b/mail/src/main/java/com/sun/mail/handlers/text_plain.java
@@ -0,0 +1,153 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.handlers; + +import java.io.*; +import javax.activation.*; +import javax.mail.internet.ContentType; +import javax.mail.internet.MimeUtility; + +/** + * DataContentHandler for text/plain. + * + */ +public class text_plain extends handler_base { + private static ActivationDataFlavor[] myDF = { + new ActivationDataFlavor(String.class, "text/plain", "Text String") + }; + + /** + * An OuputStream wrapper that doesn't close the underlying stream. + */ + private static class NoCloseOutputStream extends FilterOutputStream { + public NoCloseOutputStream(OutputStream os) { + super(os); + } + + @Override + public void close() { + // do nothing + } + } + + @Override + protected ActivationDataFlavor[] getDataFlavors() { + return myDF; + } + + @Override + public Object getContent(DataSource ds) throws IOException { + String enc = null; + InputStreamReader is = null; + + try { + enc = getCharset(ds.getContentType()); + is = new InputStreamReader(ds.getInputStream(), enc); + } catch (IllegalArgumentException iex) { + /* + * An unknown charset of the form ISO-XXX-XXX will cause + * the JDK to throw an IllegalArgumentException. The + * JDK will attempt to create a classname using this string, + * but valid classnames must not contain the character '-', + * and this results in an IllegalArgumentException, rather than + * the expected UnsupportedEncodingException. Yikes. + */ + throw new UnsupportedEncodingException(enc); + } + + try { + int pos = 0; + int count; + char buf[] = new char[1024]; + + while ((count = is.read(buf, pos, buf.length - pos)) != -1) { + pos += count; + if (pos >= buf.length) { + int size = buf.length; + if (size < 256*1024) + size += size; + else + size += 256*1024; + char tbuf[] = new char[size]; + System.arraycopy(buf, 0, tbuf, 0, pos); + buf = tbuf; + } + } + return new String(buf, 0, pos); + } finally { + try { + is.close(); + } catch (IOException ex) { + // ignore it + } + } + } + + /** + * Write the object to the output stream, using the specified MIME type. + */ + @Override + public void writeTo(Object obj, String type, OutputStream os) + throws IOException { + if (!(obj instanceof String)) + throw new IOException("\"" + getDataFlavors()[0].getMimeType() + + "\" DataContentHandler requires String object, " + + "was given object of type " + obj.getClass().toString()); + + String enc = null; + OutputStreamWriter osw = null; + + try { + enc = getCharset(type); + osw = new OutputStreamWriter(new NoCloseOutputStream(os), enc); + } catch (IllegalArgumentException iex) { + /* + * An unknown charset of the form ISO-XXX-XXX will cause + * the JDK to throw an IllegalArgumentException. The + * JDK will attempt to create a classname using this string, + * but valid classnames must not contain the character '-', + * and this results in an IllegalArgumentException, rather than + * the expected UnsupportedEncodingException. Yikes. + */ + throw new UnsupportedEncodingException(enc); + } + + String s = (String)obj; + osw.write(s, 0, s.length()); + /* + * Have to call osw.close() instead of osw.flush() because + * some charset converts, such as the iso-2022-jp converter, + * don't output the "shift out" sequence unless they're closed. + * The NoCloseOutputStream wrapper prevents the underlying + * stream from being closed. + */ + osw.close(); + } + + private String getCharset(String type) { + try { + ContentType ct = new ContentType(type); + String charset = ct.getParameter("charset"); + if (charset == null) + // If the charset parameter is absent, use US-ASCII. + charset = "us-ascii"; + return MimeUtility.javaCharset(charset); + } catch (Exception ex) { + return null; + } + } +}
diff --git a/mail/src/main/java/com/sun/mail/handlers/text_xml.java b/mail/src/main/java/com/sun/mail/handlers/text_xml.java new file mode 100644 index 0000000..1f03017 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/handlers/text_xml.java
@@ -0,0 +1,121 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.handlers; + +import java.io.IOException; +import java.io.OutputStream; + +import javax.activation.ActivationDataFlavor; +import javax.activation.DataSource; +import javax.mail.internet.ContentType; +import javax.mail.internet.ParseException; +import javax.xml.transform.Source; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.TransformerException; +import javax.xml.transform.stream.StreamResult; +import javax.xml.transform.stream.StreamSource; + +/** + * DataContentHandler for text/xml. + * + * @author Anil Vijendran + * @author Bill Shannon + */ +public class text_xml extends text_plain { + + private static final ActivationDataFlavor[] flavors = { + new ActivationDataFlavor(String.class, "text/xml", "XML String"), + new ActivationDataFlavor(String.class, "application/xml", "XML String"), + new ActivationDataFlavor(StreamSource.class, "text/xml", "XML"), + new ActivationDataFlavor(StreamSource.class, "application/xml", "XML") + }; + + @Override + protected ActivationDataFlavor[] getDataFlavors() { + return flavors; + } + + @Override + protected Object getData(ActivationDataFlavor aFlavor, DataSource ds) + throws IOException { + if (aFlavor.getRepresentationClass() == String.class) + return super.getContent(ds); + else if (aFlavor.getRepresentationClass() == StreamSource.class) + return new StreamSource(ds.getInputStream()); + else + return null; // XXX - should never happen + } + + /** + */ + @Override + public void writeTo(Object obj, String mimeType, OutputStream os) + throws IOException { + if (!isXmlType(mimeType)) + throw new IOException( + "Invalid content type \"" + mimeType + "\" for text/xml DCH"); + if (obj instanceof String) { + super.writeTo(obj, mimeType, os); + return; + } + if (!(obj instanceof DataSource || obj instanceof Source)) { + throw new IOException("Invalid Object type = "+obj.getClass()+ + ". XmlDCH can only convert DataSource or Source to XML."); + } + + try { + Transformer transformer = + TransformerFactory.newInstance().newTransformer(); + StreamResult result = new StreamResult(os); + if (obj instanceof DataSource) { + // Streaming transform applies only to + // javax.xml.transform.StreamSource + transformer.transform( + new StreamSource(((DataSource)obj).getInputStream()), + result); + } else { + transformer.transform((Source)obj, result); + } + } catch (TransformerException ex) { + IOException ioex = new IOException( + "Unable to run the JAXP transformer on a stream " + + ex.getMessage()); + ioex.initCause(ex); + throw ioex; + } catch (RuntimeException ex) { + IOException ioex = new IOException( + "Unable to run the JAXP transformer on a stream " + + ex.getMessage()); + ioex.initCause(ex); + throw ioex; + } + } + + private boolean isXmlType(String type) { + try { + ContentType ct = new ContentType(type); + return ct.getSubType().equals("xml") && + (ct.getPrimaryType().equals("text") || + ct.getPrimaryType().equals("application")); + } catch (ParseException ex) { + return false; + } catch (RuntimeException ex) { + return false; + } + } +}
diff --git a/mail/src/main/java/com/sun/mail/iap/Argument.java b/mail/src/main/java/com/sun/mail/iap/Argument.java new file mode 100644 index 0000000..677c63d --- /dev/null +++ b/mail/src/main/java/com/sun/mail/iap/Argument.java
@@ -0,0 +1,431 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.iap; + +import java.util.List; +import java.util.ArrayList; +import java.io.*; +import java.nio.charset.Charset; + +import com.sun.mail.util.ASCIIUtility; + +/** + * @author John Mani + * @author Bill Shannon + */ + +public class Argument { + protected List<Object> items; + + /** + * Constructor + */ + public Argument() { + items = new ArrayList<>(1); + } + + /** + * Append the given Argument to this Argument. All items + * from the source argument are copied into this destination + * argument. + * + * @param arg the Argument to append + * @return this + */ + public Argument append(Argument arg) { + items.addAll(arg.items); + return this; + } + + /** + * Write out given string as an ASTRING, depending on the type + * of the characters inside the string. The string should + * contain only ASCII characters. <p> + * + * XXX: Hmm .. this should really be called writeASCII() + * + * @param s String to write out + * @return this + */ + public Argument writeString(String s) { + items.add(new AString(ASCIIUtility.getBytes(s))); + return this; + } + + /** + * Convert the given string into bytes in the specified + * charset, and write the bytes out as an ASTRING + * + * @param s String to write out + * @param charset the charset + * @return this + * @exception UnsupportedEncodingException for bad charset + */ + public Argument writeString(String s, String charset) + throws UnsupportedEncodingException { + if (charset == null) // convenience + writeString(s); + else + items.add(new AString(s.getBytes(charset))); + return this; + } + + /** + * Convert the given string into bytes in the specified + * charset, and write the bytes out as an ASTRING + * + * @param s String to write out + * @param charset the charset + * @return this + * @since JavaMail 1.6.0 + */ + public Argument writeString(String s, Charset charset) { + if (charset == null) // convenience + writeString(s); + else + items.add(new AString(s.getBytes(charset))); + return this; + } + + /** + * Write out given string as an NSTRING, depending on the type + * of the characters inside the string. The string should + * contain only ASCII characters. <p> + * + * @param s String to write out + * @return this + * @since JavaMail 1.5.1 + */ + public Argument writeNString(String s) { + if (s == null) + items.add(new NString(null)); + else + items.add(new NString(ASCIIUtility.getBytes(s))); + return this; + } + + /** + * Convert the given string into bytes in the specified + * charset, and write the bytes out as an NSTRING + * + * @param s String to write out + * @param charset the charset + * @return this + * @exception UnsupportedEncodingException for bad charset + * @since JavaMail 1.5.1 + */ + public Argument writeNString(String s, String charset) + throws UnsupportedEncodingException { + if (s == null) + items.add(new NString(null)); + else if (charset == null) // convenience + writeString(s); + else + items.add(new NString(s.getBytes(charset))); + return this; + } + + /** + * Convert the given string into bytes in the specified + * charset, and write the bytes out as an NSTRING + * + * @param s String to write out + * @param charset the charset + * @return this + * @since JavaMail 1.6.0 + */ + public Argument writeNString(String s, Charset charset) { + if (s == null) + items.add(new NString(null)); + else if (charset == null) // convenience + writeString(s); + else + items.add(new NString(s.getBytes(charset))); + return this; + } + + /** + * Write out given byte[] as a Literal. + * @param b byte[] to write out + * @return this + */ + public Argument writeBytes(byte[] b) { + items.add(b); + return this; + } + + /** + * Write out given ByteArrayOutputStream as a Literal. + * @param b ByteArrayOutputStream to be written out. + * @return this + */ + public Argument writeBytes(ByteArrayOutputStream b) { + items.add(b); + return this; + } + + /** + * Write out given data as a literal. + * @param b Literal representing data to be written out. + * @return this + */ + public Argument writeBytes(Literal b) { + items.add(b); + return this; + } + + /** + * Write out given string as an Atom. Note that an Atom can contain only + * certain US-ASCII characters. No validation is done on the characters + * in the string. + * @param s String + * @return this + */ + public Argument writeAtom(String s) { + items.add(new Atom(s)); + return this; + } + + /** + * Write out number. + * @param i number + * @return this + */ + public Argument writeNumber(int i) { + items.add(Integer.valueOf(i)); + return this; + } + + /** + * Write out number. + * @param i number + * @return this + */ + public Argument writeNumber(long i) { + items.add(Long.valueOf(i)); + return this; + } + + /** + * Write out as parenthesised list. + * + * @param c the Argument + * @return this + */ + public Argument writeArgument(Argument c) { + items.add(c); + return this; + } + + /* + * Write out all the buffered items into the output stream. + */ + public void write(Protocol protocol) + throws IOException, ProtocolException { + int size = items != null ? items.size() : 0; + DataOutputStream os = (DataOutputStream)protocol.getOutputStream(); + + for (int i=0; i < size; i++) { + if (i > 0) // write delimiter if not the first item + os.write(' '); + + Object o = items.get(i); + if (o instanceof Atom) { + os.writeBytes(((Atom)o).string); + } else if (o instanceof Number) { + os.writeBytes(((Number)o).toString()); + } else if (o instanceof AString) { + astring(((AString)o).bytes, protocol); + } else if (o instanceof NString) { + nstring(((NString)o).bytes, protocol); + } else if (o instanceof byte[]) { + literal((byte[])o, protocol); + } else if (o instanceof ByteArrayOutputStream) { + literal((ByteArrayOutputStream)o, protocol); + } else if (o instanceof Literal) { + literal((Literal)o, protocol); + } else if (o instanceof Argument) { + os.write('('); // open parans + ((Argument)o).write(protocol); + os.write(')'); // close parans + } + } + } + + /** + * Write out given String as either an Atom, QuotedString or Literal + */ + private void astring(byte[] bytes, Protocol protocol) + throws IOException, ProtocolException { + nastring(bytes, protocol, false); + } + + /** + * Write out given String as either NIL, QuotedString, or Literal. + */ + private void nstring(byte[] bytes, Protocol protocol) + throws IOException, ProtocolException { + if (bytes == null) { + DataOutputStream os = (DataOutputStream)protocol.getOutputStream(); + os.writeBytes("NIL"); + } else + nastring(bytes, protocol, true); + } + + private void nastring(byte[] bytes, Protocol protocol, boolean doQuote) + throws IOException, ProtocolException { + DataOutputStream os = (DataOutputStream)protocol.getOutputStream(); + int len = bytes.length; + + // If length is greater than 1024 bytes, send as literal + if (len > 1024) { + literal(bytes, protocol); + return; + } + + // if 0 length, send as quoted-string + boolean quote = len == 0 ? true : doQuote; + boolean escape = false; + boolean utf8 = protocol.supportsUtf8(); + + byte b; + for (int i = 0; i < len; i++) { + b = bytes[i]; + if (b == '\0' || b == '\r' || b == '\n' || + (!utf8 && ((b & 0xff) > 0177))) { + // NUL, CR or LF means the bytes need to be sent as literals + literal(bytes, protocol); + return; + } + if (b == '*' || b == '%' || b == '(' || b == ')' || b == '{' || + b == '"' || b == '\\' || + ((b & 0xff) <= ' ') || ((b & 0xff) > 0177)) { + quote = true; + if (b == '"' || b == '\\') // need to escape these characters + escape = true; + } + } + + /* + * Make sure the (case-independent) string "NIL" is always quoted, + * so as not to be confused with a real NIL (handled above in nstring). + * This is more than is necessary, but it's rare to begin with and + * this makes it safer than doing the test in nstring above in case + * some code calls writeString when it should call writeNString. + */ + if (!quote && bytes.length == 3 && + (bytes[0] == 'N' || bytes[0] == 'n') && + (bytes[1] == 'I' || bytes[1] == 'i') && + (bytes[2] == 'L' || bytes[2] == 'l')) + quote = true; + + if (quote) // start quote + os.write('"'); + + if (escape) { + // already quoted + for (int i = 0; i < len; i++) { + b = bytes[i]; + if (b == '"' || b == '\\') + os.write('\\'); + os.write(b); + } + } else + os.write(bytes); + + + if (quote) // end quote + os.write('"'); + } + + /** + * Write out given byte[] as a literal + */ + private void literal(byte[] b, Protocol protocol) + throws IOException, ProtocolException { + startLiteral(protocol, b.length).write(b); + } + + /** + * Write out given ByteArrayOutputStream as a literal. + */ + private void literal(ByteArrayOutputStream b, Protocol protocol) + throws IOException, ProtocolException { + b.writeTo(startLiteral(protocol, b.size())); + } + + /** + * Write out given Literal as a literal. + */ + private void literal(Literal b, Protocol protocol) + throws IOException, ProtocolException { + b.writeTo(startLiteral(protocol, b.size())); + } + + private OutputStream startLiteral(Protocol protocol, int size) + throws IOException, ProtocolException { + DataOutputStream os = (DataOutputStream)protocol.getOutputStream(); + boolean nonSync = protocol.supportsNonSyncLiterals(); + + os.write('{'); + os.writeBytes(Integer.toString(size)); + if (nonSync) // server supports non-sync literals + os.writeBytes("+}\r\n"); + else + os.writeBytes("}\r\n"); + os.flush(); + + // If we are using synchronized literals, wait for the server's + // continuation signal + if (!nonSync) { + for (; ;) { + Response r = protocol.readResponse(); + if (r.isContinuation()) + break; + if (r.isTagged()) + throw new LiteralException(r); + // XXX - throw away untagged responses; + // violates IMAP spec, hope no servers do this + } + } + return os; + } +} + +class Atom { + String string; + + Atom(String s) { + string = s; + } +} + +class AString { + byte[] bytes; + + AString(byte[] b) { + bytes = b; + } +} + +class NString { + byte[] bytes; + + NString(byte[] b) { + bytes = b; + } +}
diff --git a/mail/src/main/java/com/sun/mail/iap/BadCommandException.java b/mail/src/main/java/com/sun/mail/iap/BadCommandException.java new file mode 100644 index 0000000..4a1b33b --- /dev/null +++ b/mail/src/main/java/com/sun/mail/iap/BadCommandException.java
@@ -0,0 +1,49 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.iap; + +/** + * @author John Mani + */ + +public class BadCommandException extends ProtocolException { + + private static final long serialVersionUID = 5769722539397237515L; + + /** + * Constructs an BadCommandException with no detail message. + */ + public BadCommandException() { + super(); + } + + /** + * Constructs an BadCommandException with the specified detail message. + * @param s the detail message + */ + public BadCommandException(String s) { + super(s); + } + + /** + * Constructs an BadCommandException with the specified Response. + * @param r the Response + */ + public BadCommandException(Response r) { + super(r); + } +}
diff --git a/mail/src/main/java/com/sun/mail/iap/ByteArray.java b/mail/src/main/java/com/sun/mail/iap/ByteArray.java new file mode 100644 index 0000000..e05110b --- /dev/null +++ b/mail/src/main/java/com/sun/mail/iap/ByteArray.java
@@ -0,0 +1,125 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.iap; + +import java.io.ByteArrayInputStream; + +/** + * A simple wrapper around a byte array, with a start position and + * count of bytes. + * + * @author John Mani + */ + +public class ByteArray { + private byte[] bytes; // the byte array + private int start; // start position + private int count; // count of bytes + + /** + * Constructor + * + * @param b the byte array to wrap + * @param start start position in byte array + * @param count number of bytes in byte array + */ + public ByteArray(byte[] b, int start, int count) { + bytes = b; + this.start = start; + this.count = count; + } + + /** + * Constructor that creates a byte array of the specified size. + * + * @param size the size of the ByteArray + * @since JavaMail 1.4.1 + */ + public ByteArray(int size) { + this(new byte[size], 0, size); + } + + /** + * Returns the internal byte array. Note that this is a live + * reference to the actual data, not a copy. + * + * @return the wrapped byte array + */ + public byte[] getBytes() { + return bytes; + } + + /** + * Returns a new byte array that is a copy of the data. + * + * @return a new byte array with the bytes from start for count + */ + public byte[] getNewBytes() { + byte[] b = new byte[count]; + System.arraycopy(bytes, start, b, 0, count); + return b; + } + + /** + * Returns the start position + * + * @return the start position + */ + public int getStart() { + return start; + } + + /** + * Returns the count of bytes + * + * @return the number of bytes + */ + public int getCount() { + return count; + } + + /** + * Set the count of bytes. + * + * @param count the number of bytes + * @since JavaMail 1.4.1 + */ + public void setCount(int count) { + this.count = count; + } + + /** + * Returns a ByteArrayInputStream. + * + * @return the ByteArrayInputStream + */ + public ByteArrayInputStream toByteArrayInputStream() { + return new ByteArrayInputStream(bytes, start, count); + } + + /** + * Grow the byte array by incr bytes. + * + * @param incr how much to grow + * @since JavaMail 1.4.1 + */ + public void grow(int incr) { + byte[] nbuf = new byte[bytes.length + incr]; + System.arraycopy(bytes, 0, nbuf, 0, bytes.length); + bytes = nbuf; + } +}
diff --git a/mail/src/main/java/com/sun/mail/iap/CommandFailedException.java b/mail/src/main/java/com/sun/mail/iap/CommandFailedException.java new file mode 100644 index 0000000..8c141e7 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/iap/CommandFailedException.java
@@ -0,0 +1,49 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.iap; + +/** + * @author John Mani + */ + +public class CommandFailedException extends ProtocolException { + + private static final long serialVersionUID = 793932807880443631L; + + /** + * Constructs an CommandFailedException with no detail message. + */ + public CommandFailedException() { + super(); + } + + /** + * Constructs an CommandFailedException with the specified detail message. + * @param s the detail message + */ + public CommandFailedException(String s) { + super(s); + } + + /** + * Constructs an CommandFailedException with the specified Response. + * @param r the Response. + */ + public CommandFailedException(Response r) { + super(r); + } +}
diff --git a/mail/src/main/java/com/sun/mail/iap/ConnectionException.java b/mail/src/main/java/com/sun/mail/iap/ConnectionException.java new file mode 100644 index 0000000..12dd496 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/iap/ConnectionException.java
@@ -0,0 +1,58 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.iap; + +/** + * @author John Mani + */ + +public class ConnectionException extends ProtocolException { + private transient Protocol p; + + private static final long serialVersionUID = 5749739604257464727L; + + /** + * Constructs an ConnectionException with no detail message. + */ + public ConnectionException() { + super(); + } + + /** + * Constructs an ConnectionException with the specified detail message. + * + * @param s the detail message + */ + public ConnectionException(String s) { + super(s); + } + + /** + * Constructs an ConnectionException with the specified Response. + * + * @param p the Protocol object + * @param r the Response + */ + public ConnectionException(Protocol p, Response r) { + super(r); + this.p = p; + } + + public Protocol getProtocol() { + return p; + } +}
diff --git a/mail/src/main/java/com/sun/mail/iap/Literal.java b/mail/src/main/java/com/sun/mail/iap/Literal.java new file mode 100644 index 0000000..14c592b --- /dev/null +++ b/mail/src/main/java/com/sun/mail/iap/Literal.java
@@ -0,0 +1,43 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.iap; + +import java.io.*; + +/** + * An interface for objects that provide data dynamically for use in + * a literal protocol element. + * + * @author Bill Shannon + */ + +public interface Literal { + /** + * Return the size of the data. + * + * @return the size of the data + */ + public int size(); + + /** + * Write the data to the OutputStream. + * + * @param os the output stream + * @exception IOException for I/O errors + */ + public void writeTo(OutputStream os) throws IOException; +}
diff --git a/mail/src/main/java/com/sun/mail/iap/LiteralException.java b/mail/src/main/java/com/sun/mail/iap/LiteralException.java new file mode 100644 index 0000000..660747f --- /dev/null +++ b/mail/src/main/java/com/sun/mail/iap/LiteralException.java
@@ -0,0 +1,36 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.iap; + +/** + * @author Bill Shannon + */ + +public class LiteralException extends ProtocolException { + + private static final long serialVersionUID = -6919179828339609913L; + + /** + * Constructs a LiteralException with the specified Response object. + * + * @param r the response object + */ + public LiteralException(Response r) { + super(r.toString()); + response = r; + } +}
diff --git a/mail/src/main/java/com/sun/mail/iap/ParsingException.java b/mail/src/main/java/com/sun/mail/iap/ParsingException.java new file mode 100644 index 0000000..a1e7168 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/iap/ParsingException.java
@@ -0,0 +1,49 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.iap; + +/** + * @author John Mani + */ + +public class ParsingException extends ProtocolException { + + private static final long serialVersionUID = 7756119840142724839L; + + /** + * Constructs an ParsingException with no detail message. + */ + public ParsingException() { + super(); + } + + /** + * Constructs an ParsingException with the specified detail message. + * @param s the detail message + */ + public ParsingException(String s) { + super(s); + } + + /** + * Constructs an ParsingException with the specified Response. + * @param r the Response + */ + public ParsingException(Response r) { + super(r); + } +}
diff --git a/mail/src/main/java/com/sun/mail/iap/Protocol.java b/mail/src/main/java/com/sun/mail/iap/Protocol.java new file mode 100644 index 0000000..1339532 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/iap/Protocol.java
@@ -0,0 +1,671 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.iap; + +import java.util.Properties; +import java.io.*; +import java.nio.channels.SocketChannel; +import java.net.*; +import javax.net.ssl.SSLSocket; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; +import java.util.zip.Deflater; +import java.util.zip.DeflaterOutputStream; +import java.util.zip.Inflater; +import java.util.zip.InflaterInputStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; + +import com.sun.mail.util.PropUtil; +import com.sun.mail.util.MailLogger; +import com.sun.mail.util.SocketFetcher; +import com.sun.mail.util.TraceInputStream; +import com.sun.mail.util.TraceOutputStream; + +/** + * General protocol handling code for IMAP-like protocols. <p> + * + * The Protocol object is multithread safe. + * + * @author John Mani + * @author Max Spivak + * @author Bill Shannon + */ + +public class Protocol { + protected String host; + private Socket socket; + // in case we turn on TLS, we'll need these later + protected boolean quote; + protected MailLogger logger; + protected MailLogger traceLogger; + protected Properties props; + protected String prefix; + + private TraceInputStream traceInput; // the Tracer + private volatile ResponseInputStream input; + + private TraceOutputStream traceOutput; // the Tracer + private volatile DataOutputStream output; + + private int tagCounter = 0; + private final String tagPrefix; + + private String localHostName; + + private final List<ResponseHandler> handlers + = new CopyOnWriteArrayList<>(); + + private volatile long timestamp; + + // package private, to allow testing + static final AtomicInteger tagNum = new AtomicInteger(); + + private static final byte[] CRLF = { (byte)'\r', (byte)'\n'}; + + /** + * Constructor. <p> + * + * Opens a connection to the given host at given port. + * + * @param host host to connect to + * @param port portnumber to connect to + * @param props Properties object used by this protocol + * @param prefix Prefix to prepend to property keys + * @param isSSL use SSL? + * @param logger log messages here + * @exception IOException for I/O errors + * @exception ProtocolException for protocol failures + */ + public Protocol(String host, int port, + Properties props, String prefix, + boolean isSSL, MailLogger logger) + throws IOException, ProtocolException { + boolean connected = false; // did constructor succeed? + tagPrefix = computePrefix(props, prefix); + try { + this.host = host; + this.props = props; + this.prefix = prefix; + this.logger = logger; + traceLogger = logger.getSubLogger("protocol", null); + + socket = SocketFetcher.getSocket(host, port, props, prefix, isSSL); + quote = PropUtil.getBooleanProperty(props, + "mail.debug.quote", false); + + initStreams(); + + // Read server greeting + processGreeting(readResponse()); + + timestamp = System.currentTimeMillis(); + + connected = true; // must be last statement in constructor + } finally { + /* + * If we get here because an exception was thrown, we need + * to disconnect to avoid leaving a connected socket that + * no one will be able to use because this object was never + * completely constructed. + */ + if (!connected) + disconnect(); + } + } + + private void initStreams() throws IOException { + traceInput = new TraceInputStream(socket.getInputStream(), traceLogger); + traceInput.setQuote(quote); + input = new ResponseInputStream(traceInput); + + traceOutput = + new TraceOutputStream(socket.getOutputStream(), traceLogger); + traceOutput.setQuote(quote); + output = new DataOutputStream(new BufferedOutputStream(traceOutput)); + } + + /** + * Compute the tag prefix to be used for this connection. + * Start with "A" - "Z", then "AA" - "ZZ", and finally "AAA" - "ZZZ". + * Wrap around after that. + */ + private String computePrefix(Properties props, String prefix) { + // XXX - in case someone depends on the tag prefix + if (PropUtil.getBooleanProperty(props, + prefix + ".reusetagprefix", false)) + return "A"; + // tag prefix, wrap around after three letters + int n = tagNum.getAndIncrement() % (26*26*26 + 26*26 + 26); + String tagPrefix; + if (n < 26) + tagPrefix = new String(new char[] { (char)('A' + n) }); + else if (n < (26*26 + 26)) { + n -= 26; + tagPrefix = new String(new char[] { + (char)('A' + n/26), (char)('A' + n%26) }); + } else { + n -= (26*26 + 26); + tagPrefix = new String(new char[] { + (char)('A' + n/(26*26)), + (char)('A' + (n%(26*26))/26), + (char)('A' + n%26) }); + } + return tagPrefix; + } + + /** + * Constructor for debugging. + * + * @param in the InputStream to read from + * @param out the PrintStream to write to + * @param props Properties object used by this protocol + * @param debug true to enable debugging output + * @exception IOException for I/O errors + */ + public Protocol(InputStream in, PrintStream out, Properties props, + boolean debug) throws IOException { + this.host = "localhost"; + this.props = props; + this.quote = false; + tagPrefix = computePrefix(props, "mail.imap"); + logger = new MailLogger(this.getClass(), "DEBUG", debug, System.out); + traceLogger = logger.getSubLogger("protocol", null); + + // XXX - inlined initStreams, won't allow later startTLS + traceInput = new TraceInputStream(in, traceLogger); + traceInput.setQuote(quote); + input = new ResponseInputStream(traceInput); + + traceOutput = new TraceOutputStream(out, traceLogger); + traceOutput.setQuote(quote); + output = new DataOutputStream(new BufferedOutputStream(traceOutput)); + + timestamp = System.currentTimeMillis(); + } + + /** + * Returns the timestamp. + * + * @return the timestamp + */ + public long getTimestamp() { + return timestamp; + } + + /** + * Adds a response handler. + * + * @param h the response handler + */ + public void addResponseHandler(ResponseHandler h) { + handlers.add(h); + } + + /** + * Removed the specified response handler. + * + * @param h the response handler + */ + public void removeResponseHandler(ResponseHandler h) { + handlers.remove(h); + } + + /** + * Notify response handlers + * + * @param responses the responses + */ + public void notifyResponseHandlers(Response[] responses) { + if (handlers.isEmpty()) { + return; + } + + for (Response r : responses) { + if (r != null) { + for (ResponseHandler rh : handlers) { + if (rh != null) { + rh.handleResponse(r); + } + } + } + } + } + + protected void processGreeting(Response r) throws ProtocolException { + if (r.isBYE()) + throw new ConnectionException(this, r); + } + + /** + * Return the Protocol's InputStream. + * + * @return the input stream + */ + protected ResponseInputStream getInputStream() { + return input; + } + + /** + * Return the Protocol's OutputStream + * + * @return the output stream + */ + protected OutputStream getOutputStream() { + return output; + } + + /** + * Returns whether this Protocol supports non-synchronizing literals + * Default is false. Subclasses should override this if required + * + * @return true if the server supports non-synchronizing literals + */ + protected synchronized boolean supportsNonSyncLiterals() { + return false; + } + + public Response readResponse() + throws IOException, ProtocolException { + return new Response(this); + } + + /** + * Is another response available in our buffer? + * + * @return true if another response is in the buffer + * @since JavaMail 1.5.4 + */ + public boolean hasResponse() { + /* + * XXX - Really should peek ahead in the buffer to see + * if there's a *complete* response available, but if there + * isn't who's going to read more data into the buffer + * until there is? + */ + try { + return input.available() > 0; + } catch (IOException ex) { + } + return false; + } + + /** + * Return a buffer to be used to read a response. + * The default implementation returns null, which causes + * a new buffer to be allocated for every response. + * + * @return the buffer to use + * @since JavaMail 1.4.1 + */ + protected ByteArray getResponseBuffer() { + return null; + } + + public String writeCommand(String command, Argument args) + throws IOException, ProtocolException { + // assert Thread.holdsLock(this); + // can't assert because it's called from constructor + String tag = tagPrefix + Integer.toString(tagCounter++); // unique tag + + output.writeBytes(tag + " " + command); + + if (args != null) { + output.write(' '); + args.write(this); + } + + output.write(CRLF); + output.flush(); + return tag; + } + + /** + * Send a command to the server. Collect all responses until either + * the corresponding command completion response or a BYE response + * (indicating server failure). Return all the collected responses. + * + * @param command the command + * @param args the arguments + * @return array of Response objects returned by the server + */ + public synchronized Response[] command(String command, Argument args) { + commandStart(command); + List<Response> v = new ArrayList<>(); + boolean done = false; + String tag = null; + + // write the command + try { + tag = writeCommand(command, args); + } catch (LiteralException lex) { + v.add(lex.getResponse()); + done = true; + } catch (Exception ex) { + // Convert this into a BYE response + v.add(Response.byeResponse(ex)); + done = true; + } + + Response byeResp = null; + while (!done) { + Response r = null; + try { + r = readResponse(); + } catch (IOException ioex) { + if (byeResp == null) // convert this into a BYE response + byeResp = Response.byeResponse(ioex); + // else, connection closed after BYE was sent + break; + } catch (ProtocolException pex) { + logger.log(Level.FINE, "ignoring bad response", pex); + continue; // skip this response + } + + if (r.isBYE()) { + byeResp = r; + continue; + } + + v.add(r); + + // If this is a matching command completion response, we are done + if (r.isTagged() && r.getTag().equals(tag)) + done = true; + } + + if (byeResp != null) + v.add(byeResp); // must be last + Response[] responses = new Response[v.size()]; + v.toArray(responses); + timestamp = System.currentTimeMillis(); + commandEnd(); + return responses; + } + + /** + * Convenience routine to handle OK, NO, BAD and BYE responses. + * + * @param response the response + * @exception ProtocolException for protocol failures + */ + public void handleResult(Response response) throws ProtocolException { + if (response.isOK()) + return; + else if (response.isNO()) + throw new CommandFailedException(response); + else if (response.isBAD()) + throw new BadCommandException(response); + else if (response.isBYE()) { + disconnect(); + throw new ConnectionException(this, response); + } + } + + /** + * Convenience routine to handle simple IAP commands + * that do not have responses specific to that command. + * + * @param cmd the command + * @param args the arguments + * @exception ProtocolException for protocol failures + */ + public void simpleCommand(String cmd, Argument args) + throws ProtocolException { + // Issue command + Response[] r = command(cmd, args); + + // dispatch untagged responses + notifyResponseHandlers(r); + + // Handle result of this command + handleResult(r[r.length-1]); + } + + /** + * Start TLS on the current connection. + * <code>cmd</code> is the command to issue to start TLS negotiation. + * If the command succeeds, we begin TLS negotiation. + * If the socket is already an SSLSocket this is a nop and the command + * is not issued. + * + * @param cmd the command to issue + * @exception IOException for I/O errors + * @exception ProtocolException for protocol failures + */ + public synchronized void startTLS(String cmd) + throws IOException, ProtocolException { + if (socket instanceof SSLSocket) + return; // nothing to do + simpleCommand(cmd, null); + socket = SocketFetcher.startTLS(socket, host, props, prefix); + initStreams(); + } + + /** + * Start compression on the current connection. + * <code>cmd</code> is the command to issue to start compression. + * If the command succeeds, we begin compression. + * + * @param cmd the command to issue + * @exception IOException for I/O errors + * @exception ProtocolException for protocol failures + */ + public synchronized void startCompression(String cmd) + throws IOException, ProtocolException { + // XXX - check whether compression is already enabled? + simpleCommand(cmd, null); + + // need to create our own Inflater and Deflater in order to set nowrap + Inflater inf = new Inflater(true); + traceInput = new TraceInputStream(new InflaterInputStream( + socket.getInputStream(), inf), traceLogger); + traceInput.setQuote(quote); + input = new ResponseInputStream(traceInput); + + // configure the Deflater + int level = PropUtil.getIntProperty(props, prefix + ".compress.level", + Deflater.DEFAULT_COMPRESSION); + int strategy = PropUtil.getIntProperty(props, + prefix + ".compress.strategy", + Deflater.DEFAULT_STRATEGY); + if (logger.isLoggable(Level.FINE)) + logger.log(Level.FINE, + "Creating Deflater with compression level {0} and strategy {1}", + new Object[] { level, strategy }); + Deflater def = new Deflater(Deflater.DEFAULT_COMPRESSION, true); + try { + def.setLevel(level); + } catch (IllegalArgumentException ex) { + logger.log(Level.FINE, "Ignoring bad compression level", ex); + } + try { + def.setStrategy(strategy); + } catch (IllegalArgumentException ex) { + logger.log(Level.FINE, "Ignoring bad compression strategy", ex); + } + traceOutput = new TraceOutputStream(new DeflaterOutputStream( + socket.getOutputStream(), def, true), traceLogger); + traceOutput.setQuote(quote); + output = new DataOutputStream(new BufferedOutputStream(traceOutput)); + } + + /** + * Is this connection using an SSL socket? + * + * @return true if using SSL + * @since JavaMail 1.4.6 + */ + public boolean isSSL() { + return socket instanceof SSLSocket; + } + + /** + * Return the address the socket connected to. + * + * @return the InetAddress the socket is connected to + * @since JavaMail 1.5.2 + */ + public InetAddress getInetAddress() { + return socket.getInetAddress(); + } + + /** + * Return the SocketChannel associated with this connection, if any. + * + * @return the SocketChannel + * @since JavaMail 1.5.2 + */ + public SocketChannel getChannel() { + SocketChannel ret = socket.getChannel(); + if (ret != null) + return ret; + + // XXX - Android is broken and SSL wrapped sockets don't delegate + // the getChannel method to the wrapped Socket + if (socket instanceof SSLSocket) { + try { + Field f = socket.getClass().getDeclaredField("socket"); + f.setAccessible(true); + Socket s = (Socket)f.get(socket); + ret = s.getChannel(); + } catch (Exception ex) { + // ignore anything that might go wrong + } + } + return ret; + } + + /** + * Does the server support UTF-8? + * This implementation returns false. + * Subclasses should override as appropriate. + * + * @return true if the server supports UTF-8 + * @since JavaMail 1.6.0 + */ + public boolean supportsUtf8() { + return false; + } + + /** + * Disconnect. + */ + protected synchronized void disconnect() { + if (socket != null) { + try { + socket.close(); + } catch (IOException e) { + // ignore it + } + socket = null; + } + } + + /** + * Get the name of the local host. + * The property <prefix>.localhost overrides + * <prefix>.localaddress, + * which overrides what InetAddress would tell us. + * + * @return the name of the local host + */ + protected synchronized String getLocalHost() { + // get our hostname and cache it for future use + if (localHostName == null || localHostName.length() <= 0) + localHostName = + props.getProperty(prefix + ".localhost"); + if (localHostName == null || localHostName.length() <= 0) + localHostName = + props.getProperty(prefix + ".localaddress"); + try { + if (localHostName == null || localHostName.length() <= 0) { + InetAddress localHost = InetAddress.getLocalHost(); + localHostName = localHost.getCanonicalHostName(); + // if we can't get our name, use local address literal + if (localHostName == null) + // XXX - not correct for IPv6 + localHostName = "[" + localHost.getHostAddress() + "]"; + } + } catch (UnknownHostException uhex) { + } + + // last chance, try to get our address from our socket + if (localHostName == null || localHostName.length() <= 0) { + if (socket != null && socket.isBound()) { + InetAddress localHost = socket.getLocalAddress(); + localHostName = localHost.getCanonicalHostName(); + // if we can't get our name, use local address literal + if (localHostName == null) + // XXX - not correct for IPv6 + localHostName = "[" + localHost.getHostAddress() + "]"; + } + } + return localHostName; + } + + /** + * Is protocol tracing enabled? + * + * @return true if protocol tracing is enabled + */ + protected boolean isTracing() { + return traceLogger.isLoggable(Level.FINEST); + } + + /** + * Temporarily turn off protocol tracing, e.g., to prevent + * tracing the authentication sequence, including the password. + */ + protected void suspendTracing() { + if (traceLogger.isLoggable(Level.FINEST)) { + traceInput.setTrace(false); + traceOutput.setTrace(false); + } + } + + /** + * Resume protocol tracing, if it was enabled to begin with. + */ + protected void resumeTracing() { + if (traceLogger.isLoggable(Level.FINEST)) { + traceInput.setTrace(true); + traceOutput.setTrace(true); + } + } + + /** + * Finalizer. + */ + @Override + protected void finalize() throws Throwable { + try { + disconnect(); + } finally { + super.finalize(); + } + } + + /* + * Probe points for GlassFish monitoring. + */ + private void commandStart(String command) { } + private void commandEnd() { } +}
diff --git a/mail/src/main/java/com/sun/mail/iap/ProtocolException.java b/mail/src/main/java/com/sun/mail/iap/ProtocolException.java new file mode 100644 index 0000000..f4d9e3a --- /dev/null +++ b/mail/src/main/java/com/sun/mail/iap/ProtocolException.java
@@ -0,0 +1,73 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.iap; + +/** + * @author John Mani + */ + +public class ProtocolException extends Exception { + protected transient Response response = null; + + private static final long serialVersionUID = -4360500807971797439L; + + /** + * Constructs a ProtocolException with no detail message. + */ + public ProtocolException() { + super(); + } + + /** + * Constructs a ProtocolException with the specified detail message. + * + * @param message the detail message + */ + public ProtocolException(String message) { + super(message); + } + + /** + * Constructs a ProtocolException with the specified detail message + * and cause. + * + * @param message the detail message + * @param cause the cause + */ + public ProtocolException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Constructs a ProtocolException with the specified Response object. + * + * @param r the Response + */ + public ProtocolException(Response r) { + super(r.toString()); + response = r; + } + + /** + * Return the offending Response object. + * + * @return the Response object + */ + public Response getResponse() { + return response; + } +}
diff --git a/mail/src/main/java/com/sun/mail/iap/Response.java b/mail/src/main/java/com/sun/mail/iap/Response.java new file mode 100644 index 0000000..cbec9e4 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/iap/Response.java
@@ -0,0 +1,600 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.iap; + +import java.io.*; +import java.util.*; +import java.nio.charset.StandardCharsets; + +import com.sun.mail.util.ASCIIUtility; + +/** + * This class represents a response obtained from the input stream + * of an IMAP server. + * + * @author John Mani + * @author Bill Shannon + */ + +public class Response { + protected int index; // internal index (updated during the parse) + protected int pindex; // index after parse, for reset + protected int size; // number of valid bytes in our buffer + protected byte[] buffer = null; + protected int type = 0; + protected String tag = null; + /** @since JavaMail 1.5.4 */ + protected Exception ex; + protected boolean utf8; + + private static final int increment = 100; + + // The first and second bits indicate whether this response + // is a Continuation, Tagged or Untagged + public final static int TAG_MASK = 0x03; + public final static int CONTINUATION = 0x01; + public final static int TAGGED = 0x02; + public final static int UNTAGGED = 0x03; + + // The third, fourth and fifth bits indicate whether this response + // is an OK, NO, BAD or BYE response + public final static int TYPE_MASK = 0x1C; + public final static int OK = 0x04; + public final static int NO = 0x08; + public final static int BAD = 0x0C; + public final static int BYE = 0x10; + + // The sixth bit indicates whether a BYE response is synthetic or real + public final static int SYNTHETIC = 0x20; + + /** + * An ATOM is any CHAR delimited by: + * SPACE | CTL | '(' | ')' | '{' | '%' | '*' | '"' | '\' | ']' + * (CTL is handled in readDelimString.) + */ + private static String ATOM_CHAR_DELIM = " (){%*\"\\]"; + + /** + * An ASTRING_CHAR is any CHAR delimited by: + * SPACE | CTL | '(' | ')' | '{' | '%' | '*' | '"' | '\' + * (CTL is handled in readDelimString.) + */ + private static String ASTRING_CHAR_DELIM = " (){%*\"\\"; + + public Response(String s) { + this(s, true); + } + + /** + * Constructor for testing. + * + * @param s the response string + * @param supportsUtf8 allow UTF-8 in response? + * @since JavaMail 1.6.0 + */ + public Response(String s, boolean supportsUtf8) { + if (supportsUtf8) + buffer = s.getBytes(StandardCharsets.UTF_8); + else + buffer = s.getBytes(StandardCharsets.US_ASCII); + size = buffer.length; + utf8 = supportsUtf8; + parse(); + } + + /** + * Read a new Response from the given Protocol + * + * @param p the Protocol object + * @exception IOException for I/O errors + * @exception ProtocolException for protocol failures + */ + public Response(Protocol p) throws IOException, ProtocolException { + // read one response into 'buffer' + ByteArray ba = p.getResponseBuffer(); + ByteArray response = p.getInputStream().readResponse(ba); + buffer = response.getBytes(); + size = response.getCount() - 2; // Skip the terminating CRLF + utf8 = p.supportsUtf8(); + + parse(); + } + + /** + * Copy constructor. + * + * @param r the Response to copy + */ + public Response(Response r) { + index = r.index; + pindex = r.pindex; + size = r.size; + buffer = r.buffer; + type = r.type; + tag = r.tag; + ex = r.ex; + utf8 = r.utf8; + } + + /** + * Return a Response object that looks like a BYE protocol response. + * Include the details of the exception in the response string. + * + * @param ex the exception + * @return the synthetic Response object + */ + public static Response byeResponse(Exception ex) { + String err = "* BYE JavaMail Exception: " + ex.toString(); + err = err.replace('\r', ' ').replace('\n', ' '); + Response r = new Response(err); + r.type |= SYNTHETIC; + r.ex = ex; + return r; + } + + /** + * Does the server support UTF-8? + * + * @return true if the server supports UTF-8 + * @since JavaMail 1.6.0 + */ + public boolean supportsUtf8() { + return utf8; + } + + private void parse() { + index = 0; // position internal index at start + + if (size == 0) // empty line + return; + if (buffer[index] == '+') { // Continuation statement + type |= CONTINUATION; + index += 1; // Position beyond the '+' + return; // return + } else if (buffer[index] == '*') { // Untagged statement + type |= UNTAGGED; + index += 1; // Position beyond the '*' + } else { // Tagged statement + type |= TAGGED; + tag = readAtom(); // read the TAG, index positioned beyond tag + if (tag == null) + tag = ""; // avoid possible NPE + } + + int mark = index; // mark + String s = readAtom(); // updates index + if (s == null) + s = ""; // avoid possible NPE + if (s.equalsIgnoreCase("OK")) + type |= OK; + else if (s.equalsIgnoreCase("NO")) + type |= NO; + else if (s.equalsIgnoreCase("BAD")) + type |= BAD; + else if (s.equalsIgnoreCase("BYE")) + type |= BYE; + else + index = mark; // reset + + pindex = index; + return; + } + + public void skipSpaces() { + while (index < size && buffer[index] == ' ') + index++; + } + + /** + * Skip past any spaces. If the next non-space character is c, + * consume it and return true. Otherwise stop at that point + * and return false. + * + * @param c the character to look for + * @return true if the character is found + */ + public boolean isNextNonSpace(char c) { + skipSpaces(); + if (index < size && buffer[index] == (byte)c) { + index++; + return true; + } + return false; + } + + /** + * Skip to the next space, for use in error recovery while parsing. + */ + public void skipToken() { + while (index < size && buffer[index] != ' ') + index++; + } + + public void skip(int count) { + index += count; + } + + public byte peekByte() { + if (index < size) + return buffer[index]; + else + return 0; // XXX - how else to signal error? + } + + /** + * Return the next byte from this Statement. + * + * @return the next byte + */ + public byte readByte() { + if (index < size) + return buffer[index++]; + else + return 0; // XXX - how else to signal error? + } + + /** + * Extract an ATOM, starting at the current position. Updates + * the internal index to beyond the Atom. + * + * @return an Atom + */ + public String readAtom() { + return readDelimString(ATOM_CHAR_DELIM); + } + + /** + * Extract a string stopping at control characters or any + * character in delim. + */ + private String readDelimString(String delim) { + skipSpaces(); + + if (index >= size) // already at end of response + return null; + + int b; + int start = index; + while (index < size && ((b = (((int)buffer[index])&0xff)) >= ' ') && + delim.indexOf((char)b) < 0 && b != 0x7f) + index++; + + return toString(buffer, start, index); + } + + /** + * Read a string as an arbitrary sequence of characters, + * stopping at the delimiter Used to read part of a + * response code inside []. + * + * @param delim the delimiter character + * @return the string + */ + public String readString(char delim) { + skipSpaces(); + + if (index >= size) // already at end of response + return null; + + int start = index; + while (index < size && buffer[index] != delim) + index++; + + return toString(buffer, start, index); + } + + public String[] readStringList() { + return readStringList(false); + } + + public String[] readAtomStringList() { + return readStringList(true); + } + + private String[] readStringList(boolean atom) { + skipSpaces(); + + if (buffer[index] != '(') { // not what we expected + return null; + } + index++; // skip '(' + + // to handle buggy IMAP servers, we tolerate multiple spaces as + // well as spaces after the left paren or before the right paren + List<String> result = new ArrayList<>(); + while (!isNextNonSpace(')')) { + String s = atom ? readAtomString() : readString(); + if (s == null) // not the expected string or atom + break; + result.add(s); + } + + return result.toArray(new String[result.size()]); + } + + /** + * Extract an integer, starting at the current position. Updates the + * internal index to beyond the number. Returns -1 if a number was + * not found. + * + * @return a number + */ + public int readNumber() { + // Skip leading spaces + skipSpaces(); + + int start = index; + while (index < size && Character.isDigit((char)buffer[index])) + index++; + + if (index > start) { + try { + return ASCIIUtility.parseInt(buffer, start, index); + } catch (NumberFormatException nex) { } + } + + return -1; + } + + /** + * Extract a long number, starting at the current position. Updates the + * internal index to beyond the number. Returns -1 if a long number + * was not found. + * + * @return a long + */ + public long readLong() { + // Skip leading spaces + skipSpaces(); + + int start = index; + while (index < size && Character.isDigit((char)buffer[index])) + index++; + + if (index > start) { + try { + return ASCIIUtility.parseLong(buffer, start, index); + } catch (NumberFormatException nex) { } + } + + return -1; + } + + /** + * Extract a NSTRING, starting at the current position. Return it as + * a String. The sequence 'NIL' is returned as null + * + * NSTRING := QuotedString | Literal | "NIL" + * + * @return a String + */ + public String readString() { + return (String)parseString(false, true); + } + + /** + * Extract a NSTRING, starting at the current position. Return it as + * a ByteArrayInputStream. The sequence 'NIL' is returned as null + * + * NSTRING := QuotedString | Literal | "NIL" + * + * @return a ByteArrayInputStream + */ + public ByteArrayInputStream readBytes() { + ByteArray ba = readByteArray(); + if (ba != null) + return ba.toByteArrayInputStream(); + else + return null; + } + + /** + * Extract a NSTRING, starting at the current position. Return it as + * a ByteArray. The sequence 'NIL' is returned as null + * + * NSTRING := QuotedString | Literal | "NIL" + * + * @return a ByteArray + */ + public ByteArray readByteArray() { + /* + * Special case, return the data after the continuation uninterpreted. + * It's usually a challenge for an AUTHENTICATE command. + */ + if (isContinuation()) { + skipSpaces(); + return new ByteArray(buffer, index, size - index); + } + return (ByteArray)parseString(false, false); + } + + /** + * Extract an ASTRING, starting at the current position + * and return as a String. An ASTRING can be a QuotedString, a + * Literal or an Atom (plus ']'). + * + * Any errors in parsing returns null + * + * ASTRING := QuotedString | Literal | 1*ASTRING_CHAR + * + * @return a String + */ + public String readAtomString() { + return (String)parseString(true, true); + } + + /** + * Generic parsing routine that can parse out a Quoted-String, + * Literal or Atom and return the parsed token as a String + * or a ByteArray. Errors or NIL data will return null. + */ + private Object parseString(boolean parseAtoms, boolean returnString) { + byte b; + + // Skip leading spaces + skipSpaces(); + + b = buffer[index]; + if (b == '"') { // QuotedString + index++; // skip the quote + int start = index; + int copyto = index; + + while (index < size && (b = buffer[index]) != '"') { + if (b == '\\') // skip escaped byte + index++; + if (index != copyto) { // only copy if we need to + // Beware: this is a destructive copy. I'm + // pretty sure this is OK, but ... ;> + buffer[copyto] = buffer[index]; + } + copyto++; + index++; + } + if (index >= size) { + // didn't find terminating quote, something is seriously wrong + //throw new ArrayIndexOutOfBoundsException( + // "index = " + index + ", size = " + size); + return null; + } else + index++; // skip past the terminating quote + + if (returnString) + return toString(buffer, start, copyto); + else + return new ByteArray(buffer, start, copyto-start); + } else if (b == '{') { // Literal + int start = ++index; // note the start position + + while (buffer[index] != '}') + index++; + + int count = 0; + try { + count = ASCIIUtility.parseInt(buffer, start, index); + } catch (NumberFormatException nex) { + // throw new ParsingException(); + return null; + } + + start = index + 3; // skip "}\r\n" + index = start + count; // position index to beyond the literal + + if (returnString) // return as String + return toString(buffer, start, start + count); + else + return new ByteArray(buffer, start, count); + } else if (parseAtoms) { // parse as ASTRING-CHARs + int start = index; // track this, so that we can use to + // creating ByteArrayInputStream below. + String s = readDelimString(ASTRING_CHAR_DELIM); + if (returnString) + return s; + else // *very* unlikely + return new ByteArray(buffer, start, index); + } else if (b == 'N' || b == 'n') { // the only valid value is 'NIL' + index += 3; // skip past NIL + return null; + } + return null; // Error + } + + private String toString(byte[] buffer, int start, int end) { + return utf8 ? + new String(buffer, start, end - start, StandardCharsets.UTF_8) : + ASCIIUtility.toString(buffer, start, end); + } + + public int getType() { + return type; + } + + public boolean isContinuation() { + return ((type & TAG_MASK) == CONTINUATION); + } + + public boolean isTagged() { + return ((type & TAG_MASK) == TAGGED); + } + + public boolean isUnTagged() { + return ((type & TAG_MASK) == UNTAGGED); + } + + public boolean isOK() { + return ((type & TYPE_MASK) == OK); + } + + public boolean isNO() { + return ((type & TYPE_MASK) == NO); + } + + public boolean isBAD() { + return ((type & TYPE_MASK) == BAD); + } + + public boolean isBYE() { + return ((type & TYPE_MASK) == BYE); + } + + public boolean isSynthetic() { + return ((type & SYNTHETIC) == SYNTHETIC); + } + + /** + * Return the tag, if this is a tagged statement. + * + * @return tag of this tagged statement + */ + public String getTag() { + return tag; + } + + /** + * Return the rest of the response as a string, usually used to + * return the arbitrary message text after a NO response. + * + * @return the rest of the response + */ + public String getRest() { + skipSpaces(); + return toString(buffer, index, size); + } + + /** + * Return the exception for a synthetic BYE response. + * + * @return the exception + * @since JavaMail 1.5.4 + */ + public Exception getException() { + return ex; + } + + /** + * Reset pointer to beginning of response. + */ + public void reset() { + index = pindex; + } + + @Override + public String toString() { + return toString(buffer, 0, size); + } + +}
diff --git a/mail/src/main/java/com/sun/mail/iap/ResponseHandler.java b/mail/src/main/java/com/sun/mail/iap/ResponseHandler.java new file mode 100644 index 0000000..7d5a61c --- /dev/null +++ b/mail/src/main/java/com/sun/mail/iap/ResponseHandler.java
@@ -0,0 +1,27 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.iap; + +/** + * This class + * + * @author John Mani + */ + +public interface ResponseHandler { + public void handleResponse(Response r); +}
diff --git a/mail/src/main/java/com/sun/mail/iap/ResponseInputStream.java b/mail/src/main/java/com/sun/mail/iap/ResponseInputStream.java new file mode 100644 index 0000000..9e05b18 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/iap/ResponseInputStream.java
@@ -0,0 +1,158 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.iap; + +import java.io.*; +import com.sun.mail.iap.ByteArray; +import com.sun.mail.util.ASCIIUtility; + +/** + * + * Inputstream that is used to read a Response. + * + * @author Arun Krishnan + * @author Bill Shannon + */ + +public class ResponseInputStream { + + private static final int minIncrement = 256; + private static final int maxIncrement = 256 * 1024; + private static final int incrementSlop = 16; + + // where we read from + private BufferedInputStream bin; + + /** + * Constructor. + * + * @param in the InputStream to wrap + */ + public ResponseInputStream(InputStream in) { + bin = new BufferedInputStream(in, 2 * 1024); + } + + /** + * Read a Response from the InputStream. + * + * @return ByteArray that contains the Response + * @exception IOException for I/O errors + */ + public ByteArray readResponse() throws IOException { + return readResponse(null); + } + + /** + * Read a Response from the InputStream. + * + * @param ba the ByteArray in which to store the response, or null + * @return ByteArray that contains the Response + * @exception IOException for I/O errors + */ + public ByteArray readResponse(ByteArray ba) throws IOException { + if (ba == null) + ba = new ByteArray(new byte[128], 0, 128); + + byte[] buffer = ba.getBytes(); + int idx = 0; + for (;;) { // read until CRLF with no preceeding literal + // XXX - b needs to be an int, to handle bytes with value 0xff + int b = 0; + boolean gotCRLF=false; + + // Read a CRLF terminated line from the InputStream + while (!gotCRLF && + ((b = bin.read()) != -1)) { + if (b == '\n') { + if ((idx > 0) && buffer[idx-1] == '\r') + gotCRLF = true; + } + if (idx >= buffer.length) { + int incr = buffer.length; + if (incr > maxIncrement) + incr = maxIncrement; + ba.grow(incr); + buffer = ba.getBytes(); + } + buffer[idx++] = (byte)b; + } + + if (b == -1) + throw new IOException("Connection dropped by server?"); + + // Now lets check for literals : {<digits>}CRLF + // Note: index needs to >= 5 for the above sequence to occur + if (idx < 5 || buffer[idx-3] != '}') + break; + + int i; + // look for left curly + for (i = idx - 4; i >= 0; i--) + if (buffer[i] == '{') + break; + + if (i < 0) // Nope, not a literal ? + break; + + int count = 0; + // OK, handle the literal .. + try { + count = ASCIIUtility.parseInt(buffer, i+1, idx-3); + } catch (NumberFormatException e) { + break; + } + + // Now read 'count' bytes. (Note: count could be 0) + if (count > 0) { + int avail = buffer.length - idx; // available space in buffer + if (count + incrementSlop > avail) { + // need count-avail more bytes + ba.grow(minIncrement > count + incrementSlop - avail ? + minIncrement : count + incrementSlop - avail); + buffer = ba.getBytes(); + } + + /* + * read() might not return all the bytes in one shot, + * so call repeatedly till we are done + */ + int actual; + while (count > 0) { + actual = bin.read(buffer, idx, count); + if (actual == -1) + throw new IOException("Connection dropped by server?"); + count -= actual; + idx += actual; + } + } + // back to top of loop to read until CRLF + } + ba.setCount(idx); + return ba; + } + + /** + * How much buffered data do we have? + * + * @return number of bytes available + * @exception IOException if the stream has been closed + * @since JavaMail 1.5.4 + */ + public int available() throws IOException { + return bin.available(); + } +}
diff --git a/mail/src/main/java/com/sun/mail/iap/package.html b/mail/src/main/java/com/sun/mail/iap/package.html new file mode 100644 index 0000000..2607e33 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/iap/package.html
@@ -0,0 +1,33 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> +<HTML> +<HEAD> +<meta http-equiv="Content-Type" content="text/html; charset=utf-8"> +<!-- + + Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<TITLE>com.sun.mail.iap package</TITLE> +</HEAD> +<BODY BGCOLOR="white"> + +<P> +This package includes internal IMAP support classes and +<strong>SHOULD NOT BE USED DIRECTLY BY APPLICATIONS</strong>. +</P> + +</BODY> +</HTML>
diff --git a/mail/src/main/java/com/sun/mail/imap/ACL.java b/mail/src/main/java/com/sun/mail/imap/ACL.java new file mode 100644 index 0000000..ee99c95 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/imap/ACL.java
@@ -0,0 +1,94 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.util.*; + +/** + * An access control list entry for a particular authentication identifier + * (user or group). Associates a set of Rights with the identifier. + * See RFC 2086. + * <p> + * + * @author Bill Shannon + */ + +public class ACL implements Cloneable { + + private String name; + private Rights rights; + + /** + * Construct an ACL entry for the given identifier and with no rights. + * + * @param name the identifier name + */ + public ACL(String name) { + this.name = name; + this.rights = new Rights(); + } + + /** + * Construct an ACL entry for the given identifier with the given rights. + * + * @param name the identifier name + * @param rights the rights + */ + public ACL(String name, Rights rights) { + this.name = name; + this.rights = rights; + } + + /** + * Get the identifier name for this ACL entry. + * + * @return the identifier name + */ + public String getName() { + return name; + } + + /** + * Set the rights associated with this ACL entry. + * + * @param rights the rights + */ + public void setRights(Rights rights) { + this.rights = rights; + } + + /** + * Get the rights associated with this ACL entry. + * Returns the actual Rights object referenced by this ACL; + * modifications to the Rights object will effect this ACL. + * + * @return the rights + */ + public Rights getRights() { + return rights; + } + + /** + * Clone this ACL entry. + */ + @Override + public Object clone() throws CloneNotSupportedException { + ACL acl = (ACL)super.clone(); + acl.rights = (Rights)this.rights.clone(); + return acl; + } +}
diff --git a/mail/src/main/java/com/sun/mail/imap/AppendUID.java b/mail/src/main/java/com/sun/mail/imap/AppendUID.java new file mode 100644 index 0000000..56c77fe --- /dev/null +++ b/mail/src/main/java/com/sun/mail/imap/AppendUID.java
@@ -0,0 +1,37 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import com.sun.mail.iap.*; + +/** + * Information from the APPENDUID response code + * defined by the UIDPLUS extension - + * <A HREF="http://www.ietf.org/rfc/rfc4315.txt">RFC 4315</A>. + * + * @author Bill Shannon + */ + +public class AppendUID { + public long uidvalidity = -1; + public long uid = -1; + + public AppendUID(long uidvalidity, long uid) { + this.uidvalidity = uidvalidity; + this.uid = uid; + } +}
diff --git a/mail/src/main/java/com/sun/mail/imap/CopyUID.java b/mail/src/main/java/com/sun/mail/imap/CopyUID.java new file mode 100644 index 0000000..3648fc5 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/imap/CopyUID.java
@@ -0,0 +1,39 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import com.sun.mail.imap.protocol.UIDSet; + +/** + * Information from the COPYUID response code + * defined by the UIDPLUS extension - + * <A HREF="http://www.ietf.org/rfc/rfc4315.txt">RFC 4315</A>. + * + * @author Bill Shannon + */ + +public class CopyUID { + public long uidvalidity = -1; + public UIDSet[] src; + public UIDSet[] dst; + + public CopyUID(long uidvalidity, UIDSet[] src, UIDSet[] dst) { + this.uidvalidity = uidvalidity; + this.src = src; + this.dst = dst; + } +}
diff --git a/mail/src/main/java/com/sun/mail/imap/DefaultFolder.java b/mail/src/main/java/com/sun/mail/imap/DefaultFolder.java new file mode 100644 index 0000000..9dab04f --- /dev/null +++ b/mail/src/main/java/com/sun/mail/imap/DefaultFolder.java
@@ -0,0 +1,127 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import javax.mail.Folder; +import javax.mail.Message; +import javax.mail.MessagingException; +import javax.mail.MethodNotSupportedException; +import com.sun.mail.iap.ProtocolException; +import com.sun.mail.imap.protocol.IMAPProtocol; +import com.sun.mail.imap.protocol.ListInfo; + +/** + * The default IMAP folder (root of the naming hierarchy). + * + * @author John Mani + */ + +public class DefaultFolder extends IMAPFolder { + + protected DefaultFolder(IMAPStore store) { + super("", UNKNOWN_SEPARATOR, store, null); + exists = true; // of course + type = HOLDS_FOLDERS; // obviously + } + + @Override + public synchronized String getName() { + return fullName; + } + + @Override + public Folder getParent() { + return null; + } + + @Override + public synchronized Folder[] list(final String pattern) + throws MessagingException { + ListInfo[] li = null; + + li = (ListInfo[])doCommand(new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) throws ProtocolException { + return p.list("", pattern); + } + }); + + if (li == null) + return new Folder[0]; + + IMAPFolder[] folders = new IMAPFolder[li.length]; + for (int i = 0; i < folders.length; i++) + folders[i] = ((IMAPStore)store).newIMAPFolder(li[i]); + return folders; + } + + @Override + public synchronized Folder[] listSubscribed(final String pattern) + throws MessagingException { + ListInfo[] li = null; + + li = (ListInfo[])doCommand(new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) throws ProtocolException { + return p.lsub("", pattern); + } + }); + + if (li == null) + return new Folder[0]; + + IMAPFolder[] folders = new IMAPFolder[li.length]; + for (int i = 0; i < folders.length; i++) + folders[i] = ((IMAPStore)store).newIMAPFolder(li[i]); + return folders; + } + + @Override + public boolean hasNewMessages() throws MessagingException { + // Not applicable on DefaultFolder + return false; + } + + @Override + public Folder getFolder(String name) throws MessagingException { + return ((IMAPStore)store).newIMAPFolder(name, UNKNOWN_SEPARATOR); + } + + @Override + public boolean delete(boolean recurse) throws MessagingException { + // Not applicable on DefaultFolder + throw new MethodNotSupportedException("Cannot delete Default Folder"); + } + + @Override + public boolean renameTo(Folder f) throws MessagingException { + // Not applicable on DefaultFolder + throw new MethodNotSupportedException("Cannot rename Default Folder"); + } + + @Override + public void appendMessages(Message[] msgs) throws MessagingException { + // Not applicable on DefaultFolder + throw new MethodNotSupportedException("Cannot append to Default Folder"); + } + + @Override + public Message[] expunge() throws MessagingException { + // Not applicable on DefaultFolder + throw new MethodNotSupportedException("Cannot expunge Default Folder"); + } +}
diff --git a/mail/src/main/java/com/sun/mail/imap/IMAPBodyPart.java b/mail/src/main/java/com/sun/mail/imap/IMAPBodyPart.java new file mode 100644 index 0000000..181bb94 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/imap/IMAPBodyPart.java
@@ -0,0 +1,454 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.io.*; + +import java.util.Enumeration; +import javax.mail.*; +import javax.mail.internet.*; +import javax.activation.*; + +import com.sun.mail.util.PropUtil; +import com.sun.mail.util.ReadableMime; +import com.sun.mail.util.LineOutputStream; +import com.sun.mail.util.SharedByteArrayOutputStream; +import com.sun.mail.iap.*; +import com.sun.mail.imap.protocol.*; + +/** + * An IMAP body part. + * + * @author John Mani + * @author Bill Shannon + */ + +public class IMAPBodyPart extends MimeBodyPart implements ReadableMime { + private IMAPMessage message; + private BODYSTRUCTURE bs; + private String sectionId; + + // processed values .. + private String type; + private String description; + + private boolean headersLoaded = false; + + private static final boolean decodeFileName = + PropUtil.getBooleanSystemProperty("mail.mime.decodefilename", false); + + protected IMAPBodyPart(BODYSTRUCTURE bs, String sid, IMAPMessage message) { + super(); + this.bs = bs; + this.sectionId = sid; + this.message = message; + // generate content-type + ContentType ct = new ContentType(bs.type, bs.subtype, bs.cParams); + type = ct.toString(); + } + + /* Override this method to make it a no-op, rather than throw + * an IllegalWriteException. This will permit IMAPBodyParts to + * be inserted in newly crafted MimeMessages, especially when + * forwarding or replying to messages. + */ + @Override + protected void updateHeaders() { + return; + } + + @Override + public int getSize() throws MessagingException { + return bs.size; + } + + @Override + public int getLineCount() throws MessagingException { + return bs.lines; + } + + @Override + public String getContentType() throws MessagingException { + return type; + } + + @Override + public String getDisposition() throws MessagingException { + return bs.disposition; + } + + @Override + public void setDisposition(String disposition) throws MessagingException { + throw new IllegalWriteException("IMAPBodyPart is read-only"); + } + + @Override + public String getEncoding() throws MessagingException { + return bs.encoding; + } + + @Override + public String getContentID() throws MessagingException { + return bs.id; + } + + @Override + public String getContentMD5() throws MessagingException { + return bs.md5; + } + + @Override + public void setContentMD5(String md5) throws MessagingException { + throw new IllegalWriteException("IMAPBodyPart is read-only"); + } + + @Override + public String getDescription() throws MessagingException { + if (description != null) // cached value ? + return description; + + if (bs.description == null) + return null; + + try { + description = MimeUtility.decodeText(bs.description); + } catch (UnsupportedEncodingException ex) { + description = bs.description; + } + + return description; + } + + @Override + public void setDescription(String description, String charset) + throws MessagingException { + throw new IllegalWriteException("IMAPBodyPart is read-only"); + } + + @Override + public String getFileName() throws MessagingException { + String filename = null; + if (bs.dParams != null) + filename = bs.dParams.get("filename"); + if ((filename == null || filename.isEmpty()) && bs.cParams != null) + filename = bs.cParams.get("name"); + if (decodeFileName && filename != null) { + try { + filename = MimeUtility.decodeText(filename); + } catch (UnsupportedEncodingException ex) { + throw new MessagingException("Can't decode filename", ex); + } + } + return filename; + } + + @Override + public void setFileName(String filename) throws MessagingException { + throw new IllegalWriteException("IMAPBodyPart is read-only"); + } + + @Override + protected InputStream getContentStream() throws MessagingException { + InputStream is = null; + boolean pk = message.getPeek(); // acquire outside of message cache lock + + // Acquire MessageCacheLock, to freeze seqnum. + synchronized(message.getMessageCacheLock()) { + try { + IMAPProtocol p = message.getProtocol(); + + // Check whether this message is expunged + message.checkExpunged(); + + if (p.isREV1() && (message.getFetchBlockSize() != -1)) + return new IMAPInputStream(message, sectionId, + message.ignoreBodyStructureSize() ? -1 : bs.size, pk); + + // Else, vanila IMAP4, no partial fetch + + int seqnum = message.getSequenceNumber(); + BODY b; + if (pk) + b = p.peekBody(seqnum, sectionId); + else + b = p.fetchBody(seqnum, sectionId); + if (b != null) + is = b.getByteArrayInputStream(); + } catch (ConnectionException cex) { + throw new FolderClosedException( + message.getFolder(), cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + } + + if (is == null) { + message.forceCheckExpunged(); // may throw MessageRemovedException + // nope, the server doesn't think it's expunged. + // can't tell the difference between the server returning NIL + // and some other error that caused null to be returned above, + // so we'll just assume it was empty content. + is = new ByteArrayInputStream(new byte[0]); + } + return is; + } + + /** + * Return the MIME format stream of headers for this body part. + */ + private InputStream getHeaderStream() throws MessagingException { + if (!message.isREV1()) + loadHeaders(); // will be needed below + + // Acquire MessageCacheLock, to freeze seqnum. + synchronized(message.getMessageCacheLock()) { + try { + IMAPProtocol p = message.getProtocol(); + + // Check whether this message got expunged + message.checkExpunged(); + + if (p.isREV1()) { + int seqnum = message.getSequenceNumber(); + BODY b = p.peekBody(seqnum, sectionId + ".MIME"); + + if (b == null) + throw new MessagingException("Failed to fetch headers"); + + ByteArrayInputStream bis = b.getByteArrayInputStream(); + if (bis == null) + throw new MessagingException("Failed to fetch headers"); + return bis; + + } else { + // Can't read it from server, have to fake it + SharedByteArrayOutputStream bos = + new SharedByteArrayOutputStream(0); + LineOutputStream los = new LineOutputStream(bos); + + try { + // Write out the header + Enumeration<String> hdrLines + = super.getAllHeaderLines(); + while (hdrLines.hasMoreElements()) + los.writeln(hdrLines.nextElement()); + + // The CRLF separator between header and content + los.writeln(); + } catch (IOException ioex) { + // should never happen + } finally { + try { + los.close(); + } catch (IOException cex) { } + } + return bos.toStream(); + } + } catch (ConnectionException cex) { + throw new FolderClosedException( + message.getFolder(), cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + } + } + + /** + * Return the MIME format stream corresponding to this message part. + * + * @return the MIME format stream + * @since JavaMail 1.4.5 + */ + @Override + public InputStream getMimeStream() throws MessagingException { + /* + * The IMAP protocol doesn't support returning the entire + * part content in one operation so we have to fake it by + * concatenating the header stream and the content stream. + */ + return new SequenceInputStream(getHeaderStream(), getContentStream()); + } + + @Override + public synchronized DataHandler getDataHandler() + throws MessagingException { + if (dh == null) { + if (bs.isMulti()) + dh = new DataHandler( + new IMAPMultipartDataSource( + this, bs.bodies, sectionId, message) + ); + else if (bs.isNested() && message.isREV1() && bs.envelope != null) + dh = new DataHandler( + new IMAPNestedMessage(message, + bs.bodies[0], + bs.envelope, + sectionId), + type + ); + } + + return super.getDataHandler(); + } + + @Override + public void setDataHandler(DataHandler content) throws MessagingException { + throw new IllegalWriteException("IMAPBodyPart is read-only"); + } + + @Override + public void setContent(Object o, String type) throws MessagingException { + throw new IllegalWriteException("IMAPBodyPart is read-only"); + } + + @Override + public void setContent(Multipart mp) throws MessagingException { + throw new IllegalWriteException("IMAPBodyPart is read-only"); + } + + @Override + public String[] getHeader(String name) throws MessagingException { + loadHeaders(); + return super.getHeader(name); + } + + @Override + public void setHeader(String name, String value) + throws MessagingException { + throw new IllegalWriteException("IMAPBodyPart is read-only"); + } + + @Override + public void addHeader(String name, String value) + throws MessagingException { + throw new IllegalWriteException("IMAPBodyPart is read-only"); + } + + @Override + public void removeHeader(String name) throws MessagingException { + throw new IllegalWriteException("IMAPBodyPart is read-only"); + } + + @Override + public Enumeration<Header> getAllHeaders() throws MessagingException { + loadHeaders(); + return super.getAllHeaders(); + } + + @Override + public Enumeration<Header> getMatchingHeaders(String[] names) + throws MessagingException { + loadHeaders(); + return super.getMatchingHeaders(names); + } + + @Override + public Enumeration<Header> getNonMatchingHeaders(String[] names) + throws MessagingException { + loadHeaders(); + return super.getNonMatchingHeaders(names); + } + + @Override + public void addHeaderLine(String line) throws MessagingException { + throw new IllegalWriteException("IMAPBodyPart is read-only"); + } + + @Override + public Enumeration<String> getAllHeaderLines() throws MessagingException { + loadHeaders(); + return super.getAllHeaderLines(); + } + + @Override + public Enumeration<String> getMatchingHeaderLines(String[] names) + throws MessagingException { + loadHeaders(); + return super.getMatchingHeaderLines(names); + } + + @Override + public Enumeration<String> getNonMatchingHeaderLines(String[] names) + throws MessagingException { + loadHeaders(); + return super.getNonMatchingHeaderLines(names); + } + + private synchronized void loadHeaders() throws MessagingException { + if (headersLoaded) + return; + + // "headers" should never be null since it's set in the constructor. + // If something did go wrong this will fix it, but is an unsynchronized + // assignment of "headers". + if (headers == null) + headers = new InternetHeaders(); + + // load headers + + // Acquire MessageCacheLock, to freeze seqnum. + synchronized(message.getMessageCacheLock()) { + try { + IMAPProtocol p = message.getProtocol(); + + // Check whether this message got expunged + message.checkExpunged(); + + if (p.isREV1()) { + int seqnum = message.getSequenceNumber(); + BODY b = p.peekBody(seqnum, sectionId + ".MIME"); + + if (b == null) + throw new MessagingException("Failed to fetch headers"); + + ByteArrayInputStream bis = b.getByteArrayInputStream(); + if (bis == null) + throw new MessagingException("Failed to fetch headers"); + + headers.load(bis); + + } else { + + // RFC 1730 does not provide for fetching BodyPart headers + // So, just dump the RFC1730 BODYSTRUCTURE into the + // headerStore + + // Content-Type + headers.addHeader("Content-Type", type); + // Content-Transfer-Encoding + headers.addHeader("Content-Transfer-Encoding", bs.encoding); + // Content-Description + if (bs.description != null) + headers.addHeader("Content-Description", + bs.description); + // Content-ID + if (bs.id != null) + headers.addHeader("Content-ID", bs.id); + // Content-MD5 + if (bs.md5 != null) + headers.addHeader("Content-MD5", bs.md5); + } + } catch (ConnectionException cex) { + throw new FolderClosedException( + message.getFolder(), cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + } + headersLoaded = true; + } +}
diff --git a/mail/src/main/java/com/sun/mail/imap/IMAPFolder.java b/mail/src/main/java/com/sun/mail/imap/IMAPFolder.java new file mode 100644 index 0000000..74f2f56 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/imap/IMAPFolder.java
@@ -0,0 +1,4153 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.util.Date; +import java.util.Vector; +import java.util.Hashtable; +import java.util.List; +import java.util.ArrayList; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Locale; +import java.util.logging.Level; +import java.io.*; +import java.net.SocketTimeoutException; +import java.nio.channels.SocketChannel; + +import javax.mail.*; +import javax.mail.event.*; +import javax.mail.internet.*; +import javax.mail.search.*; + +import com.sun.mail.util.PropUtil; +import com.sun.mail.util.MailLogger; +import com.sun.mail.util.CRLFOutputStream; +import com.sun.mail.iap.*; +import com.sun.mail.imap.protocol.*; + +/** + * This class implements an IMAP folder. <p> + * + * A closed IMAPFolder object shares a protocol connection with its IMAPStore + * object. When the folder is opened, it gets its own protocol connection. <p> + * + * Applications that need to make use of IMAP-specific features may cast + * a <code>Folder</code> object to an <code>IMAPFolder</code> object and + * use the methods on this class. <p> + * + * The {@link #getQuota getQuota} and + * {@link #setQuota setQuota} methods support the IMAP QUOTA extension. + * Refer to <A HREF="http://www.ietf.org/rfc/rfc2087.txt">RFC 2087</A> + * for more information. <p> + * + * The {@link #getACL getACL}, {@link #addACL addACL}, + * {@link #removeACL removeACL}, {@link #addRights addRights}, + * {@link #removeRights removeRights}, {@link #listRights listRights}, and + * {@link #myRights myRights} methods support the IMAP ACL extension. + * Refer to <A HREF="http://www.ietf.org/rfc/rfc2086.txt">RFC 2086</A> + * for more information. <p> + * + * The {@link #getSortedMessages getSortedMessages} + * methods support the IMAP SORT extension. + * Refer to <A HREF="http://www.ietf.org/rfc/rfc5256.txt">RFC 5256</A> + * for more information. <p> + * + * The {@link #open(int,com.sun.mail.imap.ResyncData) open(int,ResyncData)} + * method and {@link com.sun.mail.imap.ResyncData ResyncData} class supports + * the IMAP CONDSTORE and QRESYNC extensions. + * Refer to <A HREF="http://www.ietf.org/rfc/rfc4551.txt">RFC 4551</A> + * and <A HREF="http://www.ietf.org/rfc/rfc5162.txt">RFC 5162</A> + * for more information. <p> + * + * The {@link #doCommand doCommand} method and + * {@link IMAPFolder.ProtocolCommand IMAPFolder.ProtocolCommand} + * interface support use of arbitrary IMAP protocol commands. <p> + * + * See the <a href="package-summary.html">com.sun.mail.imap</a> package + * documentation for further information on the IMAP protocol provider. <p> + * + * <strong>WARNING:</strong> The APIs unique to this class should be + * considered <strong>EXPERIMENTAL</strong>. They may be changed in the + * future in ways that are incompatible with applications using the + * current APIs. + * + * @author John Mani + * @author Bill Shannon + * @author Jim Glennon + */ + +/* + * The folder object itself serves as a lock for the folder's state + * EXCEPT for the message cache (see below), typically by using + * synchronized methods. When checking that a folder is open or + * closed, the folder's lock must be held. It's important that the + * folder's lock is acquired before the messageCacheLock (see below). + * Thus, the locking hierarchy is that the folder lock, while optional, + * must be acquired before the messageCacheLock, if it's acquired at + * all. Be especially careful of callbacks that occur while holding + * the messageCacheLock into (e.g.) superclass Folder methods that are + * synchronized. Note that methods in IMAPMessage will acquire the + * messageCacheLock without acquiring the folder lock. <p> + * + * When a folder is opened, it creates a messageCache (a Vector) of + * empty IMAPMessage objects. Each Message has a messageNumber - which + * is its index into the messageCache, and a sequenceNumber - which is + * its IMAP sequence-number. All operations on a Message which involve + * communication with the server, use the message's sequenceNumber. <p> + * + * The most important thing to note here is that the server can send + * unsolicited EXPUNGE notifications as part of the responses for "most" + * commands. Refer RFC 3501, sections 5.3 & 5.5 for gory details. Also, + * the server sends these notifications AFTER the message has been + * expunged. And once a message is expunged, the sequence-numbers of + * those messages after the expunged one are renumbered. This essentially + * means that the mapping between *any* Message and its sequence-number + * can change in the period when a IMAP command is issued and its responses + * are processed. Hence we impose a strict locking model as follows: <p> + * + * We define one mutex per folder - this is just a Java Object (named + * messageCacheLock). Any time a command is to be issued to the IMAP + * server (i.e., anytime the corresponding IMAPProtocol method is + * invoked), follow the below style: + * + * synchronized (messageCacheLock) { // ACQUIRE LOCK + * issue command () + * + * // The response processing is typically done within + * // the handleResponse() callback. A few commands (Fetch, + * // Expunge) return *all* responses and hence their + * // processing is done here itself. Now, as part of the + * // processing unsolicited EXPUNGE responses, we renumber + * // the necessary sequence-numbers. Thus the renumbering + * // happens within this critical-region, surrounded by + * // locks. + * process responses () + * } // RELEASE LOCK + * + * This technique is used both by methods in IMAPFolder and by methods + * in IMAPMessage and other classes that operate on data in the folder. + * Note that holding the messageCacheLock has the side effect of + * preventing the folder from being closed, and thus ensuring that the + * folder's protocol object is still valid. The protocol object should + * only be accessed while holding the messageCacheLock (except for calls + * to IMAPProtocol.isREV1(), which don't need to be protected because it + * doesn't access the server). + * + * Note that interactions with the Store's protocol connection do + * not have to be protected as above, since the Store's protocol is + * never in a "meaningful" SELECT-ed state. + */ + +public class IMAPFolder extends Folder implements UIDFolder, ResponseHandler { + + protected volatile String fullName; // full name + protected String name; // name + protected int type; // folder type. + protected char separator; // separator + protected Flags availableFlags; // available flags + protected Flags permanentFlags; // permanent flags + protected volatile boolean exists; // whether this folder really exists ? + protected boolean isNamespace = false; // folder is a namespace name + protected volatile String[] attributes;// name attributes from LIST response + + protected volatile IMAPProtocol protocol; // this folder's protocol object + protected MessageCache messageCache;// message cache + // accessor lock for message cache + protected final Object messageCacheLock = new Object(); + + protected Hashtable<Long, IMAPMessage> uidTable; // UID->Message hashtable + + /* An IMAP delimiter is a 7bit US-ASCII character. (except NUL). + * We use '\uffff' (a non 7bit character) to indicate that we havent + * yet determined what the separator character is. + * We use '\u0000' (NUL) to indicate that no separator character + * exists, i.e., a flat hierarchy + */ + static final protected char UNKNOWN_SEPARATOR = '\uffff'; + + private volatile boolean opened = false; // is this folder opened ? + + /* This field tracks the state of this folder. If the folder is closed + * due to external causes (i.e, not thru the close() method), then + * this field will remain false. If the folder is closed thru the + * close() method, then this field is set to true. + * + * If reallyClosed is false, then a FolderClosedException is + * generated when a method is invoked on any Messaging object + * owned by this folder. If reallyClosed is true, then the + * IllegalStateException runtime exception is thrown. + */ + private boolean reallyClosed = true; + + /* + * The idleState field supports the IDLE command. + * Normally when executing an IMAP command we hold the + * messageCacheLock and often the folder lock (see above). + * While executing the IDLE command we can't hold either + * of these locks or it would prevent other threads from + * entering Folder methods even far enough to check whether + * an IDLE command is in progress. We need to check before + * issuing another command so that we can abort the IDLE + * command. + * + * The idleState field is protected by the messageCacheLock. + * The RUNNING state is the normal state and means no IDLE + * command is in progress. The IDLE state means we've issued + * an IDLE command and are reading responses. The ABORTING + * state means we've sent the DONE continuation command and + * are waiting for the thread running the IDLE command to + * break out of its read loop. + * + * When an IDLE command is in progress, the thread calling + * the idle method will be reading from the IMAP connection + * while holding neither the folder lock nor the messageCacheLock. + * It's obviously critical that no other thread try to send a + * command or read from the connection while in this state. + * However, other threads can send the DONE continuation + * command that will cause the server to break out of the IDLE + * loop and send the ending tag response to the IDLE command. + * The thread in the idle method that's reading the responses + * from the IDLE command will see this ending response and + * complete the idle method, setting the idleState field back + * to RUNNING, and notifying any threads waiting to use the + * connection. + * + * All uses of the IMAP connection (IMAPProtocol object) must + * be done while holding the messageCacheLock and must be + * preceeded by a check to make sure an IDLE command is not + * running, and abort the IDLE command if necessary. While + * waiting for the IDLE command to complete, these other threads + * will give up the messageCacheLock, but might still be holding + * the folder lock. This check is done by the getProtocol() + * method, resulting in a typical usage pattern of: + * + * synchronized (messageCacheLock) { + * IMAPProtocol p = getProtocol(); // may block waiting for IDLE + * // ... use protocol + * } + */ + private static final int RUNNING = 0; // not doing IDLE command + private static final int IDLE = 1; // IDLE command in effect + private static final int ABORTING = 2; // IDLE command aborting + private int idleState = RUNNING; + private IdleManager idleManager; + + private volatile int total = -1; // total number of messages in the + // message cache + private volatile int recent = -1; // number of recent messages + private int realTotal = -1; // total number of messages on + // the server + private long uidvalidity = -1; // UIDValidity + private long uidnext = -1; // UIDNext + private boolean uidNotSticky = false; // RFC 4315 + private volatile long highestmodseq = -1; // RFC 4551 - CONDSTORE + private boolean doExpungeNotification = true; // used in expunge handler + + private Status cachedStatus = null; + private long cachedStatusTime = 0; + + private boolean hasMessageCountListener = false; // optimize notification + + protected MailLogger logger; + private MailLogger connectionPoolLogger; + + /** + * A fetch profile item for fetching headers. + * This inner class extends the <code>FetchProfile.Item</code> + * class to add new FetchProfile item types, specific to IMAPFolders. + * + * @see FetchProfile + */ + public static class FetchProfileItem extends FetchProfile.Item { + protected FetchProfileItem(String name) { + super(name); + } + + /** + * HEADERS is a fetch profile item that can be included in a + * <code>FetchProfile</code> during a fetch request to a Folder. + * This item indicates that the headers for messages in the specified + * range are desired to be prefetched. <p> + * + * An example of how a client uses this is below: + * <blockquote><pre> + * + * FetchProfile fp = new FetchProfile(); + * fp.add(IMAPFolder.FetchProfileItem.HEADERS); + * folder.fetch(msgs, fp); + * + * </pre></blockquote> + */ + public static final FetchProfileItem HEADERS = + new FetchProfileItem("HEADERS"); + + /** + * SIZE is a fetch profile item that can be included in a + * <code>FetchProfile</code> during a fetch request to a Folder. + * This item indicates that the sizes of the messages in the specified + * range are desired to be prefetched. <p> + * + * SIZE was moved to FetchProfile.Item in JavaMail 1.5. + * + * @deprecated + */ + @Deprecated + public static final FetchProfileItem SIZE = + new FetchProfileItem("SIZE"); + + /** + * MESSAGE is a fetch profile item that can be included in a + * <code>FetchProfile</code> during a fetch request to a Folder. + * This item indicates that the entire messages (headers and body, + * including all "attachments") in the specified + * range are desired to be prefetched. Note that the entire message + * content is cached in memory while the Folder is open. The cached + * message will be parsed locally to return header information and + * message content. <p> + * + * An example of how a client uses this is below: + * <blockquote><pre> + * + * FetchProfile fp = new FetchProfile(); + * fp.add(IMAPFolder.FetchProfileItem.MESSAGE); + * folder.fetch(msgs, fp); + * + * </pre></blockquote> + * + * @since JavaMail 1.5.2 + */ + public static final FetchProfileItem MESSAGE = + new FetchProfileItem("MESSAGE"); + + /** + * INTERNALDATE is a fetch profile item that can be included in a + * <code>FetchProfile</code> during a fetch request to a Folder. + * This item indicates that the IMAP INTERNALDATE values + * (received date) of the messages in the specified + * range are desired to be prefetched. <p> + * + * An example of how a client uses this is below: + * <blockquote><pre> + * + * FetchProfile fp = new FetchProfile(); + * fp.add(IMAPFolder.FetchProfileItem.INTERNALDATE); + * folder.fetch(msgs, fp); + * + * </pre></blockquote> + * + * @since JavaMail 1.5.5 + */ + public static final FetchProfileItem INTERNALDATE = + new FetchProfileItem("INTERNALDATE"); + } + + /** + * Constructor used to create a possibly non-existent folder. + * + * @param fullName fullname of this folder + * @param separator the default separator character for this + * folder's namespace + * @param store the Store + * @param isNamespace if this folder represents a namespace + */ + protected IMAPFolder(String fullName, char separator, IMAPStore store, + Boolean isNamespace) { + super(store); + if (fullName == null) + throw new NullPointerException("Folder name is null"); + this.fullName = fullName; + this.separator = separator; + logger = new MailLogger(this.getClass(), "DEBUG IMAP", + store.getSession().getDebug(), store.getSession().getDebugOut()); + connectionPoolLogger = store.getConnectionPoolLogger(); + + /* + * Work around apparent bug in Exchange. Exchange + * will return a name of "Public Folders/" from + * LIST "%". + * + * If name has one separator, and it's at the end, + * assume this is a namespace name and treat it + * accordingly. Usually this will happen as a result + * of the list method, but this also allows getFolder + * to work with namespace names. + */ + this.isNamespace = false; + if (separator != UNKNOWN_SEPARATOR && separator != '\0') { + int i = this.fullName.indexOf(separator); + if (i > 0 && i == this.fullName.length() - 1) { + this.fullName = this.fullName.substring(0, i); + this.isNamespace = true; + } + } + + // if we were given a value, override default chosen above + if (isNamespace != null) + this.isNamespace = isNamespace.booleanValue(); + } + + /** + * Constructor used to create an existing folder. + * + * @param li the ListInfo for this folder + * @param store the store containing this folder + */ + protected IMAPFolder(ListInfo li, IMAPStore store) { + this(li.name, li.separator, store, null); + + if (li.hasInferiors) + type |= HOLDS_FOLDERS; + if (li.canOpen) + type |= HOLDS_MESSAGES; + exists = true; + attributes = li.attrs; + } + + /* + * Ensure that this folder exists. If 'exists' has been set to true, + * we don't attempt to validate it with the server again. Note that + * this can result in a possible loss of sync with the server. + * ASSERT: Must be called with this folder's synchronization lock held. + */ + protected void checkExists() throws MessagingException { + // If the boolean field 'exists' is false, check with the + // server by invoking exists() .. + if (!exists && !exists()) + throw new FolderNotFoundException( + this, fullName + " not found"); + } + + /* + * Ensure the folder is closed. + * ASSERT: Must be called with this folder's synchronization lock held. + */ + protected void checkClosed() { + if (opened) + throw new IllegalStateException( + "This operation is not allowed on an open folder" + ); + } + + /* + * Ensure the folder is open. + * ASSERT: Must be called with this folder's synchronization lock held. + */ + protected void checkOpened() throws FolderClosedException { + assert Thread.holdsLock(this); + if (!opened) { + if (reallyClosed) + throw new IllegalStateException( + "This operation is not allowed on a closed folder" + ); + else // Folder was closed "implicitly" + throw new FolderClosedException(this, + "Lost folder connection to server" + ); + } + } + + /* + * Check that the given message number is within the range + * of messages present in this folder. If the message + * number is out of range, we ping the server to obtain any + * pending new message notifications from the server. + */ + protected void checkRange(int msgno) throws MessagingException { + if (msgno < 1) // message-numbers start at 1 + throw new IndexOutOfBoundsException("message number < 1"); + + if (msgno <= total) + return; + + // Out of range, let's ping the server and see if + // the server has more messages for us. + + synchronized(messageCacheLock) { // Acquire lock + try { + keepConnectionAlive(false); + } catch (ConnectionException cex) { + // Oops, lost connection + throw new FolderClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + } // Release lock + + if (msgno > total) // Still out of range ? Throw up ... + throw new IndexOutOfBoundsException(msgno + " > " + total); + } + + /* + * Check whether the given flags are supported by this server, + * and also verify that the folder allows setting flags. + */ + private void checkFlags(Flags flags) throws MessagingException { + assert Thread.holdsLock(this); + if (mode != READ_WRITE) + throw new IllegalStateException( + "Cannot change flags on READ_ONLY folder: " + fullName + ); + /* + if (!availableFlags.contains(flags)) + throw new MessagingException( + "These flags are not supported by this implementation" + ); + */ + } + + /** + * Get the name of this folder. + */ + @Override + public synchronized String getName() { + /* Return the last component of this Folder's full name. + * Folder components are delimited by the separator character. + */ + if (name == null) { + try { + name = fullName.substring( + fullName.lastIndexOf(getSeparator()) + 1 + ); + } catch (MessagingException mex) { } + } + return name; + } + + /** + * Get the fullname of this folder. + */ + @Override + public String getFullName() { + return fullName; + } + + /** + * Get this folder's parent. + */ + @Override + public synchronized Folder getParent() throws MessagingException { + char c = getSeparator(); + int index; + if ((index = fullName.lastIndexOf(c)) != -1) + return ((IMAPStore)store).newIMAPFolder( + fullName.substring(0, index), c); + else + return new DefaultFolder((IMAPStore)store); + } + + /** + * Check whether this folder really exists on the server. + */ + @Override + public synchronized boolean exists() throws MessagingException { + // Check whether this folder exists .. + ListInfo[] li = null; + final String lname; + if (isNamespace && separator != '\0') + lname = fullName + separator; + else + lname = fullName; + + li = (ListInfo[])doCommand(new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) throws ProtocolException { + return p.list("", lname); + } + }); + + if (li != null) { + int i = findName(li, lname); + fullName = li[i].name; + separator = li[i].separator; + int len = fullName.length(); + if (separator != '\0' && len > 0 && + fullName.charAt(len - 1) == separator) { + fullName = fullName.substring(0, len - 1); + } + type = 0; + if (li[i].hasInferiors) + type |= HOLDS_FOLDERS; + if (li[i].canOpen) + type |= HOLDS_MESSAGES; + exists = true; + attributes = li[i].attrs; + } else { + exists = opened; + attributes = null; + } + + return exists; + } + + /** + * Which entry in <code>li</code> matches <code>lname</code>? + * If the name contains wildcards, more than one entry may be + * returned. + */ + private int findName(ListInfo[] li, String lname) { + int i; + // if the name contains a wildcard, there might be more than one + for (i = 0; i < li.length; i++) { + if (li[i].name.equals(lname)) + break; + } + if (i >= li.length) { // nothing matched exactly + // XXX - possibly should fail? But what if server + // is case insensitive and returns the preferred + // case of the name here? + i = 0; // use first one + } + return i; + } + + /** + * List all subfolders matching the specified pattern. + */ + @Override + public Folder[] list(String pattern) throws MessagingException { + return doList(pattern, false); + } + + /** + * List all subscribed subfolders matching the specified pattern. + */ + @Override + public Folder[] listSubscribed(String pattern) throws MessagingException { + return doList(pattern, true); + } + + private synchronized Folder[] doList(final String pattern, + final boolean subscribed) throws MessagingException { + checkExists(); // insure that this folder does exist. + + // Why waste a roundtrip to the server? + if (attributes != null && !isDirectory()) + return new Folder[0]; + + final char c = getSeparator(); + + ListInfo[] li = (ListInfo[])doCommandIgnoreFailure( + new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) + throws ProtocolException { + if (subscribed) + return p.lsub("", fullName + c + pattern); + else + return p.list("", fullName + c + pattern); + } + }); + + if (li == null) + return new Folder[0]; + + /* + * The UW based IMAP4 servers (e.g. SIMS2.0) include + * current folder (terminated with the separator), when + * the LIST pattern is '%' or '*'. i.e, <LIST "" mail/%> + * returns "mail/" as the first LIST response. + * + * Doesn't make sense to include the current folder in this + * case, so we filter it out. Note that I'm assuming that + * the offending response is the *first* one, my experiments + * with the UW & SIMS2.0 servers indicate that .. + */ + int start = 0; + // Check the first LIST response. + if (li.length > 0 && li[0].name.equals(fullName + c)) + start = 1; // start from index = 1 + + IMAPFolder[] folders = new IMAPFolder[li.length - start]; + IMAPStore st = (IMAPStore)store; + for (int i = start; i < li.length; i++) + folders[i-start] = st.newIMAPFolder(li[i]); + return folders; + } + + /** + * Get the separator character. + */ + @Override + public synchronized char getSeparator() throws MessagingException { + if (separator == UNKNOWN_SEPARATOR) { + ListInfo[] li = null; + + li = (ListInfo[])doCommand(new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) + throws ProtocolException { + // REV1 allows the following LIST format to obtain + // the hierarchy delimiter of non-existent folders + if (p.isREV1()) // IMAP4rev1 + return p.list(fullName, ""); + else // IMAP4, note that this folder must exist for this + // to work :( + return p.list("", fullName); + } + }); + + if (li != null) + separator = li[0].separator; + else + separator = '/'; // punt ! + } + return separator; + } + + /** + * Get the type of this folder. + */ + @Override + public synchronized int getType() throws MessagingException { + if (opened) { + // never throw FolderNotFoundException if folder is open + if (attributes == null) + exists(); // try to fetch attributes + } else { + checkExists(); + } + return type; + } + + /** + * Check whether this folder is subscribed. + */ + @Override + public synchronized boolean isSubscribed() { + ListInfo[] li = null; + final String lname; + if (isNamespace && separator != '\0') + lname = fullName + separator; + else + lname = fullName; + + try { + li = (ListInfo[])doProtocolCommand(new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) + throws ProtocolException { + return p.lsub("", lname); + } + }); + } catch (ProtocolException pex) { + } + + if (li != null) { + int i = findName(li, lname); + return li[i].canOpen; + } else + return false; + } + + /** + * Subscribe/Unsubscribe this folder. + */ + @Override + public synchronized void setSubscribed(final boolean subscribe) + throws MessagingException { + doCommandIgnoreFailure(new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) throws ProtocolException { + if (subscribe) + p.subscribe(fullName); + else + p.unsubscribe(fullName); + return null; + } + }); + } + + /** + * Create this folder, with the specified type. + */ + @Override + public synchronized boolean create(final int type) + throws MessagingException { + + char c = 0; + if ((type & HOLDS_MESSAGES) == 0) // only holds folders + c = getSeparator(); + final char sep = c; + Object ret = doCommandIgnoreFailure(new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) + throws ProtocolException { + if ((type & HOLDS_MESSAGES) == 0) // only holds folders + p.create(fullName + sep); + else { + p.create(fullName); + + // Certain IMAP servers do not allow creation of folders + // that can contain messages *and* subfolders. So, if we + // were asked to create such a folder, we should verify + // that we could indeed do so. + if ((type & HOLDS_FOLDERS) != 0) { + // we want to hold subfolders and messages. Check + // whether we could create such a folder. + ListInfo[] li = p.list("", fullName); + if (li != null && !li[0].hasInferiors) { + // Hmm ..the new folder + // doesn't support Inferiors ? Fail + p.delete(fullName); + throw new ProtocolException("Unsupported type"); + } + } + } + return Boolean.TRUE; + } + }); + + if (ret == null) + return false; // CREATE failure, maybe this + // folder already exists ? + + // exists = true; + // this.type = type; + boolean retb = exists(); // set exists, type, and attributes + if (retb) // Notify listeners on self and our Store + notifyFolderListeners(FolderEvent.CREATED); + return retb; + } + + /** + * Check whether this folder has new messages. + */ + @Override + public synchronized boolean hasNewMessages() throws MessagingException { + synchronized (messageCacheLock) { + if (opened) { // If we are open, we already have this information + // Folder is open, make sure information is up to date + // tickle the folder and store connections. + try { + keepConnectionAlive(true); + } catch (ConnectionException cex) { + throw new FolderClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + return recent > 0 ? true : false; + } + } + + // First, the cheap way - use LIST and look for the \Marked + // or \Unmarked tag + + ListInfo[] li = null; + final String lname; + if (isNamespace && separator != '\0') + lname = fullName + separator; + else + lname = fullName; + li = (ListInfo[])doCommandIgnoreFailure(new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) throws ProtocolException { + return p.list("", lname); + } + }); + + // if folder doesn't exist, throw exception + if (li == null) + throw new FolderNotFoundException(this, fullName + " not found"); + + int i = findName(li, lname); + if (li[i].changeState == ListInfo.CHANGED) + return true; + else if (li[i].changeState == ListInfo.UNCHANGED) + return false; + + // LIST didn't work. Try the hard way, using STATUS + try { + Status status = getStatus(); + if (status.recent > 0) + return true; + else + return false; + } catch (BadCommandException bex) { + // Probably doesn't support STATUS, tough luck. + return false; + } catch (ConnectionException cex) { + throw new StoreClosedException(store, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + } + + /** + * Get the named subfolder. + */ + @Override + public synchronized Folder getFolder(String name) + throws MessagingException { + // If we know that this folder is *not* a directory, don't + // send the request to the server at all ... + if (attributes != null && !isDirectory()) + throw new MessagingException("Cannot contain subfolders"); + + char c = getSeparator(); + return ((IMAPStore)store).newIMAPFolder(fullName + c + name, c); + } + + /** + * Delete this folder. + */ + @Override + public synchronized boolean delete(boolean recurse) + throws MessagingException { + checkClosed(); // insure that this folder is closed. + + if (recurse) { + // Delete all subfolders. + Folder[] f = list(); + for (int i = 0; i < f.length; i++) + f[i].delete(recurse); // ignore intermediate failures + } + + // Attempt to delete this folder + + Object ret = doCommandIgnoreFailure(new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) throws ProtocolException { + p.delete(fullName); + return Boolean.TRUE; + } + }); + + if (ret == null) + // Non-existent folder/No permission ?? + return false; + + // DELETE succeeded. + exists = false; + attributes = null; + + // Notify listeners on self and our Store + notifyFolderListeners(FolderEvent.DELETED); + return true; + } + + /** + * Rename this folder. + */ + @Override + public synchronized boolean renameTo(final Folder f) + throws MessagingException { + checkClosed(); // insure that we are closed. + checkExists(); + if (f.getStore() != store) + throw new MessagingException("Can't rename across Stores"); + + + Object ret = doCommandIgnoreFailure(new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) throws ProtocolException { + p.rename(fullName, f.getFullName()); + return Boolean.TRUE; + } + }); + + if (ret == null) + return false; + + exists = false; + attributes = null; + notifyFolderRenamedListeners(f); + return true; + } + + /** + * Open this folder in the given mode. + */ + @Override + public synchronized void open(int mode) throws MessagingException { + open(mode, null); + } + + /** + * Open this folder in the given mode, with the given + * resynchronization data. + * + * @param mode the open mode (Folder.READ_WRITE or Folder.READ_ONLY) + * @param rd the ResyncData instance + * @return a List of MailEvent instances, or null if none + * @exception MessagingException if the open fails + * @since JavaMail 1.5.1 + */ + public synchronized List<MailEvent> open(int mode, ResyncData rd) + throws MessagingException { + checkClosed(); // insure that we are not already open + + MailboxInfo mi = null; + // Request store for our own protocol connection. + protocol = ((IMAPStore)store).getProtocol(this); + + List<MailEvent> openEvents = null; + synchronized(messageCacheLock) { // Acquire messageCacheLock + + /* + * Add response handler right away so we get any alerts or + * notifications that occur during the SELECT or EXAMINE. + * Have to be sure to remove it if we fail to open the + * folder. + */ + protocol.addResponseHandler(this); + + try { + /* + * Enable QRESYNC or CONDSTORE if needed and not enabled. + * QRESYNC implies CONDSTORE, but servers that support + * QRESYNC are not required to support just CONDSTORE + * per RFC 5162. + */ + if (rd != null) { + if (rd == ResyncData.CONDSTORE) { + if (!protocol.isEnabled("CONDSTORE") && + !protocol.isEnabled("QRESYNC")) { + if (protocol.hasCapability("CONDSTORE")) + protocol.enable("CONDSTORE"); + else + protocol.enable("QRESYNC"); + } + } else { + if (!protocol.isEnabled("QRESYNC")) + protocol.enable("QRESYNC"); + } + } + + if (mode == READ_ONLY) + mi = protocol.examine(fullName, rd); + else + mi = protocol.select(fullName, rd); + } catch (CommandFailedException cex) { + /* + * Handle SELECT or EXAMINE failure. + * Try to figure out why the operation failed so we can + * report a more reasonable exception. + * + * Will use our existing protocol object. + */ + try { + checkExists(); // throw exception if folder doesn't exist + + if ((type & HOLDS_MESSAGES) == 0) + throw new MessagingException( + "folder cannot contain messages"); + throw new MessagingException(cex.getMessage(), cex); + + } finally { + // folder not open, don't keep this information + exists = false; + attributes = null; + type = 0; + // connection still good, return it + releaseProtocol(true); + } + // NOTREACHED + } catch (ProtocolException pex) { + // got a BAD or a BYE; connection may be bad, close it + try { + throw logoutAndThrow(pex.getMessage(), pex); + } finally { + releaseProtocol(false); + } + } + + if (mi.mode != mode) { + if (mode == READ_WRITE && mi.mode == READ_ONLY && + ((IMAPStore)store).allowReadOnlySelect()) { + ; // all ok, allow it + } else { // otherwise, it's an error + ReadOnlyFolderException ife = new ReadOnlyFolderException( + this, "Cannot open in desired mode"); + throw cleanupAndThrow(ife); + } + } + + // Initialize stuff. + opened = true; + reallyClosed = false; + this.mode = mi.mode; + availableFlags = mi.availableFlags; + permanentFlags = mi.permanentFlags; + total = realTotal = mi.total; + recent = mi.recent; + uidvalidity = mi.uidvalidity; + uidnext = mi.uidnext; + uidNotSticky = mi.uidNotSticky; + highestmodseq = mi.highestmodseq; + + // Create the message cache of appropriate size + messageCache = new MessageCache(this, (IMAPStore)store, total); + + // process saved responses and return corresponding events + if (mi.responses != null) { + openEvents = new ArrayList<>(); + for (IMAPResponse ir : mi.responses) { + if (ir.keyEquals("VANISHED")) { + // "VANISHED" SP ["(EARLIER)"] SP known-uids + String[] s = ir.readAtomStringList(); + // check that it really is "EARLIER" + if (s == null || s.length != 1 || + !s[0].equalsIgnoreCase("EARLIER")) + continue; // it's not, what to do with it here? + String uids = ir.readAtom(); + UIDSet[] uidset = UIDSet.parseUIDSets(uids); + long[] luid = UIDSet.toArray(uidset, uidnext); + if (luid != null && luid.length > 0) + openEvents.add( + new MessageVanishedEvent(this, luid)); + } else if (ir.keyEquals("FETCH")) { + assert ir instanceof FetchResponse : + "!ir instanceof FetchResponse"; + Message msg = processFetchResponse((FetchResponse)ir); + if (msg != null) + openEvents.add(new MessageChangedEvent(this, + MessageChangedEvent.FLAGS_CHANGED, msg)); + } + } + } + } // Release lock + + exists = true; // if we opened it, it must exist + attributes = null; // but we don't yet know its attributes + type = HOLDS_MESSAGES; // lacking more info, we know at least this much + + // notify listeners + notifyConnectionListeners(ConnectionEvent.OPENED); + + return openEvents; + } + + private MessagingException cleanupAndThrow(MessagingException ife) { + try { + try { + // close mailbox and return connection + protocol.close(); + releaseProtocol(true); + } catch (ProtocolException pex) { + // something went wrong, close connection + try { + addSuppressed(ife, logoutAndThrow(pex.getMessage(), pex)); + } finally { + releaseProtocol(false); + } + } + } catch (Throwable thr) { + addSuppressed(ife, thr); + } + return ife; + } + + private MessagingException logoutAndThrow(String why, ProtocolException t) { + MessagingException ife = new MessagingException(why, t); + try { + protocol.logout(); + } catch (Throwable thr) { + addSuppressed(ife, thr); + } + return ife; + } + + private void addSuppressed(Throwable ife, Throwable thr) { + if (isRecoverable(thr)) { + ife.addSuppressed(thr); + } else { + thr.addSuppressed(ife); + if (thr instanceof Error) { + throw (Error) thr; + } + if (thr instanceof RuntimeException) { + throw (RuntimeException) thr; + } + throw new RuntimeException("unexpected exception", thr); + } + } + + private boolean isRecoverable(Throwable t) { + return (t instanceof Exception) || (t instanceof LinkageError); + } + + /** + * Prefetch attributes, based on the given FetchProfile. + */ + @Override + public synchronized void fetch(Message[] msgs, FetchProfile fp) + throws MessagingException { + // cache this information in case connection is closed and + // protocol is set to null + boolean isRev1; + FetchItem[] fitems; + synchronized (messageCacheLock) { + checkOpened(); + isRev1 = protocol.isREV1(); + fitems = protocol.getFetchItems(); + } + + StringBuilder command = new StringBuilder(); + boolean first = true; + boolean allHeaders = false; + + if (fp.contains(FetchProfile.Item.ENVELOPE)) { + command.append(getEnvelopeCommand()); + first = false; + } + if (fp.contains(FetchProfile.Item.FLAGS)) { + command.append(first ? "FLAGS" : " FLAGS"); + first = false; + } + if (fp.contains(FetchProfile.Item.CONTENT_INFO)) { + command.append(first ? "BODYSTRUCTURE" : " BODYSTRUCTURE"); + first = false; + } + if (fp.contains(UIDFolder.FetchProfileItem.UID)) { + command.append(first ? "UID" : " UID"); + first = false; + } + if (fp.contains(IMAPFolder.FetchProfileItem.HEADERS)) { + allHeaders = true; + if (isRev1) + command.append(first ? + "BODY.PEEK[HEADER]" : " BODY.PEEK[HEADER]"); + else + command.append(first ? "RFC822.HEADER" : " RFC822.HEADER"); + first = false; + } + if (fp.contains(IMAPFolder.FetchProfileItem.MESSAGE)) { + allHeaders = true; + if (isRev1) + command.append(first ? "BODY.PEEK[]" : " BODY.PEEK[]"); + else + command.append(first ? "RFC822" : " RFC822"); + first = false; + } + if (fp.contains(FetchProfile.Item.SIZE) || + fp.contains(IMAPFolder.FetchProfileItem.SIZE)) { + command.append(first ? "RFC822.SIZE" : " RFC822.SIZE"); + first = false; + } + if (fp.contains(IMAPFolder.FetchProfileItem.INTERNALDATE)) { + command.append(first ? "INTERNALDATE" : " INTERNALDATE"); + first = false; + } + + // if we're not fetching all headers, fetch individual headers + String[] hdrs = null; + if (!allHeaders) { + hdrs = fp.getHeaderNames(); + if (hdrs.length > 0) { + if (!first) + command.append(" "); + command.append(createHeaderCommand(hdrs, isRev1)); + } + } + + /* + * Add any additional extension fetch items. + */ + for (int i = 0; i < fitems.length; i++) { + if (fp.contains(fitems[i].getFetchProfileItem())) { + if (command.length() != 0) + command.append(" "); + command.append(fitems[i].getName()); + } + } + + Utility.Condition condition = + new IMAPMessage.FetchProfileCondition(fp, fitems); + + // Acquire the Folder's MessageCacheLock. + synchronized (messageCacheLock) { + + // check again to make sure folder is still open + checkOpened(); + + // Apply the test, and get the sequence-number set for + // the messages that need to be prefetched. + MessageSet[] msgsets = Utility.toMessageSetSorted(msgs, condition); + + if (msgsets == null) + // We already have what we need. + return; + + Response[] r = null; + // to collect non-FETCH responses & unsolicited FETCH FLAG responses + List<Response> v = new ArrayList<>(); + try { + r = getProtocol().fetch(msgsets, command.toString()); + } catch (ConnectionException cex) { + throw new FolderClosedException(this, cex.getMessage()); + } catch (CommandFailedException cfx) { + // Ignore these, as per RFC 2180 + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + + if (r == null) + return; + + for (int i = 0; i < r.length; i++) { + if (r[i] == null) + continue; + if (!(r[i] instanceof FetchResponse)) { + v.add(r[i]); // Unsolicited Non-FETCH response + continue; + } + + // Got a FetchResponse. + FetchResponse f = (FetchResponse)r[i]; + // Get the corresponding message. + IMAPMessage msg = getMessageBySeqNumber(f.getNumber()); + + int count = f.getItemCount(); + boolean unsolicitedFlags = false; + + for (int j = 0; j < count; j++) { + Item item = f.getItem(j); + // Check for the FLAGS item + if (item instanceof Flags && + (!fp.contains(FetchProfile.Item.FLAGS) || + msg == null)) { + // Ok, Unsolicited FLAGS update. + unsolicitedFlags = true; + } else if (msg != null) + msg.handleFetchItem(item, hdrs, allHeaders); + } + if (msg != null) + msg.handleExtensionFetchItems(f.getExtensionItems()); + + // If this response contains any unsolicited FLAGS + // add it to the unsolicited response vector + if (unsolicitedFlags) + v.add(f); + } + + // Dispatch any unsolicited responses + if (!v.isEmpty()) { + Response[] responses = new Response[v.size()]; + v.toArray(responses); + handleResponses(responses); + } + + } // Release messageCacheLock + } + + /** + * Return the IMAP FETCH items to request in order to load + * all the "envelope" data. Subclasses can override this + * method to fetch more data when FetchProfile.Item.ENVELOPE + * is requested. + * + * @return the IMAP FETCH items to request + * @since JavaMail 1.4.6 + */ + protected String getEnvelopeCommand() { + return IMAPMessage.EnvelopeCmd; + } + + /** + * Create a new IMAPMessage object to represent the given message number. + * Subclasses of IMAPFolder may override this method to create a + * subclass of IMAPMessage. + * + * @param msgnum the message sequence number + * @return the new IMAPMessage object + * @since JavaMail 1.4.6 + */ + protected IMAPMessage newIMAPMessage(int msgnum) { + return new IMAPMessage(this, msgnum); + } + + /** + * Create the appropriate IMAP FETCH command items to fetch the + * requested headers. + */ + private String createHeaderCommand(String[] hdrs, boolean isRev1) { + StringBuilder sb; + + if (isRev1) + sb = new StringBuilder("BODY.PEEK[HEADER.FIELDS ("); + else + sb = new StringBuilder("RFC822.HEADER.LINES ("); + + for (int i = 0; i < hdrs.length; i++) { + if (i > 0) + sb.append(" "); + sb.append(hdrs[i]); + } + + if (isRev1) + sb.append(")]"); + else + sb.append(")"); + + return sb.toString(); + } + + /** + * Set the specified flags for the given array of messages. + */ + @Override + public synchronized void setFlags(Message[] msgs, Flags flag, boolean value) + throws MessagingException { + checkOpened(); + checkFlags(flag); // validate flags + + if (msgs.length == 0) // boundary condition + return; + + synchronized(messageCacheLock) { + try { + IMAPProtocol p = getProtocol(); + MessageSet[] ms = Utility.toMessageSetSorted(msgs, null); + if (ms == null) + throw new MessageRemovedException( + "Messages have been removed"); + p.storeFlags(ms, flag, value); + } catch (ConnectionException cex) { + throw new FolderClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + } + } + + /** + * Set the specified flags for the given range of message numbers. + */ + @Override + public synchronized void setFlags(int start, int end, + Flags flag, boolean value) throws MessagingException { + checkOpened(); + Message[] msgs = new Message[end - start + 1]; + int i = 0; + for (int n = start; n <= end; n++) + msgs[i++] = getMessage(n); + setFlags(msgs, flag, value); + } + + /** + * Set the specified flags for the given array of message numbers. + */ + @Override + public synchronized void setFlags(int[] msgnums, Flags flag, boolean value) + throws MessagingException { + checkOpened(); + Message[] msgs = new Message[msgnums.length]; + for (int i = 0; i < msgnums.length; i++) + msgs[i] = getMessage(msgnums[i]); + setFlags(msgs, flag, value); + } + + /** + * Close this folder. + */ + @Override + public synchronized void close(boolean expunge) throws MessagingException { + close(expunge, false); + } + + /** + * Close this folder without waiting for the server. + * + * @exception MessagingException for failures + */ + public synchronized void forceClose() throws MessagingException { + close(false, true); + } + + /* + * Common close method. + */ + private void close(boolean expunge, boolean force) + throws MessagingException { + assert Thread.holdsLock(this); + synchronized(messageCacheLock) { + /* + * If we already know we're closed, this is illegal. + * Can't use checkOpened() because if we were forcibly + * closed asynchronously we just want to complete the + * closing here. + */ + if (!opened && reallyClosed) + throw new IllegalStateException( + "This operation is not allowed on a closed folder" + ); + + reallyClosed = true; // Ok, lets reset + + // Maybe this folder is already closed, or maybe another + // thread which had the messageCacheLock earlier, found + // that our server connection is dead and cleaned up + // everything .. + if (!opened) + return; + + boolean reuseProtocol = true; + try { + waitIfIdle(); + if (force) { + logger.log(Level.FINE, "forcing folder {0} to close", + fullName); + if (protocol != null) + protocol.disconnect(); + } else if (((IMAPStore)store).isConnectionPoolFull()) { + // If the connection pool is full, logout the connection + logger.fine( + "pool is full, not adding an Authenticated connection"); + + // If the expunge flag is set, close the folder first. + if (expunge && protocol != null) + protocol.close(); + + if (protocol != null) + protocol.logout(); + } else { + // If the expunge flag is set or we're open read-only we + // can just close the folder, otherwise open it read-only + // before closing, or unselect it if supported. + if (!expunge && mode == READ_WRITE) { + try { + if (protocol != null && + protocol.hasCapability("UNSELECT")) + protocol.unselect(); + else { + // Unselect isn't supported so we need to + // select a folder to cause this one to be + // deselected without expunging messages. + // We try to do that by reopening the current + // folder read-only. If the current folder + // was renamed out from under us, the EXAMINE + // might fail, but that's ok because it still + // leaves us with the folder deselected. + if (protocol != null) { + boolean selected = true; + try { + protocol.examine(fullName); + // success, folder still selected + } catch (CommandFailedException ex) { + // EXAMINE failed, folder is no + // longer selected + selected = false; + } + if (selected && protocol != null) + protocol.close(); + } + } + } catch (ProtocolException pex2) { + reuseProtocol = false; // something went wrong + } + } else { + if (protocol != null) + protocol.close(); + } + } + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } finally { + // cleanup if we haven't already + if (opened) + cleanup(reuseProtocol); + } + } + } + + // NOTE: this method can currently be invoked from close() or + // from handleResponses(). Both invocations are conditional, + // based on the "opened" flag, so we are sure that multiple + // Connection.CLOSED events are not generated. Also both + // invocations are from within messageCacheLock-ed areas. + private void cleanup(boolean returnToPool) { + assert Thread.holdsLock(messageCacheLock); + releaseProtocol(returnToPool); + messageCache = null; + uidTable = null; + exists = false; // to force a recheck in exists(). + attributes = null; + opened = false; + idleState = RUNNING; // just in case + messageCacheLock.notifyAll(); // wake up anyone waiting + notifyConnectionListeners(ConnectionEvent.CLOSED); + } + + /** + * Check whether this connection is really open. + */ + @Override + public synchronized boolean isOpen() { + synchronized(messageCacheLock) { + // Probe the connection to make sure its really open. + if (opened) { + try { + keepConnectionAlive(false); + } catch (ProtocolException pex) { } + } + } + + return opened; + } + + /** + * Return the permanent flags supported by the server. + */ + @Override + public synchronized Flags getPermanentFlags() { + if (permanentFlags == null) + return null; + return (Flags)(permanentFlags.clone()); + } + + /** + * Get the total message count. + */ + @Override + public synchronized int getMessageCount() throws MessagingException { + synchronized (messageCacheLock) { + if (opened) { + // Folder is open, we know what the total message count is .. + // tickle the folder and store connections. + try { + keepConnectionAlive(true); + return total; + } catch (ConnectionException cex) { + throw new FolderClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + } + } + + // If this folder is not yet open, we use STATUS to + // get the total message count + checkExists(); + try { + Status status = getStatus(); + return status.total; + } catch (BadCommandException bex) { + // doesn't support STATUS, probably vanilla IMAP4 .. + // lets try EXAMINE + IMAPProtocol p = null; + + try { + p = getStoreProtocol(); // XXX + MailboxInfo minfo = p.examine(fullName); + p.close(); + return minfo.total; + } catch (ProtocolException pex) { + // Give up. + throw new MessagingException(pex.getMessage(), pex); + } finally { + releaseStoreProtocol(p); + } + } catch (ConnectionException cex) { + throw new StoreClosedException(store, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + } + + /** + * Get the new message count. + */ + @Override + public synchronized int getNewMessageCount() throws MessagingException { + synchronized (messageCacheLock) { + if (opened) { + // Folder is open, we know what the new message count is .. + // tickle the folder and store connections. + try { + keepConnectionAlive(true); + return recent; + } catch (ConnectionException cex) { + throw new FolderClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + } + } + + // If this folder is not yet open, we use STATUS to + // get the new message count + checkExists(); + try { + Status status = getStatus(); + return status.recent; + } catch (BadCommandException bex) { + // doesn't support STATUS, probably vanilla IMAP4 .. + // lets try EXAMINE + IMAPProtocol p = null; + + try { + p = getStoreProtocol(); // XXX + MailboxInfo minfo = p.examine(fullName); + p.close(); + return minfo.recent; + } catch (ProtocolException pex) { + // Give up. + throw new MessagingException(pex.getMessage(), pex); + } finally { + releaseStoreProtocol(p); + } + } catch (ConnectionException cex) { + throw new StoreClosedException(store, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + } + + /** + * Get the unread message count. + */ + @Override + public synchronized int getUnreadMessageCount() + throws MessagingException { + if (!opened) { + checkExists(); + // If this folder is not yet open, we use STATUS to + // get the unseen message count + try { + Status status = getStatus(); + return status.unseen; + } catch (BadCommandException bex) { + // doesn't support STATUS, probably vanilla IMAP4 .. + // Could EXAMINE, SEARCH for UNREAD messages and + // return the count .. bah, not worth it. + return -1; + } catch (ConnectionException cex) { + throw new StoreClosedException(store, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + } + + // if opened, issue server-side search for messages that do + // *not* have the SEEN flag. + Flags f = new Flags(); + f.add(Flags.Flag.SEEN); + try { + synchronized(messageCacheLock) { + int[] matches = getProtocol().search(new FlagTerm(f, false)); + return matches.length; // NOTE: 'matches' is never null + } + } catch (ConnectionException cex) { + throw new FolderClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + // Shouldn't happen + throw new MessagingException(pex.getMessage(), pex); + } + } + + /** + * Get the deleted message count. + */ + @Override + public synchronized int getDeletedMessageCount() + throws MessagingException { + if (!opened) { + checkExists(); + // no way to do this on closed folders + return -1; + } + + // if opened, issue server-side search for messages that do + // have the DELETED flag. + Flags f = new Flags(); + f.add(Flags.Flag.DELETED); + try { + synchronized(messageCacheLock) { + int[] matches = getProtocol().search(new FlagTerm(f, true)); + return matches.length; // NOTE: 'matches' is never null + } + } catch (ConnectionException cex) { + throw new FolderClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + // Shouldn't happen + throw new MessagingException(pex.getMessage(), pex); + } + } + + /* + * Get results of STATUS command for this folder, checking cache first. + * ASSERT: Must be called with this folder's synchronization lock held. + * ASSERT: The folder must be closed. + */ + private Status getStatus() throws ProtocolException { + int statusCacheTimeout = ((IMAPStore)store).getStatusCacheTimeout(); + + // if allowed to cache and our cache is still valid, return it + if (statusCacheTimeout > 0 && cachedStatus != null && + System.currentTimeMillis() - cachedStatusTime < statusCacheTimeout) + return cachedStatus; + + IMAPProtocol p = null; + + try { + p = getStoreProtocol(); // XXX + Status s = p.status(fullName, null); + // if allowed to cache, do so + if (statusCacheTimeout > 0) { + cachedStatus = s; + cachedStatusTime = System.currentTimeMillis(); + } + return s; + } finally { + releaseStoreProtocol(p); + } + } + + /** + * Get the specified message. + */ + @Override + public synchronized Message getMessage(int msgnum) + throws MessagingException { + checkOpened(); + checkRange(msgnum); + + return messageCache.getMessage(msgnum); + } + + /** + * {@inheritDoc} + */ + @Override + public synchronized Message[] getMessages() throws MessagingException { + /* + * Need to override Folder method to throw FolderClosedException + * instead of IllegalStateException if not really closed. + */ + checkOpened(); + int total = getMessageCount(); + Message[] msgs = new Message[total]; + for (int i = 1; i <= total; i++) + msgs[i - 1] = messageCache.getMessage(i); + return msgs; + } + + /** + * Append the given messages into this folder. + */ + @Override + public synchronized void appendMessages(Message[] msgs) + throws MessagingException { + checkExists(); // verify that self exists + + // XXX - have to verify that messages are in a different + // store (if any) than target folder, otherwise could + // deadlock trying to fetch messages on the same connection + // we're using for the append. + + int maxsize = ((IMAPStore)store).getAppendBufferSize(); + + for (int i = 0; i < msgs.length; i++) { + final Message m = msgs[i]; + Date d = m.getReceivedDate(); // retain dates + if (d == null) + d = m.getSentDate(); + final Date dd = d; + final Flags f = m.getFlags(); + + final MessageLiteral mos; + try { + // if we know the message is too big, don't buffer any of it + mos = new MessageLiteral(m, + m.getSize() > maxsize ? 0 : maxsize); + } catch (IOException ex) { + throw new MessagingException( + "IOException while appending messages", ex); + } catch (MessageRemovedException mrex) { + continue; // just skip this expunged message + } + + doCommand(new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) + throws ProtocolException { + p.append(fullName, f, dd, mos); + return null; + } + }); + } + } + + /** + * Append the given messages into this folder. + * Return array of AppendUID objects containing + * UIDs of these messages in the destination folder. + * Each element of the returned array corresponds to + * an element of the <code>msgs</code> array. A null + * element means the server didn't return UID information + * for the appended message. <p> + * + * Depends on the APPENDUID response code defined by the + * UIDPLUS extension - + * <A HREF="http://www.ietf.org/rfc/rfc4315.txt">RFC 4315</A>. + * + * @param msgs the messages to append + * @return array of AppendUID objects + * @exception MessagingException for failures + * @since JavaMail 1.4 + */ + public synchronized AppendUID[] appendUIDMessages(Message[] msgs) + throws MessagingException { + checkExists(); // verify that self exists + + // XXX - have to verify that messages are in a different + // store (if any) than target folder, otherwise could + // deadlock trying to fetch messages on the same connection + // we're using for the append. + + int maxsize = ((IMAPStore)store).getAppendBufferSize(); + + AppendUID[] uids = new AppendUID[msgs.length]; + for (int i = 0; i < msgs.length; i++) { + final Message m = msgs[i]; + final MessageLiteral mos; + + try { + // if we know the message is too big, don't buffer any of it + mos = new MessageLiteral(m, + m.getSize() > maxsize ? 0 : maxsize); + } catch (IOException ex) { + throw new MessagingException( + "IOException while appending messages", ex); + } catch (MessageRemovedException mrex) { + continue; // just skip this expunged message + } + + Date d = m.getReceivedDate(); // retain dates + if (d == null) + d = m.getSentDate(); + final Date dd = d; + final Flags f = m.getFlags(); + AppendUID auid = (AppendUID)doCommand(new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) + throws ProtocolException { + return p.appenduid(fullName, f, dd, mos); + } + }); + uids[i] = auid; + } + return uids; + } + + /** + * Append the given messages into this folder. + * Return array of Message objects representing + * the messages in the destination folder. Note + * that the folder must be open. + * Each element of the returned array corresponds to + * an element of the <code>msgs</code> array. A null + * element means the server didn't return UID information + * for the appended message. <p> + * + * Depends on the APPENDUID response code defined by the + * UIDPLUS extension - + * <A HREF="http://www.ietf.org/rfc/rfc4315.txt">RFC 4315</A>. + * + * @param msgs the messages to add + * @return the messages in this folder + * @exception MessagingException for failures + * @since JavaMail 1.4 + */ + public synchronized Message[] addMessages(Message[] msgs) + throws MessagingException { + checkOpened(); + Message[] rmsgs = new MimeMessage[msgs.length]; + AppendUID[] uids = appendUIDMessages(msgs); + for (int i = 0; i < uids.length; i++) { + AppendUID auid = uids[i]; + if (auid != null) { + if (auid.uidvalidity == uidvalidity) { + try { + rmsgs[i] = getMessageByUID(auid.uid); + } catch (MessagingException mex) { + // ignore errors at this stage + } + } + } + } + return rmsgs; + } + + /** + * Copy the specified messages from this folder, to the + * specified destination. + */ + @Override + public synchronized void copyMessages(Message[] msgs, Folder folder) + throws MessagingException { + copymoveMessages(msgs, folder, false); + } + + /** + * Copy the specified messages from this folder, to the + * specified destination. + * Return array of AppendUID objects containing + * UIDs of these messages in the destination folder. + * Each element of the returned array corresponds to + * an element of the <code>msgs</code> array. A null + * element means the server didn't return UID information + * for the copied message. <p> + * + * Depends on the COPYUID response code defined by the + * UIDPLUS extension - + * <A HREF="http://www.ietf.org/rfc/rfc4315.txt">RFC 4315</A>. + * + * @param msgs the messages to copy + * @param folder the folder to copy the messages to + * @return array of AppendUID objects + * @exception MessagingException for failures + * @since JavaMail 1.5.1 + */ + public synchronized AppendUID[] copyUIDMessages(Message[] msgs, + Folder folder) throws MessagingException { + return copymoveUIDMessages(msgs, folder, false); + } + + /** + * Move the specified messages from this folder, to the + * specified destination. + * + * Depends on the MOVE extension + * (<A HREF="http://www.ietf.org/rfc/rfc6851.txt">RFC 6851</A>). + * + * @param msgs the messages to move + * @param folder the folder to move the messages to + * @exception MessagingException for failures + * + * @since JavaMail 1.5.4 + */ + public synchronized void moveMessages(Message[] msgs, Folder folder) + throws MessagingException { + copymoveMessages(msgs, folder, true); + } + + /** + * Move the specified messages from this folder, to the + * specified destination. + * Return array of AppendUID objects containing + * UIDs of these messages in the destination folder. + * Each element of the returned array corresponds to + * an element of the <code>msgs</code> array. A null + * element means the server didn't return UID information + * for the moved message. <p> + * + * Depends on the MOVE extension + * (<A HREF="http://www.ietf.org/rfc/rfc6851.txt">RFC 6851</A>) + * and the COPYUID response code defined by the + * UIDPLUS extension + * (<A HREF="http://www.ietf.org/rfc/rfc4315.txt">RFC 4315</A>). + * + * @param msgs the messages to move + * @param folder the folder to move the messages to + * @return array of AppendUID objects + * @exception MessagingException for failures + * @since JavaMail 1.5.4 + */ + public synchronized AppendUID[] moveUIDMessages(Message[] msgs, + Folder folder) throws MessagingException { + return copymoveUIDMessages(msgs, folder, true); + } + + /** + * Copy or move the specified messages from this folder, to the + * specified destination. + * + * @since JavaMail 1.5.4 + */ + private synchronized void copymoveMessages(Message[] msgs, Folder folder, + boolean move) throws MessagingException { + checkOpened(); + + if (msgs.length == 0) // boundary condition + return; + + // If the destination belongs to our same store, optimize + if (folder.getStore() == store) { + synchronized(messageCacheLock) { + try { + IMAPProtocol p = getProtocol(); + MessageSet[] ms = Utility.toMessageSet(msgs, null); + if (ms == null) + throw new MessageRemovedException( + "Messages have been removed"); + if (move) + p.move(ms, folder.getFullName()); + else + p.copy(ms, folder.getFullName()); + } catch (CommandFailedException cfx) { + if (cfx.getMessage().indexOf("TRYCREATE") != -1) + throw new FolderNotFoundException( + folder, + folder.getFullName() + " does not exist" + ); + else + throw new MessagingException(cfx.getMessage(), cfx); + } catch (ConnectionException cex) { + throw new FolderClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + } + } else // destination is a different store. + if (move) + throw new MessagingException( + "Move between stores not supported"); + else + super.copyMessages(msgs, folder); + } + + /** + * Copy or move the specified messages from this folder, to the + * specified destination. + * Return array of AppendUID objects containing + * UIDs of these messages in the destination folder. + * Each element of the returned array corresponds to + * an element of the <code>msgs</code> array. A null + * element means the server didn't return UID information + * for the copied message. <p> + * + * Depends on the COPYUID response code defined by the + * UIDPLUS extension - + * <A HREF="http://www.ietf.org/rfc/rfc4315.txt">RFC 4315</A>. + * Move depends on the MOVE extension - + * <A HREF="http://www.ietf.org/rfc/rfc6851.txt">RFC 6851</A>. + * + * @param msgs the messages to copy + * @param folder the folder to copy the messages to + * @param move move instead of copy? + * @return array of AppendUID objects + * @exception MessagingException for failures + * @since JavaMail 1.5.4 + */ + private synchronized AppendUID[] copymoveUIDMessages(Message[] msgs, + Folder folder, boolean move) throws MessagingException { + checkOpened(); + + if (msgs.length == 0) // boundary condition + return null; + + // the destination must belong to our same store + if (folder.getStore() != store) // destination is a different store. + throw new MessagingException( + move ? + "can't moveUIDMessages to a different store" : + "can't copyUIDMessages to a different store"); + + // call fetch to make sure we have all the UIDs + // necessary to interpret the COPYUID response + FetchProfile fp = new FetchProfile(); + fp.add(UIDFolder.FetchProfileItem.UID); + fetch(msgs, fp); + // XXX - could pipeline the FETCH with the COPY/MOVE below + + synchronized (messageCacheLock) { + try { + IMAPProtocol p = getProtocol(); + // XXX - messages have to be from this Folder, who checks? + MessageSet[] ms = Utility.toMessageSet(msgs, null); + if (ms == null) + throw new MessageRemovedException( + "Messages have been removed"); + CopyUID cuid; + if (move) + cuid = p.moveuid(ms, folder.getFullName()); + else + cuid = p.copyuid(ms, folder.getFullName()); + + /* + * Correlate source UIDs with destination UIDs. + * This won't be time or space efficient if there's + * a lot of messages. + * + * In order to make sense of the returned UIDs, we need + * the UIDs for every one of the original messages. + * We fetch them above, to make sure we have them. + * This is critical for MOVE since after the MOVE the + * messages are gone/expunged. + * + * Assume the common case is that the messages are + * in order by UID. Map the returned source + * UIDs to their corresponding Message objects. + * Step through the msgs array looking for the + * Message object in the returned source message + * list. Most commonly the source message (UID) + * for the Nth original message will be in the Nth + * position in the returned source message (UID) + * list. Thus, the destination UID is in the Nth + * position in the returned destination UID list. + * But if the source message isn't where expected, + * we have to search the entire source message + * list, starting from where we expect it and + * wrapping around until we've searched it all. + * (Gmail will often return the lists in an unexpected order.) + * + * A possible optimization: + * If the number of UIDs returned is the same as the + * number of messages being copied/moved, we could + * sort the source messages by message number, sort + * the source and destination parallel arrays by source + * UID, and the resulting message and destination UID + * arrays will correspond. + * + * If the returned UID array size is different, some + * message was expunged while we were trying to copy/move it. + * This should be rare but would mean falling back to the + * general algorithm. + */ + long[] srcuids = UIDSet.toArray(cuid.src); + long[] dstuids = UIDSet.toArray(cuid.dst); + // map source UIDs to Message objects + // XXX - could inline/optimize this + Message[] srcmsgs = getMessagesByUID(srcuids); + AppendUID[] result = new AppendUID[msgs.length]; + for (int i = 0; i < msgs.length; i++) { + int j = i; + do { + if (msgs[i] == srcmsgs[j]) { + result[i] = new AppendUID( + cuid.uidvalidity, dstuids[j]); + break; + } + j++; + if (j >= srcmsgs.length) + j = 0; + } while (j != i); + } + return result; + } catch (CommandFailedException cfx) { + if (cfx.getMessage().indexOf("TRYCREATE") != -1) + throw new FolderNotFoundException( + folder, + folder.getFullName() + " does not exist" + ); + else + throw new MessagingException(cfx.getMessage(), cfx); + } catch (ConnectionException cex) { + throw new FolderClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + } + } + + /** + * Expunge all messages marked as DELETED. + */ + @Override + public synchronized Message[] expunge() throws MessagingException { + return expunge(null); + } + + /** + * Expunge the indicated messages, which must have been marked as DELETED. + * + * Depends on the UIDPLUS extension - + * <A HREF="http://www.ietf.org/rfc/rfc4315.txt">RFC 4315</A>. + * + * @param msgs the messages to expunge + * @return the expunged messages + * @exception MessagingException for failures + */ + public synchronized Message[] expunge(Message[] msgs) + throws MessagingException { + checkOpened(); + + if (msgs != null) { + // call fetch to make sure we have all the UIDs + FetchProfile fp = new FetchProfile(); + fp.add(UIDFolder.FetchProfileItem.UID); + fetch(msgs, fp); + } + + IMAPMessage[] rmsgs; + synchronized(messageCacheLock) { + doExpungeNotification = false; // We do this ourselves later + try { + IMAPProtocol p = getProtocol(); + if (msgs != null) + p.uidexpunge(Utility.toUIDSet(msgs)); + else + p.expunge(); + } catch (CommandFailedException cfx) { + // expunge not allowed, perhaps due to a permission problem? + if (mode != READ_WRITE) + throw new IllegalStateException( + "Cannot expunge READ_ONLY folder: " + fullName); + else + throw new MessagingException(cfx.getMessage(), cfx); + } catch (ConnectionException cex) { + throw new FolderClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + // Bad bad server .. + throw new MessagingException(pex.getMessage(), pex); + } finally { + doExpungeNotification = true; + } + + // Cleanup expunged messages and sync messageCache with reality. + if (msgs != null) + rmsgs = messageCache.removeExpungedMessages(msgs); + else + rmsgs = messageCache.removeExpungedMessages(); + if (uidTable != null) { + for (int i = 0; i < rmsgs.length; i++) { + IMAPMessage m = rmsgs[i]; + /* remove this message from the UIDTable */ + long uid = m.getUID(); + if (uid != -1) + uidTable.remove(Long.valueOf(uid)); + } + } + + // Update 'total' + total = messageCache.size(); + } + + // Notify listeners. This time its for real, guys. + if (rmsgs.length > 0) + notifyMessageRemovedListeners(true, rmsgs); + return rmsgs; + } + + /** + * Search whole folder for messages matching the given term. + * If the property <code>mail.imap.throwsearchexception</code> is true, + * and the search term is too complex for the IMAP protocol, + * SearchException is thrown. Otherwise, if the search term is too + * complex, <code>super.search</code> is called to do the search on + * the client. + * + * @param term the search term + * @return the messages that match + * @exception SearchException if mail.imap.throwsearchexception is + * true and the search is too complex for the IMAP protocol + * @exception MessagingException for other failures + */ + @Override + public synchronized Message[] search(SearchTerm term) + throws MessagingException { + checkOpened(); + + try { + Message[] matchMsgs = null; + + synchronized(messageCacheLock) { + int[] matches = getProtocol().search(term); + if (matches != null) + matchMsgs = getMessagesBySeqNumbers(matches); + } + return matchMsgs; + + } catch (CommandFailedException cfx) { + // unsupported charset or search criterion + return super.search(term); + } catch (SearchException sex) { + // too complex for IMAP + if (((IMAPStore)store).throwSearchException()) + throw sex; + return super.search(term); + } catch (ConnectionException cex) { + throw new FolderClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + // bug in our IMAP layer ? + throw new MessagingException(pex.getMessage(), pex); + } + } + + /** + * Search the folder for messages matching the given term. Returns + * array of matching messages. Returns an empty array if no matching + * messages are found. + */ + @Override + public synchronized Message[] search(SearchTerm term, Message[] msgs) + throws MessagingException { + checkOpened(); + + if (msgs.length == 0) + // need to return an empty array (not null!) + return msgs; + + try { + Message[] matchMsgs = null; + + synchronized(messageCacheLock) { + IMAPProtocol p = getProtocol(); + MessageSet[] ms = Utility.toMessageSetSorted(msgs, null); + if (ms == null) + throw new MessageRemovedException( + "Messages have been removed"); + int[] matches = p.search(ms, term); + if (matches != null) + matchMsgs = getMessagesBySeqNumbers(matches); + } + return matchMsgs; + + } catch (CommandFailedException cfx) { + // unsupported charset or search criterion + return super.search(term, msgs); + } catch (SearchException sex) { + // too complex for IMAP + return super.search(term, msgs); + } catch (ConnectionException cex) { + throw new FolderClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + // bug in our IMAP layer ? + throw new MessagingException(pex.getMessage(), pex); + } + } + + /** + * Sort the messages in the folder according to the sort criteria. + * The messages are returned in the sorted order, but the order of + * the messages in the folder is not changed. <p> + * + * Depends on the SORT extension - + * <A HREF="http://www.ietf.org/rfc/rfc5256.txt">RFC 5256</A>. + * + * @param term the SortTerms + * @return the messages in sorted order + * @exception MessagingException for failures + * @since JavaMail 1.4.4 + */ + public synchronized Message[] getSortedMessages(SortTerm[] term) + throws MessagingException { + return getSortedMessages(term, null); + } + + /** + * Sort the messages in the folder according to the sort criteria. + * The messages are returned in the sorted order, but the order of + * the messages in the folder is not changed. Only messages matching + * the search criteria are considered. <p> + * + * Depends on the SORT extension - + * <A HREF="http://www.ietf.org/rfc/rfc5256.txt">RFC 5256</A>. + * + * @param term the SortTerms + * @param sterm the SearchTerm + * @return the messages in sorted order + * @exception MessagingException for failures + * @since JavaMail 1.4.4 + */ + public synchronized Message[] getSortedMessages(SortTerm[] term, + SearchTerm sterm) throws MessagingException { + checkOpened(); + + try { + Message[] matchMsgs = null; + + synchronized(messageCacheLock) { + int[] matches = getProtocol().sort(term, sterm); + if (matches != null) + matchMsgs = getMessagesBySeqNumbers(matches); + } + return matchMsgs; + + } catch (CommandFailedException cfx) { + // unsupported charset or search criterion + throw new MessagingException(cfx.getMessage(), cfx); + } catch (SearchException sex) { + // too complex for IMAP + throw new MessagingException(sex.getMessage(), sex); + } catch (ConnectionException cex) { + throw new FolderClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + // bug in our IMAP layer ? + throw new MessagingException(pex.getMessage(), pex); + } + } + + /* + * Override Folder method to keep track of whether we have any + * message count listeners. Normally we won't have any, so we + * can avoid creating message objects to pass to the notify + * method. It's too hard to keep track of when all listeners + * are removed, and that's a rare case, so we don't try. + */ + @Override + public synchronized void addMessageCountListener(MessageCountListener l) { + super.addMessageCountListener(l); + hasMessageCountListener = true; + } + + /*********************************************************** + * UIDFolder interface methods + **********************************************************/ + + /** + * Returns the UIDValidity for this folder. + */ + @Override + public synchronized long getUIDValidity() throws MessagingException { + if (opened) // we already have this information + return uidvalidity; + + IMAPProtocol p = null; + Status status = null; + + try { + p = getStoreProtocol(); // XXX + String[] item = { "UIDVALIDITY" }; + status = p.status(fullName, item); + } catch (BadCommandException bex) { + // Probably a RFC1730 server + throw new MessagingException("Cannot obtain UIDValidity", bex); + } catch (ConnectionException cex) { + // Oops, the store or folder died on us. + throwClosedException(cex); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } finally { + releaseStoreProtocol(p); + } + + if (status == null) + throw new MessagingException("Cannot obtain UIDValidity"); + return status.uidvalidity; + } + + /** + * Returns the predicted UID that will be assigned to the + * next message that is appended to this folder. + * If the folder is closed, the STATUS command is used to + * retrieve this value. If the folder is open, the value + * returned from the SELECT or EXAMINE command is returned. + * Note that messages may have been appended to the folder + * while it was open and thus this value may be out of + * date. <p> + * + * Servers implementing RFC2060 likely won't return this value + * when a folder is opened. Servers implementing RFC3501 + * should return this value when a folder is opened. <p> + * + * @return the UIDNEXT value, or -1 if unknown + * @exception MessagingException for failures + * @since JavaMail 1.3.3 + */ + @Override + public synchronized long getUIDNext() throws MessagingException { + if (opened) // we already have this information + return uidnext; + + IMAPProtocol p = null; + Status status = null; + + try { + p = getStoreProtocol(); // XXX + String[] item = { "UIDNEXT" }; + status = p.status(fullName, item); + } catch (BadCommandException bex) { + // Probably a RFC1730 server + throw new MessagingException("Cannot obtain UIDNext", bex); + } catch (ConnectionException cex) { + // Oops, the store or folder died on us. + throwClosedException(cex); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } finally { + releaseStoreProtocol(p); + } + + if (status == null) + throw new MessagingException("Cannot obtain UIDNext"); + return status.uidnext; + } + + /** + * Get the Message corresponding to the given UID. + * If no such message exists, <code> null </code> is returned. + */ + @Override + public synchronized Message getMessageByUID(long uid) + throws MessagingException { + checkOpened(); // insure folder is open + + IMAPMessage m = null; + + try { + synchronized(messageCacheLock) { + Long l = Long.valueOf(uid); + + if (uidTable != null) { + // Check in uidTable + m = uidTable.get(l); + if (m != null) // found it + return m; + } else + uidTable = new Hashtable<>(); + + // Check with the server + // Issue UID FETCH command + getProtocol().fetchSequenceNumber(uid); + + if (uidTable != null) { + // Check in uidTable + m = uidTable.get(l); + if (m != null) // found it + return m; + } + } + } catch(ConnectionException cex) { + throw new FolderClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + + return m; + } + + /** + * Get the Messages specified by the given range. <p> + * Returns Message objects for all valid messages in this range. + * Returns an empty array if no messages are found. + */ + @Override + public synchronized Message[] getMessagesByUID(long start, long end) + throws MessagingException { + checkOpened(); // insure that folder is open + + Message[] msgs; // array of messages to be returned + + try { + synchronized(messageCacheLock) { + if (uidTable == null) + uidTable = new Hashtable<>(); + + // Issue UID FETCH for given range + long[] ua = getProtocol().fetchSequenceNumbers(start, end); + + List<Message> ma = new ArrayList<>(); + // NOTE: Below must be within messageCacheLock region + for (int i = 0; i < ua.length; i++) { + Message m = uidTable.get(Long.valueOf(ua[i])); + if (m != null) // found it + ma.add(m); + } + msgs = ma.toArray(new Message[ma.size()]); + } + } catch(ConnectionException cex) { + throw new FolderClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + + return msgs; + } + + /** + * Get the Messages specified by the given array. <p> + * + * <code>uids.length()</code> elements are returned. + * If any UID in the array is invalid, a <code>null</code> entry + * is returned for that element. + */ + @Override + public synchronized Message[] getMessagesByUID(long[] uids) + throws MessagingException { + checkOpened(); // insure that folder is open + + try { + synchronized(messageCacheLock) { + long[] unavailUids = uids; + if (uidTable != null) { + // to collect unavailable UIDs + List<Long> v = new ArrayList<>(); + for (long uid : uids) { + if (!uidTable.containsKey(uid)) { + // This UID has not been loaded yet. + v.add(uid); + } + } + + int vsize = v.size(); + unavailUids = new long[vsize]; + for (int i = 0; i < vsize; i++) { + unavailUids[i] = v.get(i); + } + } else + uidTable = new Hashtable<>(); + + if (unavailUids.length > 0) { + // Issue UID FETCH request for given uids + getProtocol().fetchSequenceNumbers(unavailUids); + } + + // Return array of size = uids.length + Message[] msgs = new Message[uids.length]; + for (int i = 0; i < uids.length; i++) + msgs[i] = (Message)uidTable.get(Long.valueOf(uids[i])); + return msgs; + } + } catch(ConnectionException cex) { + throw new FolderClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + } + + /** + * Get the UID for the specified message. + */ + @Override + public synchronized long getUID(Message message) + throws MessagingException { + if (message.getFolder() != this) + throw new NoSuchElementException( + "Message does not belong to this folder"); + + checkOpened(); // insure that folder is open + + if (!(message instanceof IMAPMessage)) + throw new MessagingException("message is not an IMAPMessage"); + IMAPMessage m = (IMAPMessage)message; + // If the message already knows its UID, great .. + long uid; + if ((uid = m.getUID()) != -1) + return uid; + + synchronized(messageCacheLock) { // Acquire Lock + try { + IMAPProtocol p = getProtocol(); + m.checkExpunged(); // insure that message is not expunged + UID u = p.fetchUID(m.getSequenceNumber()); + + if (u != null) { + uid = u.uid; + m.setUID(uid); // set message's UID + + // insert this message into uidTable + if (uidTable == null) + uidTable = new Hashtable<>(); + uidTable.put(Long.valueOf(uid), m); + } + } catch (ConnectionException cex) { + throw new FolderClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + } + + return uid; + } + + /** + * Servers that support the UIDPLUS extension + * (<A HREF="http://www.ietf.org/rfc/rfc4315.txt">RFC 4315</A>) + * may indicate that this folder does not support persistent UIDs; + * that is, UIDVALIDITY will be different each time the folder is + * opened. Only valid when the folder is open. + * + * @return true if UIDs are not sticky + * @exception MessagingException for failures + * @exception IllegalStateException if the folder isn't open + * @see "RFC 4315" + * @since JavaMail 1.6.0 + */ + public synchronized boolean getUIDNotSticky() throws MessagingException { + checkOpened(); + return uidNotSticky; + } + + /** + * Get or create Message objects for the UIDs. + */ + private Message[] createMessagesForUIDs(long[] uids) { + IMAPMessage[] msgs = new IMAPMessage[uids.length]; + for (int i = 0; i < uids.length; i++) { + IMAPMessage m = null; + if (uidTable != null) + m = uidTable.get(Long.valueOf(uids[i])); + if (m == null) { + // fake it, we don't know what message this really is + m = newIMAPMessage(-1); // no sequence number + m.setUID(uids[i]); + m.setExpunged(true); + } + msgs[i++] = m; + } + return msgs; + } + + /** + * Returns the HIGHESTMODSEQ for this folder. + * + * @return the HIGHESTMODSEQ value + * @exception MessagingException for failures + * @see "RFC 4551" + * @since JavaMail 1.5.1 + */ + public synchronized long getHighestModSeq() throws MessagingException { + if (opened) // we already have this information + return highestmodseq; + + IMAPProtocol p = null; + Status status = null; + + try { + p = getStoreProtocol(); // XXX + if (!p.hasCapability("CONDSTORE")) + throw new BadCommandException("CONDSTORE not supported"); + String[] item = { "HIGHESTMODSEQ" }; + status = p.status(fullName, item); + } catch (BadCommandException bex) { + // Probably a RFC1730 server + throw new MessagingException("Cannot obtain HIGHESTMODSEQ", bex); + } catch (ConnectionException cex) { + // Oops, the store or folder died on us. + throwClosedException(cex); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } finally { + releaseStoreProtocol(p); + } + + if (status == null) + throw new MessagingException("Cannot obtain HIGHESTMODSEQ"); + return status.highestmodseq; + } + + /** + * Get the messages that have been changed since the given MODSEQ value. + * Also, prefetch the flags for the messages. <p> + * + * The server must support the CONDSTORE extension. + * + * @param start the first message number + * @param end the last message number + * @param modseq the MODSEQ value + * @return the changed messages + * @exception MessagingException for failures + * @see "RFC 4551" + * @since JavaMail 1.5.1 + */ + public synchronized Message[] getMessagesByUIDChangedSince( + long start, long end, long modseq) + throws MessagingException { + checkOpened(); // insure that folder is open + + try { + synchronized (messageCacheLock) { + IMAPProtocol p = getProtocol(); + if (!p.hasCapability("CONDSTORE")) + throw new BadCommandException("CONDSTORE not supported"); + + // Issue FETCH for given range + int[] nums = p.uidfetchChangedSince(start, end, modseq); + return getMessagesBySeqNumbers(nums); + } + } catch(ConnectionException cex) { + throw new FolderClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + } + + /** + * Get the quotas for the quotaroot associated with this + * folder. Note that many folders may have the same quotaroot. + * Quotas are controlled on the basis of a quotaroot, not + * (necessarily) a folder. The relationship between folders + * and quotaroots depends on the IMAP server. Some servers + * might implement a single quotaroot for all folders owned by + * a user. Other servers might implement a separate quotaroot + * for each folder. A single folder can even have multiple + * quotaroots, perhaps controlling quotas for different + * resources. + * + * @return array of Quota objects for the quotaroots associated with + * this folder + * @exception MessagingException if the server doesn't support the + * QUOTA extension + */ + public Quota[] getQuota() throws MessagingException { + return (Quota[])doOptionalCommand("QUOTA not supported", + new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) + throws ProtocolException { + return p.getQuotaRoot(fullName); + } + }); + } + + /** + * Set the quotas for the quotaroot specified in the quota argument. + * Typically this will be one of the quotaroots associated with this + * folder, as obtained from the <code>getQuota</code> method, but it + * need not be. + * + * @param quota the quota to set + * @exception MessagingException if the server doesn't support the + * QUOTA extension + */ + public void setQuota(final Quota quota) throws MessagingException { + doOptionalCommand("QUOTA not supported", + new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) + throws ProtocolException { + p.setQuota(quota); + return null; + } + }); + } + + /** + * Get the access control list entries for this folder. + * + * @return array of access control list entries + * @exception MessagingException if the server doesn't support the + * ACL extension + */ + public ACL[] getACL() throws MessagingException { + return (ACL[])doOptionalCommand("ACL not supported", + new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) + throws ProtocolException { + return p.getACL(fullName); + } + }); + } + + /** + * Add an access control list entry to the access control list + * for this folder. + * + * @param acl the access control list entry to add + * @exception MessagingException if the server doesn't support the + * ACL extension + */ + public void addACL(ACL acl) throws MessagingException { + setACL(acl, '\0'); + } + + /** + * Remove any access control list entry for the given identifier + * from the access control list for this folder. + * + * @param name the identifier for which to remove all ACL entries + * @exception MessagingException if the server doesn't support the + * ACL extension + */ + public void removeACL(final String name) throws MessagingException { + doOptionalCommand("ACL not supported", + new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) + throws ProtocolException { + p.deleteACL(fullName, name); + return null; + } + }); + } + + /** + * Add the rights specified in the ACL to the entry for the + * identifier specified in the ACL. If an entry for the identifier + * doesn't already exist, add one. + * + * @param acl the identifer and rights to add + * @exception MessagingException if the server doesn't support the + * ACL extension + */ + public void addRights(ACL acl) throws MessagingException { + setACL(acl, '+'); + } + + /** + * Remove the rights specified in the ACL from the entry for the + * identifier specified in the ACL. + * + * @param acl the identifer and rights to remove + * @exception MessagingException if the server doesn't support the + * ACL extension + */ + public void removeRights(ACL acl) throws MessagingException { + setACL(acl, '-'); + } + + /** + * Get all the rights that may be allowed to the given identifier. + * Rights are grouped per RFC 2086 and each group is returned as an + * element of the array. The first element of the array is the set + * of rights that are always granted to the identifier. Later + * elements are rights that may be optionally granted to the + * identifier. <p> + * + * Note that this method lists the rights that it is possible to + * assign to the given identifier, <em>not</em> the rights that are + * actually granted to the given identifier. For the latter, see + * the <code>getACL</code> method. + * + * @param name the identifier to list rights for + * @return array of Rights objects representing possible + * rights for the identifier + * @exception MessagingException if the server doesn't support the + * ACL extension + */ + public Rights[] listRights(final String name) throws MessagingException { + return (Rights[])doOptionalCommand("ACL not supported", + new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) + throws ProtocolException { + return p.listRights(fullName, name); + } + }); + } + + /** + * Get the rights allowed to the currently authenticated user. + * + * @return the rights granted to the current user + * @exception MessagingException if the server doesn't support the + * ACL extension + */ + public Rights myRights() throws MessagingException { + return (Rights)doOptionalCommand("ACL not supported", + new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) + throws ProtocolException { + return p.myRights(fullName); + } + }); + } + + private void setACL(final ACL acl, final char mod) + throws MessagingException { + doOptionalCommand("ACL not supported", + new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) + throws ProtocolException { + p.setACL(fullName, mod, acl); + return null; + } + }); + } + + /** + * Get the attributes that the IMAP server returns with the + * LIST response. + * + * @return array of attributes for this folder + * @exception MessagingException for failures + * @since JavaMail 1.3.3 + */ + public synchronized String[] getAttributes() throws MessagingException { + checkExists(); + if (attributes == null) + exists(); // do a LIST to set the attributes + return attributes == null ? new String[0] : attributes.clone(); + } + + /** + * Use the IMAP IDLE command (see + * <A HREF="http://www.ietf.org/rfc/rfc2177.txt">RFC 2177</A>), + * if supported by the server, to enter idle mode so that the server + * can send unsolicited notifications of new messages arriving, etc. + * without the need for the client to constantly poll the server. + * Use an appropriate listener to be notified of new messages or + * other events. When another thread (e.g., the listener thread) + * needs to issue an IMAP comand for this folder, the idle mode will + * be terminated and this method will return. Typically the caller + * will invoke this method in a loop. <p> + * + * The mail.imap.minidletime property enforces a minimum delay + * before returning from this method, to ensure that other threads + * have a chance to issue commands before the caller invokes this + * method again. The default delay is 10 milliseconds. + * + * @exception MessagingException if the server doesn't support the + * IDLE extension + * @exception IllegalStateException if the folder isn't open + * + * @since JavaMail 1.4.1 + */ + public void idle() throws MessagingException { + idle(false); + } + + /** + * Like {@link #idle}, but if <code>once</code> is true, abort the + * IDLE command after the first notification, to allow the caller + * to process any notification synchronously. + * + * @param once only do one notification? + * @exception MessagingException if the server doesn't support the + * IDLE extension + * @exception IllegalStateException if the folder isn't open + * + * @since JavaMail 1.4.3 + */ + public void idle(boolean once) throws MessagingException { + synchronized (this) { + /* + * We can't support the idle method if we're using SocketChannels + * because SocketChannels don't allow simultaneous read and write. + * If we're blocked in a read waiting for IDLE responses, we can't + * send the DONE message to abort the IDLE. Sigh. + * XXX - We could do select here too, like IdleManager, instead + * of blocking in read, but that's more complicated. + */ + if (protocol != null && protocol.getChannel() != null) + throw new MessagingException( + "idle method not supported with SocketChannels"); + } + if (!startIdle(null)) + return; + + /* + * We gave up the folder lock so that other threads + * can get into the folder far enough to see that we're + * in IDLE and abort the IDLE. + * + * Now we read responses from the IDLE command, especially + * including unsolicited notifications from the server. + * We don't hold the messageCacheLock while reading because + * it protects the idleState and other threads need to be + * able to examine the state. + * + * The messageCacheLock is held in handleIdle while processing + * the responses so that we can update the number of messages + * in the folder (for example). + */ + for (;;) { + if (!handleIdle(once)) + break; + } + + /* + * Enforce a minimum delay to give time to threads + * processing the responses that came in while we + * were idle. + */ + int minidle = ((IMAPStore)store).getMinIdleTime(); + if (minidle > 0) { + try { + Thread.sleep(minidle); + } catch (InterruptedException ex) { + // restore the interrupted state, which callers might depend on + Thread.currentThread().interrupt(); + } + } + } + + /** + * Start the IDLE command and put this folder into the IDLE state. + * IDLE processing is done later in handleIdle(), e.g., called from + * the IdleManager. + * + * @return true if IDLE started, false otherwise + * @exception MessagingException if the server doesn't support the + * IDLE extension + * @exception IllegalStateException if the folder isn't open + * @since JavaMail 1.5.2 + */ + boolean startIdle(final IdleManager im) throws MessagingException { + // ASSERT: Must NOT be called with this folder's + // synchronization lock held. + assert !Thread.holdsLock(this); + synchronized(this) { + checkOpened(); + if (im != null && idleManager != null && im != idleManager) + throw new MessagingException( + "Folder already being watched by another IdleManager"); + Boolean started = (Boolean)doOptionalCommand("IDLE not supported", + new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) + throws ProtocolException { + // if the IdleManager is already watching this folder, + // there's nothing to do here + if (idleState == IDLE && + im != null && im == idleManager) + return Boolean.TRUE; // already watching it + if (idleState == RUNNING) { + p.idleStart(); + logger.finest("startIdle: set to IDLE"); + idleState = IDLE; + idleManager = im; + return Boolean.TRUE; + } else { + // some other thread must be running the IDLE + // command, we'll just wait for it to finish + // without aborting it ourselves + try { + // give up lock and wait to be not idle + messageCacheLock.wait(); + } catch (InterruptedException ex) { + // restore the interrupted state, which callers + // might depend on + Thread.currentThread().interrupt(); + } + return Boolean.FALSE; + } + } + }); + logger.log(Level.FINEST, "startIdle: return {0}", started); + return started.booleanValue(); + } + } + + /** + * Read a response from the server while we're in the IDLE state. + * We hold the messageCacheLock while processing the + * responses so that we can update the number of messages + * in the folder (for example). + * + * @param once only do one notification? + * @return true if we should look for more IDLE responses, + * false if IDLE is done + * @exception MessagingException for errors + * @since JavaMail 1.5.2 + */ + boolean handleIdle(boolean once) throws MessagingException { + Response r = null; + do { + r = protocol.readIdleResponse(); + try { + synchronized (messageCacheLock) { + if (r.isBYE() && r.isSynthetic() && idleState == IDLE) { + /* + * If it was a timeout and no bytes were transferred + * we ignore it and go back and read again. + * If the I/O was otherwise interrupted, and no + * bytes were transferred, we take it as a request + * to abort the IDLE. + */ + Exception ex = r.getException(); + if (ex instanceof InterruptedIOException && + ((InterruptedIOException)ex). + bytesTransferred == 0) { + if (ex instanceof SocketTimeoutException) { + logger.finest( + "handleIdle: ignoring socket timeout"); + r = null; // repeat do/while loop + } else { + logger.finest("handleIdle: interrupting IDLE"); + IdleManager im = idleManager; + if (im != null) { + logger.finest( + "handleIdle: request IdleManager to abort"); + im.requestAbort(this); + } else { + logger.finest("handleIdle: abort IDLE"); + protocol.idleAbort(); + idleState = ABORTING; + } + // normally will exit the do/while loop + } + continue; + } + } + boolean done = true; + try { + if (protocol == null || + !protocol.processIdleResponse(r)) + return false; // done + done = false; + } finally { + if (done) { + logger.finest("handleIdle: set to RUNNING"); + idleState = RUNNING; + idleManager = null; + messageCacheLock.notifyAll(); + } + } + if (once) { + if (idleState == IDLE) { + try { + protocol.idleAbort(); + } catch (Exception ex) { + // ignore any failures, still have to abort. + // connection failures will be detected above + // in the call to readIdleResponse. + } + idleState = ABORTING; + } + } + } + } catch (ConnectionException cex) { + // Oops, the folder died on us. + throw new FolderClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + // keep processing responses already in our buffer + } while (r == null || protocol.hasResponse()); + return true; + } + + /* + * If an IDLE command is in progress, abort it if necessary, + * and wait until it completes. + * ASSERT: Must be called with the message cache lock held. + */ + void waitIfIdle() throws ProtocolException { + assert Thread.holdsLock(messageCacheLock); + while (idleState != RUNNING) { + if (idleState == IDLE) { + IdleManager im = idleManager; + if (im != null) { + logger.finest("waitIfIdle: request IdleManager to abort"); + im.requestAbort(this); + } else { + logger.finest("waitIfIdle: abort IDLE"); + protocol.idleAbort(); + idleState = ABORTING; + } + } else + logger.log(Level.FINEST, "waitIfIdle: idleState {0}", idleState); + try { + // give up lock and wait to be not idle + if (logger.isLoggable(Level.FINEST)) + logger.finest("waitIfIdle: wait to be not idle: " + + Thread.currentThread()); + messageCacheLock.wait(); + if (logger.isLoggable(Level.FINEST)) + logger.finest("waitIfIdle: wait done, idleState " + + idleState + ": " + Thread.currentThread()); + } catch (InterruptedException ex) { + // restore the interrupted state, which callers might depend on + Thread.currentThread().interrupt(); + // If someone is trying to interrupt us we can't keep going + // around the loop waiting for IDLE to complete, but we can't + // just return because callers expect the idleState to be + // RUNNING when we return. Throwing this exception seems + // like the best choice. + throw new ProtocolException("Interrupted waitIfIdle", ex); + } + } + } + + /* + * Send the DONE command that aborts the IDLE; used by IdleManager. + */ + void idleAbort() { + synchronized (messageCacheLock) { + if (idleState == IDLE && protocol != null) { + protocol.idleAbort(); + idleState = ABORTING; + } + } + } + + /* + * Send the DONE command that aborts the IDLE and wait for the response; + * used by IdleManager. + */ + void idleAbortWait() { + synchronized (messageCacheLock) { + if (idleState == IDLE && protocol != null) { + protocol.idleAbort(); + idleState = ABORTING; + + // read responses until OK or connection failure + try { + for (;;) { + if (!handleIdle(false)) + break; + } + } catch (Exception ex) { + // assume it's a connection failure; nothing more to do + logger.log(Level.FINEST, "Exception in idleAbortWait", ex); + } + logger.finest("IDLE aborted"); + } + } + } + + /** + * Return the SocketChannel for this connection, if any, for use + * in IdleManager. + */ + SocketChannel getChannel() { + return protocol != null ? protocol.getChannel() : null; + } + + /** + * Send the IMAP ID command (if supported by the server) and return + * the result from the server. The ID command identfies the client + * to the server and returns information about the server to the client. + * See <A HREF="http://www.ietf.org/rfc/rfc2971.txt">RFC 2971</A>. + * The returned Map is unmodifiable. + * + * @param clientParams a Map of keys and values identifying the client + * @return a Map of keys and values identifying the server + * @exception MessagingException if the server doesn't support the + * ID extension + * @since JavaMail 1.5.1 + */ + @SuppressWarnings("unchecked") + public Map<String, String> id(final Map<String, String> clientParams) + throws MessagingException { + checkOpened(); + return (Map<String,String>)doOptionalCommand("ID not supported", + new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) + throws ProtocolException { + return p.id(clientParams); + } + }); + } + + /** + * Use the IMAP STATUS command to get the indicated item. + * The STATUS item may be a standard item such as "RECENT" or "UNSEEN", + * or may be a server-specific item. + * The folder must be closed. If the item is not found, or the + * folder is open, -1 is returned. + * + * @param item the STATUS item to fetch + * @return the value of the STATUS item, or -1 + * @exception MessagingException for errors + * @since JavaMail 1.5.2 + */ + public synchronized long getStatusItem(String item) + throws MessagingException { + if (!opened) { + checkExists(); + + IMAPProtocol p = null; + Status status = null; + try { + p = getStoreProtocol(); // XXX + String[] items = { item }; + status = p.status(fullName, items); + return status != null ? status.getItem(item) : -1; + } catch (BadCommandException bex) { + // doesn't support STATUS, probably vanilla IMAP4 .. + // Could EXAMINE, SEARCH for UNREAD messages and + // return the count .. bah, not worth it. + return -1; + } catch (ConnectionException cex) { + throw new StoreClosedException(store, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } finally { + releaseStoreProtocol(p); + } + } + return -1; + } + + /** + * The response handler. This is the callback routine that is + * invoked by the protocol layer. + */ + /* + * ASSERT: This method must be called only when holding the + * messageCacheLock. + * ASSERT: This method must *not* invoke any other method that + * might grab the 'folder' lock or 'message' lock (i.e., any + * synchronized methods on IMAPFolder or IMAPMessage) + * since that will result in violating the locking hierarchy. + */ + @Override + public void handleResponse(Response r) { + assert Thread.holdsLock(messageCacheLock); + + /* + * First, delegate possible ALERT or notification to the Store. + */ + if (r.isOK() || r.isNO() || r.isBAD() || r.isBYE()) + ((IMAPStore)store).handleResponseCode(r); + + /* + * Now check whether this is a BYE or OK response and + * handle appropriately. + */ + if (r.isBYE()) { + if (opened) // XXX - accessed without holding folder lock + cleanup(false); + return; + } else if (r.isOK()) { + // HIGHESTMODSEQ can be updated on any OK response + r.skipSpaces(); + if (r.readByte() == '[') { + String s = r.readAtom(); + if (s.equalsIgnoreCase("HIGHESTMODSEQ")) + highestmodseq = r.readLong(); + } + r.reset(); + return; + } else if (!r.isUnTagged()) { + return; // might be a continuation for IDLE + } + + /* Now check whether this is an IMAP specific response */ + if (!(r instanceof IMAPResponse)) { + // Probably a bug in our code ! + // XXX - should be an assert + logger.fine("UNEXPECTED RESPONSE : " + r.toString()); + return; + } + + IMAPResponse ir = (IMAPResponse)r; + + if (ir.keyEquals("EXISTS")) { // EXISTS + int exists = ir.getNumber(); + if (exists <= realTotal) + // Could be the EXISTS following EXPUNGE, ignore 'em + return; + + int count = exists - realTotal; // number of new messages + Message[] msgs = new Message[count]; + + // Add 'count' new IMAPMessage objects into the messageCache + messageCache.addMessages(count, realTotal + 1); + int oldtotal = total; // used in loop below + realTotal += count; + total += count; + + // avoid instantiating Message objects if no listeners. + if (hasMessageCountListener) { + for (int i = 0; i < count; i++) + msgs[i] = messageCache.getMessage(++oldtotal); + + // Notify listeners. + notifyMessageAddedListeners(msgs); + } + + } else if (ir.keyEquals("EXPUNGE")) { + // EXPUNGE response. + + int seqnum = ir.getNumber(); + if (seqnum > realTotal) { + // A message was expunged that we never knew about. + // Exchange will do this. Just ignore the notification. + // (Alternatively, we could simulate an EXISTS for the + // expunged message before expunging it.) + return; + } + Message[] msgs = null; + if (doExpungeNotification && hasMessageCountListener) { + // save the Message object first; can't look it + // up after it's expunged + msgs = new Message[] { getMessageBySeqNumber(seqnum) }; + if (msgs[0] == null) // XXX - should never happen + msgs = null; + } + + messageCache.expungeMessage(seqnum); + + // decrement 'realTotal'; but leave 'total' unchanged + realTotal--; + + if (msgs != null) // Do the notification here. + notifyMessageRemovedListeners(false, msgs); + + } else if (ir.keyEquals("VANISHED")) { + // after the folder is opened with QRESYNC, a VANISHED response + // without the (EARLIER) tag is used instead of the EXPUNGE + // response + + // "VANISHED" SP ["(EARLIER)"] SP known-uids + String[] s = ir.readAtomStringList(); + if (s == null) { // no (EARLIER) + String uids = ir.readAtom(); + UIDSet[] uidset = UIDSet.parseUIDSets(uids); + // assume no duplicates and no UIDs out of range + realTotal -= UIDSet.size(uidset); + long[] luid = UIDSet.toArray(uidset); + Message[] msgs = createMessagesForUIDs(luid); + for (Message m : msgs) { + if (m.getMessageNumber() > 0) + messageCache.expungeMessage(m.getMessageNumber()); + } + if (doExpungeNotification && hasMessageCountListener) { + notifyMessageRemovedListeners(true, msgs); + } + } // else if (EARLIER), ignore + + } else if (ir.keyEquals("FETCH")) { + assert ir instanceof FetchResponse : "!ir instanceof FetchResponse"; + Message msg = processFetchResponse((FetchResponse)ir); + if (msg != null) + notifyMessageChangedListeners( + MessageChangedEvent.FLAGS_CHANGED, msg); + + } else if (ir.keyEquals("RECENT")) { + // update 'recent' + recent = ir.getNumber(); + } + } + + /** + * Process a FETCH response. + * The only unsolicited FETCH response that makes sense + * to me (for now) is FLAGS updates, which might include + * UID and MODSEQ information. Ignore any other junk. + */ + private Message processFetchResponse(FetchResponse fr) { + IMAPMessage msg = getMessageBySeqNumber(fr.getNumber()); + if (msg != null) { // should always be true + boolean notify = false; + + UID uid = fr.getItem(UID.class); + if (uid != null && msg.getUID() != uid.uid) { + msg.setUID(uid.uid); + if (uidTable == null) + uidTable = new Hashtable<>(); + uidTable.put(Long.valueOf(uid.uid), msg); + notify = true; + } + + MODSEQ modseq = fr.getItem(MODSEQ.class); + if (modseq != null && msg._getModSeq() != modseq.modseq) { + msg.setModSeq(modseq.modseq); + /* + * XXX - should we update the folder's HIGHESTMODSEQ or not? + * + if (modseq.modseq > highestmodseq) + highestmodseq = modseq.modseq; + */ + notify = true; + } + + // Get FLAGS response, if present + FLAGS flags = fr.getItem(FLAGS.class); + if (flags != null) { + msg._setFlags(flags); // assume flags changed + notify = true; + } + + // handle any extension items that might've changed + // XXX - no notifications associated with extension items + msg.handleExtensionFetchItems(fr.getExtensionItems()); + + if (!notify) + msg = null; + } + return msg; + } + + /** + * Handle the given array of Responses. + * + * ASSERT: This method must be called only when holding the + * messageCacheLock + */ + void handleResponses(Response[] r) { + for (int i = 0; i < r.length; i++) { + if (r[i] != null) + handleResponse(r[i]); + } + } + + /** + * Get this folder's Store's protocol connection. + * + * When acquiring a store protocol object, it is important to + * use the following steps: + * + * <blockquote><pre> + * IMAPProtocol p = null; + * try { + * p = getStoreProtocol(); + * // perform the command + * } catch (WhateverException ex) { + * // handle it + * } finally { + * releaseStoreProtocol(p); + * } + * </pre></blockquote> + * + * ASSERT: Must be called with this folder's synchronization lock held. + * + * @return the IMAPProtocol for the Store's connection + * @exception ProtocolException for protocol errors + */ + protected synchronized IMAPProtocol getStoreProtocol() + throws ProtocolException { + connectionPoolLogger.fine("getStoreProtocol() borrowing a connection"); + return ((IMAPStore)store).getFolderStoreProtocol(); + } + + /** + * Throw the appropriate 'closed' exception. + * + * @param cex the ConnectionException + * @exception FolderClosedException if the folder is closed + * @exception StoreClosedException if the store is closed + */ + protected synchronized void throwClosedException(ConnectionException cex) + throws FolderClosedException, StoreClosedException { + // If it's the folder's protocol object, throw a FolderClosedException; + // otherwise, throw a StoreClosedException. + // If a command has failed because the connection is closed, + // the folder will have already been forced closed by the + // time we get here and our protocol object will have been + // released, so if we no longer have a protocol object we base + // this decision on whether we *think* the folder is open. + if ((protocol != null && cex.getProtocol() == protocol) || + (protocol == null && !reallyClosed)) + throw new FolderClosedException(this, cex.getMessage()); + else + throw new StoreClosedException(store, cex.getMessage()); + } + + /** + * Return the IMAPProtocol object for this folder. <p> + * + * This method will block if necessary to wait for an IDLE + * command to finish. + * + * @return the IMAPProtocol object used when the folder is open + * @exception ProtocolException for protocol errors + */ + protected IMAPProtocol getProtocol() throws ProtocolException { + assert Thread.holdsLock(messageCacheLock); + waitIfIdle(); + // if we no longer have a protocol object after waiting, it probably + // means the connection has been closed due to a communnication error, + // or possibly because the folder has been closed + if (protocol == null) + throw new ConnectionException("Connection closed"); + return protocol; + } + + /** + * A simple interface for user-defined IMAP protocol commands. + */ + public static interface ProtocolCommand { + /** + * Execute the user-defined command using the supplied IMAPProtocol + * object. + * + * @param protocol the IMAPProtocol for the connection + * @return the results of the command + * @exception ProtocolException for protocol errors + */ + public Object doCommand(IMAPProtocol protocol) throws ProtocolException; + } + + /** + * Execute a user-supplied IMAP command. The command is executed + * in the appropriate context with the necessary locks held and + * using the appropriate <code>IMAPProtocol</code> object. <p> + * + * This method returns whatever the <code>ProtocolCommand</code> + * object's <code>doCommand</code> method returns. If the + * <code>doCommand</code> method throws a <code>ConnectionException</code> + * it is translated into a <code>StoreClosedException</code> or + * <code>FolderClosedException</code> as appropriate. If the + * <code>doCommand</code> method throws a <code>ProtocolException</code> + * it is translated into a <code>MessagingException</code>. <p> + * + * The following example shows how to execute the IMAP NOOP command. + * Executing more complex IMAP commands requires intimate knowledge + * of the <code>com.sun.mail.iap</code> and + * <code>com.sun.mail.imap.protocol</code> packages, best acquired by + * reading the source code. + * + * <blockquote><pre> + * import com.sun.mail.iap.*; + * import com.sun.mail.imap.*; + * import com.sun.mail.imap.protocol.*; + * + * ... + * + * IMAPFolder f = (IMAPFolder)folder; + * Object val = f.doCommand(new IMAPFolder.ProtocolCommand() { + * public Object doCommand(IMAPProtocol p) + * throws ProtocolException { + * p.simpleCommand("NOOP", null); + * return null; + * } + * }); + * </pre></blockquote> + * <p> + * + * Here's a more complex example showing how to use the proposed + * IMAP SORT extension: + * + * <blockquote><pre> + * import com.sun.mail.iap.*; + * import com.sun.mail.imap.*; + * import com.sun.mail.imap.protocol.*; + * + * ... + * + * IMAPFolder f = (IMAPFolder)folder; + * Object val = f.doCommand(new IMAPFolder.ProtocolCommand() { + * public Object doCommand(IMAPProtocol p) + * throws ProtocolException { + * // Issue command + * Argument args = new Argument(); + * Argument list = new Argument(); + * list.writeString("SUBJECT"); + * args.writeArgument(list); + * args.writeString("UTF-8"); + * args.writeString("ALL"); + * Response[] r = p.command("SORT", args); + * Response response = r[r.length-1]; + * + * // Grab response + * Vector v = new Vector(); + * if (response.isOK()) { // command succesful + * for (int i = 0, len = r.length; i < len; i++) { + * if (!(r[i] instanceof IMAPResponse)) + * continue; + * + * IMAPResponse ir = (IMAPResponse)r[i]; + * if (ir.keyEquals("SORT")) { + * String num; + * while ((num = ir.readAtomString()) != null) + * System.out.println(num); + * r[i] = null; + * } + * } + * } + * + * // dispatch remaining untagged responses + * p.notifyResponseHandlers(r); + * p.handleResult(response); + * + * return null; + * } + * }); + * </pre></blockquote> + * + * @param cmd the protocol command + * @return the result of the command + * @exception MessagingException for failures + */ + public Object doCommand(ProtocolCommand cmd) throws MessagingException { + try { + return doProtocolCommand(cmd); + } catch (ConnectionException cex) { + // Oops, the store or folder died on us. + throwClosedException(cex); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + return null; + } + + public Object doOptionalCommand(String err, ProtocolCommand cmd) + throws MessagingException { + try { + return doProtocolCommand(cmd); + } catch (BadCommandException bex) { + throw new MessagingException(err, bex); + } catch (ConnectionException cex) { + // Oops, the store or folder died on us. + throwClosedException(cex); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + return null; + } + + public Object doCommandIgnoreFailure(ProtocolCommand cmd) + throws MessagingException { + try { + return doProtocolCommand(cmd); + } catch (CommandFailedException cfx) { + return null; + } catch (ConnectionException cex) { + // Oops, the store or folder died on us. + throwClosedException(cex); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + return null; + } + + protected synchronized Object doProtocolCommand(ProtocolCommand cmd) + throws ProtocolException { + /* + * Check whether we have a protocol object, not whether we're + * opened, to allow use of the exsting protocol object in the + * open method before the state is changed to "opened". + */ + if (protocol != null) { + synchronized (messageCacheLock) { + return cmd.doCommand(getProtocol()); + } + } + + // only get here if using store's connection + IMAPProtocol p = null; + + try { + p = getStoreProtocol(); + return cmd.doCommand(p); + } finally { + releaseStoreProtocol(p); + } + } + + /** + * Release the store protocol object. If we borrowed a protocol + * object from the connection pool, give it back. If we used our + * own protocol object, nothing to do. + * + * ASSERT: Must be called with this folder's synchronization lock held. + * + * @param p the IMAPProtocol object + */ + protected synchronized void releaseStoreProtocol(IMAPProtocol p) { + if (p != protocol) + ((IMAPStore)store).releaseFolderStoreProtocol(p); + else { + // XXX - should never happen + logger.fine("releasing our protocol as store protocol?"); + } + } + + /** + * Release the protocol object. + * + * ASSERT: This method must be called only when holding the + * messageCacheLock + * + * @param returnToPool return the protocol object to the pool? + */ + protected void releaseProtocol(boolean returnToPool) { + if (protocol != null) { + protocol.removeResponseHandler(this); + + if (returnToPool) + ((IMAPStore)store).releaseProtocol(this, protocol); + else { + protocol.disconnect(); // make sure it's disconnected + ((IMAPStore)store).releaseProtocol(this, null); + } + protocol = null; + } + } + + /** + * Issue a noop command for the connection if the connection has not been + * used in more than a second. If <code>keepStoreAlive</code> is true, + * also issue a noop over the store's connection. + * + * ASSERT: This method must be called only when holding the + * messageCacheLock + * + * @param keepStoreAlive keep the Store alive too? + * @exception ProtocolException for protocol errors + */ + protected void keepConnectionAlive(boolean keepStoreAlive) + throws ProtocolException { + + assert Thread.holdsLock(messageCacheLock); + if (protocol == null) // in case connection was closed + return; + if (System.currentTimeMillis() - protocol.getTimestamp() > 1000) { + waitIfIdle(); + if (protocol != null) + protocol.noop(); + } + + if (keepStoreAlive && ((IMAPStore)store).hasSeparateStoreConnection()) { + IMAPProtocol p = null; + try { + p = ((IMAPStore)store).getFolderStoreProtocol(); + if (System.currentTimeMillis() - p.getTimestamp() > 1000) + p.noop(); + } finally { + ((IMAPStore)store).releaseFolderStoreProtocol(p); + } + } + } + + /** + * Get the message object for the given sequence number. If + * none found, null is returned. + * + * ASSERT: This method must be called only when holding the + * messageCacheLock + * + * @param seqnum the message sequence number + * @return the IMAPMessage object + */ + protected IMAPMessage getMessageBySeqNumber(int seqnum) { + if (seqnum > messageCache.size()) { + // Microsoft Exchange will sometimes return message + // numbers that it has not yet notified the client + // about via EXISTS; ignore those messages here. + // GoDaddy IMAP does this too. + if (logger.isLoggable(Level.FINE)) + logger.fine("ignoring message number " + + seqnum + " outside range " + messageCache.size()); + return null; + } + return messageCache.getMessageBySeqnum(seqnum); + } + + /** + * Get the message objects for the given sequence numbers. + * + * ASSERT: This method must be called only when holding the + * messageCacheLock + * + * @param seqnums the array of message sequence numbers + * @return the IMAPMessage objects + * @since JavaMail 1.5.3 + */ + protected IMAPMessage[] getMessagesBySeqNumbers(int[] seqnums) { + IMAPMessage[] msgs = new IMAPMessage[seqnums.length]; + int nulls = 0; + // Map seq-numbers into actual Messages. + for (int i = 0; i < seqnums.length; i++) { + msgs[i] = getMessageBySeqNumber(seqnums[i]); + if (msgs[i] == null) + nulls++; + } + if (nulls > 0) { // compress the array to remove the nulls + IMAPMessage[] nmsgs = new IMAPMessage[seqnums.length - nulls]; + for (int i = 0, j = 0; i < msgs.length; i++) { + if (msgs[i] != null) + nmsgs[j++] = msgs[i]; + } + msgs = nmsgs; + } + return msgs; + } + + private boolean isDirectory() { + return ((type & HOLDS_FOLDERS) != 0); + } +} + +/** + * An object that holds a Message object + * and reports its size and writes it to another OutputStream + * on demand. Used by appendMessages to avoid the need to + * buffer the entire message in memory in a single byte array + * before sending it to the server. + */ +class MessageLiteral implements Literal { + private Message msg; + private int msgSize = -1; + private byte[] buf; // the buffered message, if not null + + public MessageLiteral(Message msg, int maxsize) + throws MessagingException, IOException { + this.msg = msg; + // compute the size here so exceptions can be returned immediately + LengthCounter lc = new LengthCounter(maxsize); + OutputStream os = new CRLFOutputStream(lc); + msg.writeTo(os); + os.flush(); + msgSize = lc.getSize(); + buf = lc.getBytes(); + } + + @Override + public int size() { + return msgSize; + } + + @Override + public void writeTo(OutputStream os) throws IOException { + // the message should not change between the constructor and this call + try { + if (buf != null) + os.write(buf, 0, msgSize); + else { + os = new CRLFOutputStream(os); + msg.writeTo(os); + } + } catch (MessagingException mex) { + // exceptions here are bad, "should" never happen + throw new IOException("MessagingException while appending message: " + + mex); + } + } +} + +/** + * Count the number of bytes written to the stream. + * Also, save a copy of small messages to avoid having to process + * the data again. + */ +class LengthCounter extends OutputStream { + private int size = 0; + private byte[] buf; + private int maxsize; + + public LengthCounter(int maxsize) { + buf = new byte[8192]; + this.maxsize = maxsize; + } + + @Override + public void write(int b) { + int newsize = size + 1; + if (buf != null) { + if (newsize > maxsize && maxsize >= 0) { + buf = null; + } else if (newsize > buf.length) { + byte newbuf[] = new byte[Math.max(buf.length << 1, newsize)]; + System.arraycopy(buf, 0, newbuf, 0, size); + buf = newbuf; + buf[size] = (byte)b; + } else { + buf[size] = (byte)b; + } + } + size = newsize; + } + + @Override + public void write(byte b[], int off, int len) { + if ((off < 0) || (off > b.length) || (len < 0) || + ((off + len) > b.length) || ((off + len) < 0)) { + throw new IndexOutOfBoundsException(); + } else if (len == 0) { + return; + } + int newsize = size + len; + if (buf != null) { + if (newsize > maxsize && maxsize >= 0) { + buf = null; + } else if (newsize > buf.length) { + byte newbuf[] = new byte[Math.max(buf.length << 1, newsize)]; + System.arraycopy(buf, 0, newbuf, 0, size); + buf = newbuf; + System.arraycopy(b, off, buf, size, len); + } else { + System.arraycopy(b, off, buf, size, len); + } + } + size = newsize; + } + + @Override + public void write(byte[] b) throws IOException { + write(b, 0, b.length); + } + + public int getSize() { + return size; + } + + public byte[] getBytes() { + return buf; + } +}
diff --git a/mail/src/main/java/com/sun/mail/imap/IMAPInputStream.java b/mail/src/main/java/com/sun/mail/imap/IMAPInputStream.java new file mode 100644 index 0000000..c1c993d --- /dev/null +++ b/mail/src/main/java/com/sun/mail/imap/IMAPInputStream.java
@@ -0,0 +1,259 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.io.*; +import javax.mail.*; +import com.sun.mail.imap.protocol.*; +import com.sun.mail.iap.*; +import com.sun.mail.util.FolderClosedIOException; +import com.sun.mail.util.MessageRemovedIOException; + +/** + * This class implements an IMAP data stream. + * + * @author John Mani + */ + +public class IMAPInputStream extends InputStream { + private IMAPMessage msg; // this message + private String section; // section-id + private int pos; // track the position within the IMAP datastream + private int blksize; // number of bytes to read in each FETCH request + private int max; // the total number of bytes in this section. + // -1 indicates unknown + private byte[] buf; // the buffer obtained from fetchBODY() + private int bufcount; // The index one greater than the index of the + // last valid byte in 'buf' + private int bufpos; // The current position within 'buf' + private boolean lastBuffer; // is this the last buffer of data? + private boolean peek; // peek instead of fetch? + private ByteArray readbuf; // reuse for each read + + // Allocate this much extra space in the read buffer to allow + // space for the FETCH response overhead + private static final int slop = 64; + + + /** + * Create an IMAPInputStream. + * + * @param msg the IMAPMessage the data will come from + * @param section the IMAP section/part identifier for the data + * @param max the number of bytes in this section + * @param peek peek instead of fetch? + */ + public IMAPInputStream(IMAPMessage msg, String section, int max, + boolean peek) { + this.msg = msg; + this.section = section; + this.max = max; + this.peek = peek; + pos = 0; + blksize = msg.getFetchBlockSize(); + } + + /** + * Do a NOOP to force any untagged EXPUNGE responses + * and then check if this message is expunged. + */ + private void forceCheckExpunged() + throws MessageRemovedIOException, FolderClosedIOException { + synchronized (msg.getMessageCacheLock()) { + try { + msg.getProtocol().noop(); + } catch (ConnectionException cex) { + throw new FolderClosedIOException(msg.getFolder(), + cex.getMessage()); + } catch (FolderClosedException fex) { + throw new FolderClosedIOException(fex.getFolder(), + fex.getMessage()); + } catch (ProtocolException pex) { + // ignore it + } + } + if (msg.isExpunged()) + throw new MessageRemovedIOException(); + } + + /** + * Fetch more data from the server. This method assumes that all + * data has already been read in, hence bufpos > bufcount. + */ + private void fill() throws IOException { + /* + * If we've read the last buffer, there's no more to read. + * If we know the total number of bytes available from this + * section, let's check if we have consumed that many bytes. + */ + if (lastBuffer || max != -1 && pos >= max) { + if (pos == 0) + checkSeen(); + readbuf = null; // XXX - return to pool? + return; // the caller of fill() will return -1. + } + + BODY b = null; + if (readbuf == null) + readbuf = new ByteArray(blksize + slop); + + ByteArray ba; + int cnt; + // Acquire MessageCacheLock, to freeze seqnum. + synchronized (msg.getMessageCacheLock()) { + try { + IMAPProtocol p = msg.getProtocol(); + + // Check whether this message is expunged + if (msg.isExpunged()) + throw new MessageRemovedIOException( + "No content for expunged message"); + + int seqnum = msg.getSequenceNumber(); + cnt = blksize; + if (max != -1 && pos + blksize > max) + cnt = max - pos; + if (peek) + b = p.peekBody(seqnum, section, pos, cnt, readbuf); + else + b = p.fetchBody(seqnum, section, pos, cnt, readbuf); + } catch (ProtocolException pex) { + forceCheckExpunged(); + throw new IOException(pex.getMessage()); + } catch (FolderClosedException fex) { + throw new FolderClosedIOException(fex.getFolder(), + fex.getMessage()); + } + + if (b == null || ((ba = b.getByteArray()) == null)) { + forceCheckExpunged(); + // nope, the server doesn't think it's expunged. + // can't tell the difference between the server returning NIL + // and some other error that caused null to be returned above, + // so we'll just assume it was empty content. + ba = new ByteArray(0); + } + } + + // make sure the SEEN flag is set after reading the first chunk + if (pos == 0) + checkSeen(); + + // setup new values .. + buf = ba.getBytes(); + bufpos = ba.getStart(); + int n = ba.getCount(); // will be zero, if all data has been + // consumed from the server. + // if we got less than we asked for, this is the last buffer of data + lastBuffer = n < cnt; + bufcount = bufpos + n; + pos += n; + } + + /** + * Reads the next byte of data from this buffered input stream. + * If no byte is available, the value <code>-1</code> is returned. + */ + @Override + public synchronized int read() throws IOException { + if (bufpos >= bufcount) { + fill(); + if (bufpos >= bufcount) + return -1; // EOF + } + return buf[bufpos++] & 0xff; + } + + /** + * Reads up to <code>len</code> bytes of data from this + * input stream into the given buffer. <p> + * + * Returns the total number of bytes read into the buffer, + * or <code>-1</code> if there is no more data. <p> + * + * Note that this method mimics the "weird !" semantics of + * BufferedInputStream in that the number of bytes actually + * returned may be less that the requested value. So callers + * of this routine should be aware of this and must check + * the return value to insure that they have obtained the + * requisite number of bytes. + */ + @Override + public synchronized int read(byte b[], int off, int len) + throws IOException { + + int avail = bufcount - bufpos; + if (avail <= 0) { + fill(); + avail = bufcount - bufpos; + if (avail <= 0) + return -1; // EOF + } + int cnt = (avail < len) ? avail : len; + System.arraycopy(buf, bufpos, b, off, cnt); + bufpos += cnt; + return cnt; + } + + /** + * Reads up to <code>b.length</code> bytes of data from this input + * stream into an array of bytes. <p> + * + * Returns the total number of bytes read into the buffer, or + * <code>-1</code> is there is no more data. <p> + * + * Note that this method mimics the "weird !" semantics of + * BufferedInputStream in that the number of bytes actually + * returned may be less that the requested value. So callers + * of this routine should be aware of this and must check + * the return value to insure that they have obtained the + * requisite number of bytes. + */ + @Override + public int read(byte b[]) throws IOException { + return read(b, 0, b.length); + } + + /** + * Returns the number of bytes that can be read from this input + * stream without blocking. + */ + @Override + public synchronized int available() throws IOException { + return (bufcount - bufpos); + } + + /** + * Normally the SEEN flag will have been set by now, but if not, + * force it to be set (as long as the folder isn't open read-only + * and we're not peeking). + * And of course, if there's no folder (e.g., a nested message) + * don't do anything. + */ + private void checkSeen() { + if (peek) // if we're peeking, don't set the SEEN flag + return; + try { + Folder f = msg.getFolder(); + if (f != null && f.getMode() != Folder.READ_ONLY && + !msg.isSet(Flags.Flag.SEEN)) + msg.setFlag(Flags.Flag.SEEN, true); + } catch (MessagingException ex) { + // ignore it + } + } +}
diff --git a/mail/src/main/java/com/sun/mail/imap/IMAPMessage.java b/mail/src/main/java/com/sun/mail/imap/IMAPMessage.java new file mode 100644 index 0000000..453479d --- /dev/null +++ b/mail/src/main/java/com/sun/mail/imap/IMAPMessage.java
@@ -0,0 +1,1700 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.util.Date; +import java.io.*; +import java.util.*; + +import javax.mail.*; +import javax.mail.internet.*; +import javax.activation.*; + +import com.sun.mail.util.ReadableMime; +import com.sun.mail.iap.*; +import com.sun.mail.imap.protocol.*; + +/** + * This class implements an IMAPMessage object. <p> + * + * An IMAPMessage object starts out as a light-weight object. It gets + * filled-in incrementally when a request is made for some item. Or + * when a prefetch is done using the FetchProfile. <p> + * + * An IMAPMessage has a messageNumber and a sequenceNumber. The + * messageNumber is its index into its containing folder's messageCache. + * The sequenceNumber is its IMAP sequence-number. + * + * @author John Mani + * @author Bill Shannon + */ +/* + * The lock hierarchy is that the lock on the IMAPMessage object, if + * it's acquired at all, must be acquired before the message cache lock. + * The IMAPMessage lock protects the message flags, sort of. + * + * XXX - I'm not convinced that all fields of IMAPMessage are properly + * protected by locks. + */ + +public class IMAPMessage extends MimeMessage implements ReadableMime { + protected BODYSTRUCTURE bs; // BODYSTRUCTURE + protected ENVELOPE envelope; // ENVELOPE + + /** + * A map of the extension FETCH items. In addition to saving the + * data in this map, an entry in this map indicates that we *have* + * the data, and so it doesn't need to be fetched again. The map + * is created only when needed, to avoid significantly increasing + * the effective size of an IMAPMessage object. + * + * @since JavaMail 1.4.6 + */ + protected Map<String, Object> items; // Map<String,Object> + + private Date receivedDate; // INTERNALDATE + private long size = -1; // RFC822.SIZE + + private Boolean peek; // use BODY.PEEK when fetching content? + + // this message's IMAP UID + private volatile long uid = -1; + + // this message's IMAP MODSEQ - RFC 4551 CONDSTORE + private volatile long modseq = -1; + + // this message's IMAP sectionId (null for toplevel message, + // non-null for a nested message) + protected String sectionId; + + // processed values + private String type; // Content-Type (with params) + private String subject; // decoded (Unicode) subject + private String description; // decoded (Unicode) desc + + // Indicates that we've loaded *all* headers for this message + private volatile boolean headersLoaded = false; + + // Indicates that we've cached the body of this message + private volatile boolean bodyLoaded = false; + + /* Hashtable of names of headers we've loaded from the server. + * Used in isHeaderLoaded() and getHeaderLoaded() to keep track + * of those headers we've attempted to load from the server. We + * need this table of names to avoid multiple attempts at loading + * headers that don't exist for a particular message. + * + * Could this somehow be included in the InternetHeaders object ?? + */ + private Hashtable<String, String> loadedHeaders + = new Hashtable<>(1); + + // This is our Envelope + static final String EnvelopeCmd = "ENVELOPE INTERNALDATE RFC822.SIZE"; + + /** + * Constructor. + * + * @param folder the folder containing this message + * @param msgnum the message sequence number + */ + protected IMAPMessage(IMAPFolder folder, int msgnum) { + super(folder, msgnum); + flags = null; + } + + /** + * Constructor, for use by IMAPNestedMessage. + * + * @param session the Session + */ + protected IMAPMessage(Session session) { + super(session); + } + + /** + * Get this message's folder's protocol connection. + * Throws FolderClosedException, if the protocol connection + * is not available. + * + * ASSERT: Must hold the messageCacheLock. + * + * @return the IMAPProtocol object for the containing folder + * @exception ProtocolException for protocol errors + * @exception FolderClosedException if the folder is closed + */ + protected IMAPProtocol getProtocol() + throws ProtocolException, FolderClosedException { + ((IMAPFolder)folder).waitIfIdle(); + IMAPProtocol p = ((IMAPFolder)folder).protocol; + if (p == null) + throw new FolderClosedException(folder); + else + return p; + } + + /* + * Is this an IMAP4 REV1 server? + */ + protected boolean isREV1() throws FolderClosedException { + // access the folder's protocol object without waiting + // for IDLE to complete + IMAPProtocol p = ((IMAPFolder)folder).protocol; + if (p == null) + throw new FolderClosedException(folder); + else + return p.isREV1(); + } + + /** + * Get the messageCacheLock, associated with this Message's + * Folder. + * + * @return the message cache lock object + */ + protected Object getMessageCacheLock() { + return ((IMAPFolder)folder).messageCacheLock; + } + + /** + * Get this message's IMAP sequence number. + * + * ASSERT: This method must be called only when holding the + * messageCacheLock. + * + * @return the message sequence number + */ + protected int getSequenceNumber() { + return ((IMAPFolder)folder).messageCache.seqnumOf(getMessageNumber()); + } + + /** + * Wrapper around the protected method Message.setMessageNumber() to + * make that method accessible to IMAPFolder. + */ + @Override + protected void setMessageNumber(int msgnum) { + super.setMessageNumber(msgnum); + } + + /** + * Return the UID for this message. + * Returns -1 if not known; use UIDFolder.getUID() in this case. + * + * @return the UID + * @see javax.mail.UIDFolder#getUID + */ + protected long getUID() { + return uid; + } + + protected void setUID(long uid) { + this.uid = uid; + } + + /** + * Return the modification sequence number (MODSEQ) for this message. + * Returns -1 if not known. + * + * @return the modification sequence number + * @exception MessagingException for failures + * @see "RFC 4551" + * @since JavaMail 1.5.1 + */ + public synchronized long getModSeq() throws MessagingException { + if (modseq != -1) + return modseq; + + synchronized (getMessageCacheLock()) { // Acquire Lock + try { + IMAPProtocol p = getProtocol(); + checkExpunged(); // insure that message is not expunged + MODSEQ ms = p.fetchMODSEQ(getSequenceNumber()); + + if (ms != null) + modseq = ms.modseq; + } catch (ConnectionException cex) { + throw new FolderClosedException(folder, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + } + return modseq; + } + + long _getModSeq() { + return modseq; + } + + void setModSeq(long modseq) { + this.modseq = modseq; + } + + // expose to MessageCache + @Override + protected void setExpunged(boolean set) { + super.setExpunged(set); + } + + // Convenience routine + protected void checkExpunged() throws MessageRemovedException { + if (expunged) + throw new MessageRemovedException(); + } + + /** + * Do a NOOP to force any untagged EXPUNGE responses + * and then check if this message is expunged. + * + * @exception MessageRemovedException if the message has been removed + * @exception FolderClosedException if the folder has been closed + */ + protected void forceCheckExpunged() + throws MessageRemovedException, FolderClosedException { + synchronized (getMessageCacheLock()) { + try { + getProtocol().noop(); + } catch (ConnectionException cex) { + throw new FolderClosedException(folder, cex.getMessage()); + } catch (ProtocolException pex) { + // ignore it + } + } + if (expunged) + throw new MessageRemovedException(); + } + + // Return the block size for FETCH requests + // MUST be overridden by IMAPNestedMessage + protected int getFetchBlockSize() { + return ((IMAPStore)folder.getStore()).getFetchBlockSize(); + } + + // Should we ignore the size in the BODYSTRUCTURE? + // MUST be overridden by IMAPNestedMessage + protected boolean ignoreBodyStructureSize() { + return ((IMAPStore)folder.getStore()).ignoreBodyStructureSize(); + } + + /** + * Get the "From" attribute. + */ + @Override + public Address[] getFrom() throws MessagingException { + checkExpunged(); + if (bodyLoaded) + return super.getFrom(); + loadEnvelope(); + InternetAddress[] a = envelope.from; + /* + * Per RFC 2822, the From header is required, and thus the IMAP + * spec also requires that it be present, but we know that in + * practice it is often missing. Some servers fill in the + * From field with the Sender field in this case, but at least + * Exchange 2007 does not. Use the same fallback strategy used + * by MimeMessage. + */ + if (a == null || a.length == 0) + a = envelope.sender; + return aaclone(a); + } + + @Override + public void setFrom(Address address) throws MessagingException { + throw new IllegalWriteException("IMAPMessage is read-only"); + } + + @Override + public void addFrom(Address[] addresses) throws MessagingException { + throw new IllegalWriteException("IMAPMessage is read-only"); + } + + /** + * Get the "Sender" attribute. + */ + @Override + public Address getSender() throws MessagingException { + checkExpunged(); + if (bodyLoaded) + return super.getSender(); + loadEnvelope(); + if (envelope.sender != null && envelope.sender.length > 0) + return (envelope.sender)[0]; // there can be only one sender + else + return null; + } + + + @Override + public void setSender(Address address) throws MessagingException { + throw new IllegalWriteException("IMAPMessage is read-only"); + } + + /** + * Get the desired Recipient type. + */ + @Override + public Address[] getRecipients(Message.RecipientType type) + throws MessagingException { + checkExpunged(); + if (bodyLoaded) + return super.getRecipients(type); + loadEnvelope(); + + if (type == Message.RecipientType.TO) + return aaclone(envelope.to); + else if (type == Message.RecipientType.CC) + return aaclone(envelope.cc); + else if (type == Message.RecipientType.BCC) + return aaclone(envelope.bcc); + else + return super.getRecipients(type); + } + + @Override + public void setRecipients(Message.RecipientType type, Address[] addresses) + throws MessagingException { + throw new IllegalWriteException("IMAPMessage is read-only"); + } + + @Override + public void addRecipients(Message.RecipientType type, Address[] addresses) + throws MessagingException { + throw new IllegalWriteException("IMAPMessage is read-only"); + } + + /** + * Get the ReplyTo addresses. + */ + @Override + public Address[] getReplyTo() throws MessagingException { + checkExpunged(); + if (bodyLoaded) + return super.getReplyTo(); + loadEnvelope(); + /* + * The IMAP spec requires that the Reply-To field never be + * null, but at least Exchange 2007 fails to fill it in in + * some cases. Use the same fallback strategy used by + * MimeMessage. + */ + if (envelope.replyTo == null || envelope.replyTo.length == 0) + return getFrom(); + return aaclone(envelope.replyTo); + } + + @Override + public void setReplyTo(Address[] addresses) throws MessagingException { + throw new IllegalWriteException("IMAPMessage is read-only"); + } + + /** + * Get the decoded subject. + */ + @Override + public String getSubject() throws MessagingException { + checkExpunged(); + if (bodyLoaded) + return super.getSubject(); + + if (subject != null) // already cached ? + return subject; + + loadEnvelope(); + if (envelope.subject == null) // no subject + return null; + + // Cache and return the decoded value. + try { + // The server *should* unfold the value, but just in case it + // doesn't we unfold it here. + subject = + MimeUtility.decodeText(MimeUtility.unfold(envelope.subject)); + } catch (UnsupportedEncodingException ex) { + subject = envelope.subject; + } + + return subject; + } + + @Override + public void setSubject(String subject, String charset) + throws MessagingException { + throw new IllegalWriteException("IMAPMessage is read-only"); + } + + /** + * Get the SentDate. + */ + @Override + public Date getSentDate() throws MessagingException { + checkExpunged(); + if (bodyLoaded) + return super.getSentDate(); + loadEnvelope(); + if (envelope.date == null) + return null; + else + return new Date(envelope.date.getTime()); + } + + @Override + public void setSentDate(Date d) throws MessagingException { + throw new IllegalWriteException("IMAPMessage is read-only"); + } + + /** + * Get the received date (INTERNALDATE). + */ + @Override + public Date getReceivedDate() throws MessagingException { + checkExpunged(); + if (receivedDate == null) + loadEnvelope(); // have to go to the server for this + if (receivedDate == null) + return null; + else + return new Date(receivedDate.getTime()); + } + + /** + * Get the message size. <p> + * + * Note that this returns RFC822.SIZE. That is, it's the + * size of the whole message, header and body included. + * Note also that if the size of the message is greater than + * Integer.MAX_VALUE (2GB), this method returns Integer.MAX_VALUE. + */ + @Override + public int getSize() throws MessagingException { + checkExpunged(); + // if bodyLoaded, size is already set + if (size == -1) + loadEnvelope(); // XXX - could just fetch the size + if (size > Integer.MAX_VALUE) + return Integer.MAX_VALUE; // the best we can do... + else + return (int)size; + } + + /** + * Get the message size as a long. <p> + * + * Suitable for messages that might be larger than 2GB. + * @return the message size as a long integer + * @exception MessagingException for failures + * @since JavaMail 1.6 + */ + public long getSizeLong() throws MessagingException { + checkExpunged(); + // if bodyLoaded, size is already set + if (size == -1) + loadEnvelope(); // XXX - could just fetch the size + return size; + } + + /** + * Get the total number of lines. <p> + * + * Returns the "body_fld_lines" field from the + * BODYSTRUCTURE. Note that this field is available + * only for text/plain and message/rfc822 types + */ + @Override + public int getLineCount() throws MessagingException { + checkExpunged(); + // XXX - superclass doesn't implement this + loadBODYSTRUCTURE(); + return bs.lines; + } + + /** + * Get the content language. + */ + @Override + public String[] getContentLanguage() throws MessagingException { + checkExpunged(); + if (bodyLoaded) + return super.getContentLanguage(); + loadBODYSTRUCTURE(); + if (bs.language != null) + return bs.language.clone(); + else + return null; + } + + @Override + public void setContentLanguage(String[] languages) + throws MessagingException { + throw new IllegalWriteException("IMAPMessage is read-only"); + } + + /** + * Get the In-Reply-To header. + * + * @return the In-Reply-To header + * @exception MessagingException for failures + * @since JavaMail 1.3.3 + */ + public String getInReplyTo() throws MessagingException { + checkExpunged(); + if (bodyLoaded) + return super.getHeader("In-Reply-To", " "); + loadEnvelope(); + return envelope.inReplyTo; + } + + /** + * Get the Content-Type. + * + * Generate this header from the BODYSTRUCTURE. Append parameters + * as well. + */ + @Override + public synchronized String getContentType() throws MessagingException { + checkExpunged(); + if (bodyLoaded) + return super.getContentType(); + + // If we haven't cached the type yet .. + if (type == null) { + loadBODYSTRUCTURE(); + // generate content-type from BODYSTRUCTURE + ContentType ct = new ContentType(bs.type, bs.subtype, bs.cParams); + type = ct.toString(); + } + return type; + } + + /** + * Get the Content-Disposition. + */ + @Override + public String getDisposition() throws MessagingException { + checkExpunged(); + if (bodyLoaded) + return super.getDisposition(); + loadBODYSTRUCTURE(); + return bs.disposition; + } + + @Override + public void setDisposition(String disposition) throws MessagingException { + throw new IllegalWriteException("IMAPMessage is read-only"); + } + + /** + * Get the Content-Transfer-Encoding. + */ + @Override + public String getEncoding() throws MessagingException { + checkExpunged(); + if (bodyLoaded) + return super.getEncoding(); + loadBODYSTRUCTURE(); + return bs.encoding; + } + + /** + * Get the Content-ID. + */ + @Override + public String getContentID() throws MessagingException { + checkExpunged(); + if (bodyLoaded) + return super.getContentID(); + loadBODYSTRUCTURE(); + return bs.id; + } + + @Override + public void setContentID(String cid) throws MessagingException { + throw new IllegalWriteException("IMAPMessage is read-only"); + } + + /** + * Get the Content-MD5. + */ + @Override + public String getContentMD5() throws MessagingException { + checkExpunged(); + if (bodyLoaded) + return super.getContentMD5(); + loadBODYSTRUCTURE(); + return bs.md5; + } + + @Override + public void setContentMD5(String md5) throws MessagingException { + throw new IllegalWriteException("IMAPMessage is read-only"); + } + + /** + * Get the decoded Content-Description. + */ + @Override + public String getDescription() throws MessagingException { + checkExpunged(); + if (bodyLoaded) + return super.getDescription(); + + if (description != null) // cached value ? + return description; + + loadBODYSTRUCTURE(); + if (bs.description == null) + return null; + + try { + description = MimeUtility.decodeText(bs.description); + } catch (UnsupportedEncodingException ex) { + description = bs.description; + } + + return description; + } + + @Override + public void setDescription(String description, String charset) + throws MessagingException { + throw new IllegalWriteException("IMAPMessage is read-only"); + } + + /** + * Get the Message-ID. + */ + @Override + public String getMessageID() throws MessagingException { + checkExpunged(); + if (bodyLoaded) + return super.getMessageID(); + loadEnvelope(); + return envelope.messageId; + } + + /** + * Get the "filename" Disposition parameter. (Only available in + * IMAP4rev1). If thats not available, get the "name" ContentType + * parameter. + */ + @Override + public String getFileName() throws MessagingException { + checkExpunged(); + if (bodyLoaded) + return super.getFileName(); + + String filename = null; + loadBODYSTRUCTURE(); + + if (bs.dParams != null) + filename = bs.dParams.get("filename"); + if (filename == null && bs.cParams != null) + filename = bs.cParams.get("name"); + return filename; + } + + @Override + public void setFileName(String filename) throws MessagingException { + throw new IllegalWriteException("IMAPMessage is read-only"); + } + + /** + * Get all the bytes for this message. Overrides getContentStream() + * in MimeMessage. This method is ultimately used by the DataHandler + * to obtain the input stream for this message. + * + * @see javax.mail.internet.MimeMessage#getContentStream + */ + @Override + protected InputStream getContentStream() throws MessagingException { + if (bodyLoaded) + return super.getContentStream(); + InputStream is = null; + boolean pk = getPeek(); // get before acquiring message cache lock + + // Acquire MessageCacheLock, to freeze seqnum. + synchronized(getMessageCacheLock()) { + try { + IMAPProtocol p = getProtocol(); + + // This message could be expunged when we were waiting + // to acquire the lock ... + checkExpunged(); + + if (p.isREV1() && (getFetchBlockSize() != -1)) // IMAP4rev1 + return new IMAPInputStream(this, toSection("TEXT"), + bs != null && !ignoreBodyStructureSize() ? + bs.size : -1, pk); + + if (p.isREV1()) { + BODY b; + if (pk) + b = p.peekBody(getSequenceNumber(), toSection("TEXT")); + else + b = p.fetchBody(getSequenceNumber(), toSection("TEXT")); + if (b != null) + is = b.getByteArrayInputStream(); + } else { + RFC822DATA rd = p.fetchRFC822(getSequenceNumber(), "TEXT"); + if (rd != null) + is = rd.getByteArrayInputStream(); + } + } catch (ConnectionException cex) { + throw new FolderClosedException(folder, cex.getMessage()); + } catch (ProtocolException pex) { + forceCheckExpunged(); + throw new MessagingException(pex.getMessage(), pex); + } + } + + if (is == null) { + forceCheckExpunged(); // may throw MessageRemovedException + // nope, the server doesn't think it's expunged. + // can't tell the difference between the server returning NIL + // and some other error that caused null to be returned above, + // so we'll just assume it was empty content. + is = new ByteArrayInputStream(new byte[0]); + } + return is; + } + + /** + * Get the DataHandler object for this message. + */ + @Override + public synchronized DataHandler getDataHandler() + throws MessagingException { + checkExpunged(); + + if (dh == null && !bodyLoaded) { + loadBODYSTRUCTURE(); + if (type == null) { // type not yet computed + // generate content-type from BODYSTRUCTURE + ContentType ct = new ContentType(bs.type, bs.subtype, + bs.cParams); + type = ct.toString(); + } + + /* Special-case Multipart and Nested content. All other + * cases are handled by the superclass. + */ + if (bs.isMulti()) + dh = new DataHandler( + new IMAPMultipartDataSource(this, bs.bodies, + sectionId, this) + ); + else if (bs.isNested() && isREV1() && bs.envelope != null) + /* Nested messages are handled specially only for + * IMAP4rev1. IMAP4 doesn't provide enough support to + * FETCH the components of nested messages + */ + dh = new DataHandler( + new IMAPNestedMessage(this, + bs.bodies[0], + bs.envelope, + sectionId == null ? "1" : sectionId + ".1"), + type + ); + } + + return super.getDataHandler(); + } + + @Override + public void setDataHandler(DataHandler content) + throws MessagingException { + throw new IllegalWriteException("IMAPMessage is read-only"); + } + + /** + * Return the MIME format stream corresponding to this message. + * + * @return the MIME format stream + * @since JavaMail 1.4.5 + */ + @Override + public InputStream getMimeStream() throws MessagingException { + // XXX - need an "if (bodyLoaded)" version + InputStream is = null; + boolean pk = getPeek(); // get before acquiring message cache lock + + // Acquire MessageCacheLock, to freeze seqnum. + synchronized(getMessageCacheLock()) { + try { + IMAPProtocol p = getProtocol(); + + checkExpunged(); // insure this message is not expunged + + if (p.isREV1() && (getFetchBlockSize() != -1)) // IMAP4rev1 + return new IMAPInputStream(this, sectionId, -1, pk); + + if (p.isREV1()) { + BODY b; + if (pk) + b = p.peekBody(getSequenceNumber(), sectionId); + else + b = p.fetchBody(getSequenceNumber(), sectionId); + if (b != null) + is = b.getByteArrayInputStream(); + } else { + RFC822DATA rd = p.fetchRFC822(getSequenceNumber(), null); + if (rd != null) + is = rd.getByteArrayInputStream(); + } + } catch (ConnectionException cex) { + throw new FolderClosedException(folder, cex.getMessage()); + } catch (ProtocolException pex) { + forceCheckExpunged(); + throw new MessagingException(pex.getMessage(), pex); + } + } + + if (is == null) { + forceCheckExpunged(); // may throw MessageRemovedException + // nope, the server doesn't think it's expunged. + // can't tell the difference between the server returning NIL + // and some other error that caused null to be returned above, + // so we'll just assume it was empty content. + is = new ByteArrayInputStream(new byte[0]); + } + return is; + } + + /** + * Write out the bytes into the given OutputStream. + */ + @Override + public void writeTo(OutputStream os) + throws IOException, MessagingException { + if (bodyLoaded) { + super.writeTo(os); + return; + } + InputStream is = getMimeStream(); + try { + // write out the bytes + byte[] bytes = new byte[16*1024]; + int count; + while ((count = is.read(bytes)) != -1) + os.write(bytes, 0, count); + } finally { + is.close(); + } + } + + /** + * Get the named header. + */ + @Override + public String[] getHeader(String name) throws MessagingException { + checkExpunged(); + + if (isHeaderLoaded(name)) // already loaded ? + return headers.getHeader(name); + + // Load this particular header + InputStream is = null; + + // Acquire MessageCacheLock, to freeze seqnum. + synchronized(getMessageCacheLock()) { + try { + IMAPProtocol p = getProtocol(); + + // This message could be expunged when we were waiting + // to acquire the lock ... + checkExpunged(); + + if (p.isREV1()) { + BODY b = p.peekBody(getSequenceNumber(), + toSection("HEADER.FIELDS (" + name + ")") + ); + if (b != null) + is = b.getByteArrayInputStream(); + } else { + RFC822DATA rd = p.fetchRFC822(getSequenceNumber(), + "HEADER.LINES (" + name + ")"); + if (rd != null) + is = rd.getByteArrayInputStream(); + } + } catch (ConnectionException cex) { + throw new FolderClosedException(folder, cex.getMessage()); + } catch (ProtocolException pex) { + forceCheckExpunged(); + throw new MessagingException(pex.getMessage(), pex); + } + } + + // if we get this far without "is" being set, something has gone + // wrong; prevent a later NullPointerException and return null here + if (is == null) + return null; + + if (headers == null) + headers = new InternetHeaders(); + headers.load(is); // load this header into the Headers object. + setHeaderLoaded(name); // Mark this header as loaded + + return headers.getHeader(name); + } + + /** + * Get the named header. + */ + @Override + public String getHeader(String name, String delimiter) + throws MessagingException { + checkExpunged(); + + // force the header to be loaded by invoking getHeader(name) + if (getHeader(name) == null) + return null; + return headers.getHeader(name, delimiter); + } + + @Override + public void setHeader(String name, String value) + throws MessagingException { + throw new IllegalWriteException("IMAPMessage is read-only"); + } + + @Override + public void addHeader(String name, String value) + throws MessagingException { + throw new IllegalWriteException("IMAPMessage is read-only"); + } + + @Override + public void removeHeader(String name) + throws MessagingException { + throw new IllegalWriteException("IMAPMessage is read-only"); + } + + /** + * Get all headers. + */ + @Override + public Enumeration<Header> getAllHeaders() throws MessagingException { + checkExpunged(); + loadHeaders(); + return super.getAllHeaders(); + } + + /** + * Get matching headers. + */ + @Override + public Enumeration<Header> getMatchingHeaders(String[] names) + throws MessagingException { + checkExpunged(); + loadHeaders(); + return super.getMatchingHeaders(names); + } + + /** + * Get non-matching headers. + */ + @Override + public Enumeration<Header> getNonMatchingHeaders(String[] names) + throws MessagingException { + checkExpunged(); + loadHeaders(); + return super.getNonMatchingHeaders(names); + } + + @Override + public void addHeaderLine(String line) throws MessagingException { + throw new IllegalWriteException("IMAPMessage is read-only"); + } + + /** + * Get all header-lines. + */ + @Override + public Enumeration<String> getAllHeaderLines() throws MessagingException { + checkExpunged(); + loadHeaders(); + return super.getAllHeaderLines(); + } + + /** + * Get all matching header-lines. + */ + @Override + public Enumeration<String> getMatchingHeaderLines(String[] names) + throws MessagingException { + checkExpunged(); + loadHeaders(); + return super.getMatchingHeaderLines(names); + } + + /** + * Get all non-matching headerlines. + */ + @Override + public Enumeration<String> getNonMatchingHeaderLines(String[] names) + throws MessagingException { + checkExpunged(); + loadHeaders(); + return super.getNonMatchingHeaderLines(names); + } + + /** + * Get the Flags for this message. + */ + @Override + public synchronized Flags getFlags() throws MessagingException { + checkExpunged(); + loadFlags(); + return super.getFlags(); + } + + /** + * Test if the given Flags are set in this message. + */ + @Override + public synchronized boolean isSet(Flags.Flag flag) + throws MessagingException { + checkExpunged(); + loadFlags(); + return super.isSet(flag); + } + + /** + * Set/Unset the given flags in this message. + */ + @Override + public synchronized void setFlags(Flags flag, boolean set) + throws MessagingException { + // Acquire MessageCacheLock, to freeze seqnum. + synchronized(getMessageCacheLock()) { + try { + IMAPProtocol p = getProtocol(); + checkExpunged(); // Insure that this message is not expunged + p.storeFlags(getSequenceNumber(), flag, set); + } catch (ConnectionException cex) { + throw new FolderClosedException(folder, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + } + } + + /** + * Set whether or not to use the PEEK variant of FETCH when + * fetching message content. This overrides the default + * value from the "mail.imap.peek" property. + * + * @param peek the peek flag + * @since JavaMail 1.3.3 + */ + public synchronized void setPeek(boolean peek) { + this.peek = Boolean.valueOf(peek); + } + + /** + * Get whether or not to use the PEEK variant of FETCH when + * fetching message content. + * + * @return the peek flag + * @since JavaMail 1.3.3 + */ + public synchronized boolean getPeek() { + if (peek == null) + return ((IMAPStore)folder.getStore()).getPeek(); + else + return peek.booleanValue(); + } + + /** + * Invalidate cached header and envelope information for this + * message. Subsequent accesses of this information will + * cause it to be fetched from the server. + * + * @since JavaMail 1.3.3 + */ + public synchronized void invalidateHeaders() { + headersLoaded = false; + loadedHeaders.clear(); + headers = null; + envelope = null; + bs = null; + receivedDate = null; + size = -1; + type = null; + subject = null; + description = null; + flags = null; + content = null; + contentStream = null; + bodyLoaded = false; + } + + /** + * This class implements the test to be done on each + * message in the folder. The test is to check whether the + * message has already cached all the items requested in the + * FetchProfile. If any item is missing, the test succeeds and + * breaks out. + */ + public static class FetchProfileCondition implements Utility.Condition { + private boolean needEnvelope = false; + private boolean needFlags = false; + private boolean needBodyStructure = false; + private boolean needUID = false; + private boolean needHeaders = false; + private boolean needSize = false; + private boolean needMessage = false; + private boolean needRDate = false; + private String[] hdrs = null; + private Set<FetchItem> need = new HashSet<>(); + + /** + * Create a FetchProfileCondition to determine if we need to fetch + * any of the information specified in the FetchProfile. + * + * @param fp the FetchProfile + * @param fitems the FETCH items + */ + @SuppressWarnings("deprecation") // for FetchProfile.Item.SIZE + public FetchProfileCondition(FetchProfile fp, FetchItem[] fitems) { + if (fp.contains(FetchProfile.Item.ENVELOPE)) + needEnvelope = true; + if (fp.contains(FetchProfile.Item.FLAGS)) + needFlags = true; + if (fp.contains(FetchProfile.Item.CONTENT_INFO)) + needBodyStructure = true; + if (fp.contains(FetchProfile.Item.SIZE)) + needSize = true; + if (fp.contains(UIDFolder.FetchProfileItem.UID)) + needUID = true; + if (fp.contains(IMAPFolder.FetchProfileItem.HEADERS)) + needHeaders = true; + if (fp.contains(IMAPFolder.FetchProfileItem.SIZE)) + needSize = true; + if (fp.contains(IMAPFolder.FetchProfileItem.MESSAGE)) + needMessage = true; + if (fp.contains(IMAPFolder.FetchProfileItem.INTERNALDATE)) + needRDate = true; + hdrs = fp.getHeaderNames(); + for (int i = 0; i < fitems.length; i++) { + if (fp.contains(fitems[i].getFetchProfileItem())) + need.add(fitems[i]); + } + } + + /** + * Return true if we NEED to fetch the requested information + * for the specified message. + */ + @Override + public boolean test(IMAPMessage m) { + if (needEnvelope && m._getEnvelope() == null && !m.bodyLoaded) + return true; // no envelope + if (needFlags && m._getFlags() == null) + return true; // no flags + if (needBodyStructure && m._getBodyStructure() == null && + !m.bodyLoaded) + return true; // no BODYSTRUCTURE + if (needUID && m.getUID() == -1) // no UID + return true; + if (needHeaders && !m.areHeadersLoaded()) // no headers + return true; + if (needSize && m.size == -1 && !m.bodyLoaded) // no size + return true; + if (needMessage && !m.bodyLoaded) // no message body + return true; + if (needRDate && m.receivedDate == null) // no received date + return true; + + // Is the desired header present ? + for (int i = 0; i < hdrs.length; i++) { + if (!m.isHeaderLoaded(hdrs[i])) + return true; // Nope, return + } + Iterator<FetchItem> it = need.iterator(); + while (it.hasNext()) { + FetchItem fitem = it.next(); + if (m.items == null || m.items.get(fitem.getName()) == null) + return true; + } + + return false; + } + } + + /** + * Apply the data in the FETCH item to this message. + * + * ASSERT: Must hold the messageCacheLock. + * + * @param item the fetch item + * @param hdrs the headers we're asking for + * @param allHeaders load all headers? + * @return did we handle this fetch item? + * @exception MessagingException for failures + * @since JavaMail 1.4.6 + */ + protected boolean handleFetchItem(Item item, + String[] hdrs, boolean allHeaders) + throws MessagingException { + // Check for the FLAGS item + if (item instanceof Flags) + flags = (Flags)item; + // Check for ENVELOPE items + else if (item instanceof ENVELOPE) + envelope = (ENVELOPE)item; + else if (item instanceof INTERNALDATE) + receivedDate = ((INTERNALDATE)item).getDate(); + else if (item instanceof RFC822SIZE) + size = ((RFC822SIZE)item).size; + else if (item instanceof MODSEQ) + modseq = ((MODSEQ)item).modseq; + + // Check for the BODYSTRUCTURE item + else if (item instanceof BODYSTRUCTURE) + bs = (BODYSTRUCTURE)item; + // Check for the UID item + else if (item instanceof UID) { + UID u = (UID)item; + uid = u.uid; // set uid + // add entry into uid table + if (((IMAPFolder)folder).uidTable == null) + ((IMAPFolder) folder).uidTable + = new Hashtable<>(); + ((IMAPFolder)folder).uidTable.put(Long.valueOf(u.uid), this); + } + + // Check for header items + else if (item instanceof RFC822DATA || + item instanceof BODY) { + InputStream headerStream; + boolean isHeader; + if (item instanceof RFC822DATA) { // IMAP4 + headerStream = + ((RFC822DATA)item).getByteArrayInputStream(); + isHeader = ((RFC822DATA)item).isHeader(); + } else { // IMAP4rev1 + headerStream = + ((BODY)item).getByteArrayInputStream(); + isHeader = ((BODY)item).isHeader(); + } + + if (!isHeader) { + // load the entire message by using the superclass + // MimeMessage.parse method + // first, save the size of the message + try { + size = headerStream.available(); + } catch (IOException ex) { + // should never occur + } + parse(headerStream); + bodyLoaded = true; + setHeadersLoaded(true); + } else { + // Load the obtained headers. + InternetHeaders h = new InternetHeaders(); + // Some IMAP servers (e.g., gmx.net) return NIL + // instead of a string just containing a CR/LF + // when the header list is empty. + if (headerStream != null) + h.load(headerStream); + if (headers == null || allHeaders) + headers = h; + else { + /* + * This is really painful. A second fetch + * of the same headers (which might occur because + * a new header was added to the set requested) + * will return headers we already know about. + * In this case, only load the headers we haven't + * seen before to avoid adding duplicates of + * headers we already have. + * + * XXX - There's a race condition here if another + * thread is reading headers in the same message + * object, because InternetHeaders is not thread + * safe. + */ + Enumeration<Header> e = h.getAllHeaders(); + while (e.hasMoreElements()) { + Header he = e.nextElement(); + if (!isHeaderLoaded(he.getName())) + headers.addHeader( + he.getName(), he.getValue()); + } + } + + // if we asked for all headers, assume we got them + if (allHeaders) + setHeadersLoaded(true); + else { + // Mark all headers we asked for as 'loaded' + for (int k = 0; k < hdrs.length; k++) + setHeaderLoaded(hdrs[k]); + } + } + } else + return false; // not handled + return true; // something above handled it + } + + /** + * Apply the data in the extension FETCH items to this message. + * This method adds all the items to the items map. + * Subclasses may override this method to call super and then + * also copy the data to a more convenient form. + * + * ASSERT: Must hold the messageCacheLock. + * + * @param extensionItems the Map to add fetch items to + * @since JavaMail 1.4.6 + */ + protected void handleExtensionFetchItems( + Map<String, Object> extensionItems) { + if (extensionItems == null || extensionItems.isEmpty()) + return; + if (items == null) + items = new HashMap<>(); + items.putAll(extensionItems); + } + + /** + * Fetch an individual item for the current message. + * Note that handleExtensionFetchItems will have been called + * to store this item in the message before this method + * returns. + * + * @param fitem the FetchItem + * @return the data associated with the FetchItem + * @exception MessagingException for failures + * @since JavaMail 1.4.6 + */ + protected Object fetchItem(FetchItem fitem) + throws MessagingException { + + // Acquire MessageCacheLock, to freeze seqnum. + synchronized(getMessageCacheLock()) { + Object robj = null; + + try { + IMAPProtocol p = getProtocol(); + + checkExpunged(); // Insure that this message is not expunged + + int seqnum = getSequenceNumber(); + Response[] r = p.fetch(seqnum, fitem.getName()); + + for (int i = 0; i < r.length; i++) { + // If this response is NOT a FetchResponse or if it does + // not match our seqnum, skip. + if (r[i] == null || + !(r[i] instanceof FetchResponse) || + ((FetchResponse)r[i]).getNumber() != seqnum) + continue; + + FetchResponse f = (FetchResponse)r[i]; + handleExtensionFetchItems(f.getExtensionItems()); + if (items != null) { + Object o = items.get(fitem.getName()); + if (o != null) + robj = o; + } + } + + // ((IMAPFolder)folder).handleResponses(r); + p.notifyResponseHandlers(r); + p.handleResult(r[r.length - 1]); + } catch (ConnectionException cex) { + throw new FolderClosedException(folder, cex.getMessage()); + } catch (ProtocolException pex) { + forceCheckExpunged(); + throw new MessagingException(pex.getMessage(), pex); + } + return robj; + + } // Release MessageCacheLock + } + + /** + * Return the data associated with the FetchItem. + * If the data hasn't been fetched, call the fetchItem + * method to fetch it. Returns null if there is no + * data for the FetchItem. + * + * @param fitem the FetchItem + * @return the data associated with the FetchItem + * @exception MessagingException for failures + * @since JavaMail 1.4.6 + */ + public synchronized Object getItem(FetchItem fitem) + throws MessagingException { + Object item = items == null ? null : items.get(fitem.getName()); + if (item == null) + item = fetchItem(fitem); + return item; + } + + /* + * Load the Envelope for this message. + */ + private synchronized void loadEnvelope() throws MessagingException { + if (envelope != null) // already loaded + return; + + Response[] r = null; + + // Acquire MessageCacheLock, to freeze seqnum. + synchronized(getMessageCacheLock()) { + try { + IMAPProtocol p = getProtocol(); + + checkExpunged(); // Insure that this message is not expunged + + int seqnum = getSequenceNumber(); + r = p.fetch(seqnum, EnvelopeCmd); + + for (int i = 0; i < r.length; i++) { + // If this response is NOT a FetchResponse or if it does + // not match our seqnum, skip. + if (r[i] == null || + !(r[i] instanceof FetchResponse) || + ((FetchResponse)r[i]).getNumber() != seqnum) + continue; + + FetchResponse f = (FetchResponse)r[i]; + + // Look for the Envelope items. + int count = f.getItemCount(); + for (int j = 0; j < count; j++) { + Item item = f.getItem(j); + + if (item instanceof ENVELOPE) + envelope = (ENVELOPE)item; + else if (item instanceof INTERNALDATE) + receivedDate = ((INTERNALDATE)item).getDate(); + else if (item instanceof RFC822SIZE) + size = ((RFC822SIZE)item).size; + } + } + + // ((IMAPFolder)folder).handleResponses(r); + p.notifyResponseHandlers(r); + p.handleResult(r[r.length - 1]); + } catch (ConnectionException cex) { + throw new FolderClosedException(folder, cex.getMessage()); + } catch (ProtocolException pex) { + forceCheckExpunged(); + throw new MessagingException(pex.getMessage(), pex); + } + + } // Release MessageCacheLock + + if (envelope == null) + throw new MessagingException("Failed to load IMAP envelope"); + } + + /* + * Load the BODYSTRUCTURE + */ + private synchronized void loadBODYSTRUCTURE() + throws MessagingException { + if (bs != null) // already loaded + return; + + // Acquire MessageCacheLock, to freeze seqnum. + synchronized(getMessageCacheLock()) { + try { + IMAPProtocol p = getProtocol(); + + // This message could be expunged when we were waiting + // to acquire the lock ... + checkExpunged(); + + bs = p.fetchBodyStructure(getSequenceNumber()); + } catch (ConnectionException cex) { + throw new FolderClosedException(folder, cex.getMessage()); + } catch (ProtocolException pex) { + forceCheckExpunged(); + throw new MessagingException(pex.getMessage(), pex); + } + if (bs == null) { + // if the FETCH is successful, we should always get a + // BODYSTRUCTURE, but some servers fail to return it + // if the message has been expunged + forceCheckExpunged(); + throw new MessagingException("Unable to load BODYSTRUCTURE"); + } + } + } + + /* + * Load all headers. + */ + private synchronized void loadHeaders() throws MessagingException { + if (headersLoaded) + return; + + InputStream is = null; + + // Acquire MessageCacheLock, to freeze seqnum. + synchronized (getMessageCacheLock()) { + try { + IMAPProtocol p = getProtocol(); + + // This message could be expunged when we were waiting + // to acquire the lock ... + checkExpunged(); + + if (p.isREV1()) { + BODY b = p.peekBody(getSequenceNumber(), + toSection("HEADER")); + if (b != null) + is = b.getByteArrayInputStream(); + } else { + RFC822DATA rd = p.fetchRFC822(getSequenceNumber(), + "HEADER"); + if (rd != null) + is = rd.getByteArrayInputStream(); + } + } catch (ConnectionException cex) { + throw new FolderClosedException(folder, cex.getMessage()); + } catch (ProtocolException pex) { + forceCheckExpunged(); + throw new MessagingException(pex.getMessage(), pex); + } + } // Release MessageCacheLock + + if (is == null) + throw new MessagingException("Cannot load header"); + headers = new InternetHeaders(is); + headersLoaded = true; + } + + /* + * Load this message's Flags + */ + private synchronized void loadFlags() throws MessagingException { + if (flags != null) + return; + + // Acquire MessageCacheLock, to freeze seqnum. + synchronized(getMessageCacheLock()) { + try { + IMAPProtocol p = getProtocol(); + + // This message could be expunged when we were waiting + // to acquire the lock ... + checkExpunged(); + + flags = p.fetchFlags(getSequenceNumber()); + // make sure flags is always set, even if server is broken + if (flags == null) + flags = new Flags(); + } catch (ConnectionException cex) { + throw new FolderClosedException(folder, cex.getMessage()); + } catch (ProtocolException pex) { + forceCheckExpunged(); + throw new MessagingException(pex.getMessage(), pex); + } + } // Release MessageCacheLock + } + + /* + * Are all headers loaded? + */ + private boolean areHeadersLoaded() { + return headersLoaded; + } + + /* + * Set whether all headers are loaded. + */ + private void setHeadersLoaded(boolean loaded) { + headersLoaded = loaded; + } + + /* + * Check if the given header was ever loaded from the server + */ + private boolean isHeaderLoaded(String name) { + if (headersLoaded) // All headers for this message have been loaded + return true; + + return loadedHeaders.containsKey(name.toUpperCase(Locale.ENGLISH)); + } + + /* + * Mark that the given headers have been loaded from the server. + */ + private void setHeaderLoaded(String name) { + loadedHeaders.put(name.toUpperCase(Locale.ENGLISH), name); + } + + /* + * Convert the given FETCH item identifier to the approriate + * section-string for this message. + */ + private String toSection(String what) { + if (sectionId == null) + return what; + else + return sectionId + "." + what; + } + + /* + * Clone an array of InternetAddresses. + */ + private InternetAddress[] aaclone(InternetAddress[] aa) { + if (aa == null) + return null; + else + return aa.clone(); + } + + private Flags _getFlags() { + return flags; + } + + private ENVELOPE _getEnvelope() { + return envelope; + } + + private BODYSTRUCTURE _getBodyStructure() { + return bs; + } + + /*********************************************************** + * accessor routines to make available certain private/protected + * fields to other classes in this package. + ***********************************************************/ + + /* + * Called by IMAPFolder. + * Must not be synchronized. + */ + void _setFlags(Flags flags) { + this.flags = flags; + } + + /* + * Called by IMAPNestedMessage. + */ + Session _getSession() { + return session; + } +}
diff --git a/mail/src/main/java/com/sun/mail/imap/IMAPMultipartDataSource.java b/mail/src/main/java/com/sun/mail/imap/IMAPMultipartDataSource.java new file mode 100644 index 0000000..c55ef02 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/imap/IMAPMultipartDataSource.java
@@ -0,0 +1,61 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + + +import javax.mail.*; +import javax.mail.internet.*; + +import com.sun.mail.imap.protocol.*; +import java.util.ArrayList; +import java.util.List; + +/** + * This class + * + * @author John Mani + */ + +public class IMAPMultipartDataSource extends MimePartDataSource + implements MultipartDataSource { + private List<IMAPBodyPart> parts; + + protected IMAPMultipartDataSource(MimePart part, BODYSTRUCTURE[] bs, + String sectionId, IMAPMessage msg) { + super(part); + + parts = new ArrayList<>(bs.length); + for (int i = 0; i < bs.length; i++) + parts.add( + new IMAPBodyPart(bs[i], + sectionId == null ? + Integer.toString(i+1) : + sectionId + "." + Integer.toString(i+1), + msg) + ); + } + + @Override + public int getCount() { + return parts.size(); + } + + @Override + public BodyPart getBodyPart(int index) throws MessagingException { + return parts.get(index); + } +}
diff --git a/mail/src/main/java/com/sun/mail/imap/IMAPNestedMessage.java b/mail/src/main/java/com/sun/mail/imap/IMAPNestedMessage.java new file mode 100644 index 0000000..12cd4e4 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/imap/IMAPNestedMessage.java
@@ -0,0 +1,137 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.io.*; +import javax.mail.*; +import com.sun.mail.imap.protocol.*; +import com.sun.mail.iap.ProtocolException; + +/** + * This class implements a nested IMAP message + * + * @author John Mani + */ + +public class IMAPNestedMessage extends IMAPMessage { + private IMAPMessage msg; // the enclosure of this nested message + + /** + * Package private constructor. <p> + * + * Note that nested messages have no containing folder, nor + * a message number. + */ + IMAPNestedMessage(IMAPMessage m, BODYSTRUCTURE b, ENVELOPE e, String sid) { + super(m._getSession()); + msg = m; + bs = b; + envelope = e; + sectionId = sid; + setPeek(m.getPeek()); + } + + /* + * Get the enclosing message's Protocol object. Overrides + * IMAPMessage.getProtocol(). + */ + @Override + protected IMAPProtocol getProtocol() + throws ProtocolException, FolderClosedException { + return msg.getProtocol(); + } + + /* + * Is this an IMAP4 REV1 server? + */ + @Override + protected boolean isREV1() throws FolderClosedException { + return msg.isREV1(); + } + + /* + * Get the enclosing message's messageCacheLock. Overrides + * IMAPMessage.getMessageCacheLock(). + */ + @Override + protected Object getMessageCacheLock() { + return msg.getMessageCacheLock(); + } + + /* + * Get the enclosing message's sequence number. Overrides + * IMAPMessage.getSequenceNumber(). + */ + @Override + protected int getSequenceNumber() { + return msg.getSequenceNumber(); + } + + /* + * Check whether the enclosing message is expunged. Overrides + * IMAPMessage.checkExpunged(). + */ + @Override + protected void checkExpunged() throws MessageRemovedException { + msg.checkExpunged(); + } + + /* + * Check whether the enclosing message is expunged. Overrides + * Message.isExpunged(). + */ + @Override + public boolean isExpunged() { + return msg.isExpunged(); + } + + /* + * Get the enclosing message's fetchBlockSize. + */ + @Override + protected int getFetchBlockSize() { + return msg.getFetchBlockSize(); + } + + /* + * Get the enclosing message's ignoreBodyStructureSize. + */ + @Override + protected boolean ignoreBodyStructureSize() { + return msg.ignoreBodyStructureSize(); + } + + /* + * IMAPMessage uses RFC822.SIZE. We use the "size" field from + * our BODYSTRUCTURE. + */ + @Override + public int getSize() throws MessagingException { + return bs.size; + } + + /* + * Disallow setting flags on nested messages + */ + @Override + public synchronized void setFlags(Flags flag, boolean set) + throws MessagingException { + // Cannot set FLAGS on a nested IMAP message + throw new MethodNotSupportedException( + "Cannot set flags on this nested message"); + } +}
diff --git a/mail/src/main/java/com/sun/mail/imap/IMAPProvider.java b/mail/src/main/java/com/sun/mail/imap/IMAPProvider.java new file mode 100644 index 0000000..1604501 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/imap/IMAPProvider.java
@@ -0,0 +1,29 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import javax.mail.Provider; + +/** + * The IMAP protocol provider. + */ +public class IMAPProvider extends Provider { + public IMAPProvider() { + super(Provider.Type.STORE, "imap", IMAPStore.class.getName(), + "Oracle", null); + } +}
diff --git a/mail/src/main/java/com/sun/mail/imap/IMAPSSLProvider.java b/mail/src/main/java/com/sun/mail/imap/IMAPSSLProvider.java new file mode 100644 index 0000000..71fe686 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/imap/IMAPSSLProvider.java
@@ -0,0 +1,29 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import javax.mail.Provider; + +/** + * The IMAP SSL protocol provider. + */ +public class IMAPSSLProvider extends Provider { + public IMAPSSLProvider() { + super(Provider.Type.STORE, "imaps", IMAPSSLStore.class.getName(), + "Oracle", null); + } +}
diff --git a/mail/src/main/java/com/sun/mail/imap/IMAPSSLStore.java b/mail/src/main/java/com/sun/mail/imap/IMAPSSLStore.java new file mode 100644 index 0000000..488b4ec --- /dev/null +++ b/mail/src/main/java/com/sun/mail/imap/IMAPSSLStore.java
@@ -0,0 +1,37 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import javax.mail.*; + +/** + * This class provides access to an IMAP message store over SSL. + */ + +public class IMAPSSLStore extends IMAPStore { + + /** + * Constructor that takes a Session object and a URLName that + * represents a specific IMAP server. + * + * @param session the Session + * @param url the URLName of this store + */ + public IMAPSSLStore(Session session, URLName url) { + super(session, url, "imaps", true); // call super constructor + } +}
diff --git a/mail/src/main/java/com/sun/mail/imap/IMAPStore.java b/mail/src/main/java/com/sun/mail/imap/IMAPStore.java new file mode 100644 index 0000000..16b5182 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/imap/IMAPStore.java
@@ -0,0 +1,2210 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.lang.reflect.*; +import java.util.Vector; +import java.util.StringTokenizer; +import java.util.Locale; +import java.util.Properties; +import java.io.IOException; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Map; +import java.util.HashMap; +import java.util.logging.Level; + +import javax.mail.*; +import javax.mail.event.*; + +import com.sun.mail.iap.*; +import com.sun.mail.imap.protocol.*; +import com.sun.mail.util.PropUtil; +import com.sun.mail.util.MailLogger; +import com.sun.mail.util.SocketConnectException; +import com.sun.mail.util.MailConnectException; +import java.util.ArrayList; +import java.util.List; + +/** + * This class provides access to an IMAP message store. <p> + * + * Applications that need to make use of IMAP-specific features may cast + * a <code>Store</code> object to an <code>IMAPStore</code> object and + * use the methods on this class. The {@link #getQuota getQuota} and + * {@link #setQuota setQuota} methods support the IMAP QUOTA extension. + * Refer to <A HREF="http://www.ietf.org/rfc/rfc2087.txt">RFC 2087</A> + * for more information. <p> + * + * The {@link #id id} method supports the IMAP ID extension; + * see <A HREF="http://www.ietf.org/rfc/rfc2971.txt">RFC 2971</A>. + * The fields ID_NAME, ID_VERSION, etc. represent the suggested field names + * in RFC 2971 section 3.3 and may be used as keys in the Map containing + * client values or server values. <p> + * + * See the <a href="package-summary.html">com.sun.mail.imap</a> package + * documentation for further information on the IMAP protocol provider. <p> + * + * <strong>WARNING:</strong> The APIs unique to this class should be + * considered <strong>EXPERIMENTAL</strong>. They may be changed in the + * future in ways that are incompatible with applications using the + * current APIs. + * + * @author John Mani + * @author Bill Shannon + * @author Jim Glennon + */ +/* + * This package is implemented over the "imap.protocol" package, which + * implements the protocol-level commands. <p> + * + * A connected IMAPStore maintains a pool of IMAP protocol objects for + * use in communicating with the IMAP server. The IMAPStore will create + * the initial AUTHENTICATED connection and seed the pool with this + * connection. As folders are opened and new IMAP protocol objects are + * needed, the IMAPStore will provide them from the connection pool, + * or create them if none are available. When a folder is closed, + * its IMAP protocol object is returned to the connection pool if the + * pool is not over capacity. The pool size can be configured by setting + * the mail.imap.connectionpoolsize property. <p> + * + * Note that all connections in the connection pool have their response + * handler set to be the Store. When the connection is removed from the + * pool for use by a folder, the response handler is removed and then set + * to either the Folder or to the special nonStoreResponseHandler, depending + * on how the connection is being used. This is probably excessive. + * Better would be for the Protocol object to support only a single + * response handler, which would be set before the connection is used + * and cleared when the connection is in the pool and can't be used. <p> + * + * A mechanism is provided for timing out idle connection pool IMAP + * protocol objects. Timed out connections are closed and removed (pruned) + * from the connection pool. The time out interval can be configured via + * the mail.imap.connectionpooltimeout property. <p> + * + * The connected IMAPStore object may or may not maintain a separate IMAP + * protocol object that provides the store a dedicated connection to the + * IMAP server. This is provided mainly for compatibility with previous + * implementations of JavaMail and is determined by the value of the + * mail.imap.separatestoreconnection property. <p> + * + * An IMAPStore object provides closed IMAPFolder objects thru its list() + * and listSubscribed() methods. A closed IMAPFolder object acquires an + * IMAP protocol object from the store to communicate with the server. When + * the folder is opened, it gets its own protocol object and thus its own, + * separate connection to the server. The store maintains references to + * all 'open' folders. When a folder is/gets closed, the store removes + * it from its list. When the store is/gets closed, it closes all open + * folders in its list, thus cleaning up all open connections to the + * server. <p> + * + * A mutex is used to control access to the connection pool resources. + * Any time any of these resources need to be accessed, the following + * convention should be followed: + * + * synchronized (pool) { // ACQUIRE LOCK + * // access connection pool resources + * } // RELEASE LOCK <p> + * + * The locking relationship between the store and folders is that the + * store lock must be acquired before a folder lock. This is currently only + * applicable in the store's cleanup method. It's important that the + * connection pool lock is not held when calling into folder objects. + * The locking hierarchy is that a folder lock must be acquired before + * any connection pool operations are performed. You never need to hold + * all three locks, but if you hold more than one this is the order you + * have to acquire them in. <p> + * + * That is: Store > Folder, Folder > pool, Store > pool <p> + * + * The IMAPStore implements the ResponseHandler interface and listens to + * BYE or untagged OK-notification events from the server as a result of + * Store operations. IMAPFolder forwards notifications that result from + * Folder operations using the store connection; the IMAPStore ResponseHandler + * is not used directly in this case. <p> + */ + +public class IMAPStore extends Store + implements QuotaAwareStore, ResponseHandler { + + /** + * A special event type for a StoreEvent to indicate an IMAP + * response, if the mail.imap.enableimapevents property is set. + */ + public static final int RESPONSE = 1000; + + public static final String ID_NAME = "name"; + public static final String ID_VERSION = "version"; + public static final String ID_OS = "os"; + public static final String ID_OS_VERSION = "os-version"; + public static final String ID_VENDOR = "vendor"; + public static final String ID_SUPPORT_URL = "support-url"; + public static final String ID_ADDRESS = "address"; + public static final String ID_DATE = "date"; + public static final String ID_COMMAND = "command"; + public static final String ID_ARGUMENTS = "arguments"; + public static final String ID_ENVIRONMENT = "environment"; + + protected final String name; // name of this protocol + protected final int defaultPort; // default IMAP port + protected final boolean isSSL; // use SSL? + + private final int blksize; // Block size for data requested + // in FETCH requests. Defaults to + // 16K + + private boolean ignoreSize; // ignore the size in BODYSTRUCTURE? + + private final int statusCacheTimeout; // cache Status for 1 second + + private final int appendBufferSize; // max size of msg buffered for append + + private final int minIdleTime; // minimum idle time + + private volatile int port = -1; // port to use + + // Auth info + protected String host; + protected String user; + protected String password; + protected String proxyAuthUser; + protected String authorizationID; + protected String saslRealm; + + private Namespaces namespaces; + + private boolean enableStartTLS = false; // enable STARTTLS + private boolean requireStartTLS = false; // require STARTTLS + private boolean usingSSL = false; // using SSL? + private boolean enableSASL = false; // enable SASL authentication + private String[] saslMechanisms; + private boolean forcePasswordRefresh = false; + // enable notification of IMAP responses + private boolean enableResponseEvents = false; + // enable notification of IMAP responses during IDLE + private boolean enableImapEvents = false; + private String guid; // for Yahoo! Mail IMAP + private boolean throwSearchException = false; + private boolean peek = false; + private boolean closeFoldersOnStoreFailure = true; + private boolean enableCompress = false; // enable COMPRESS=DEFLATE + private boolean finalizeCleanClose = false; + + /* + * This field is set in the Store's response handler if we see + * a BYE response. The releaseStore method checks this field + * and if set it cleans up the Store. Field is volatile because + * there's no lock we consistently hold while manipulating it. + * + * Because volatile doesn't really work before JDK 1.5, + * use a lock to protect these two fields. + */ + private volatile boolean connectionFailed = false; + private volatile boolean forceClose = false; + private final Object connectionFailedLock = new Object(); + + private boolean debugusername; // include username in debug output? + private boolean debugpassword; // include password in debug output? + protected MailLogger logger; // for debug output + + private boolean messageCacheDebug; + + // constructors for IMAPFolder class provided by user + private volatile Constructor<?> folderConstructor = null; + private volatile Constructor<?> folderConstructorLI = null; + + // Connection pool info + + static class ConnectionPool { + + // container for the pool's IMAP protocol objects + private Vector<IMAPProtocol> authenticatedConnections + = new Vector<>(); + + // vectore of open folders + private Vector<IMAPFolder> folders; + + // is the store connection being used? + private boolean storeConnectionInUse = false; + + // the last time (in millis) the pool was checked for timed out + // connections + private long lastTimePruned; + + // flag to indicate whether there is a dedicated connection for + // store commands + private final boolean separateStoreConnection; + + // client timeout interval + private final long clientTimeoutInterval; + + // server timeout interval + private final long serverTimeoutInterval; + + // size of the connection pool + private final int poolSize; + + // interval for checking for timed out connections + private final long pruningInterval; + + // connection pool logger + private final MailLogger logger; + + /* + * The idleState field supports the IDLE command. + * Normally when executing an IMAP command we hold the + * store's lock. + * While executing the IDLE command we can't hold the + * lock or it would prevent other threads from + * entering Store methods even far enough to check whether + * an IDLE command is in progress. We need to check before + * issuing another command so that we can abort the IDLE + * command. + * + * The idleState field is protected by the store's lock. + * The RUNNING state is the normal state and means no IDLE + * command is in progress. The IDLE state means we've issued + * an IDLE command and are reading responses. The ABORTING + * state means we've sent the DONE continuation command and + * are waiting for the thread running the IDLE command to + * break out of its read loop. + * + * When an IDLE command is in progress, the thread calling + * the idle method will be reading from the IMAP connection + * while not holding the store's lock. + * It's obviously critical that no other thread try to send a + * command or read from the connection while in this state. + * However, other threads can send the DONE continuation + * command that will cause the server to break out of the IDLE + * loop and send the ending tag response to the IDLE command. + * The thread in the idle method that's reading the responses + * from the IDLE command will see this ending response and + * complete the idle method, setting the idleState field back + * to RUNNING, and notifying any threads waiting to use the + * connection. + * + * All uses of the IMAP connection (IMAPProtocol object) must + * be preceeded by a check to make sure an IDLE command is not + * running, and abort the IDLE command if necessary. This check + * is made while holding the connection pool lock. While + * waiting for the IDLE command to complete, these other threads + * will give up the connection pool lock. This check is done by + * the getStoreProtocol() method. + */ + private static final int RUNNING = 0; // not doing IDLE command + private static final int IDLE = 1; // IDLE command in effect + private static final int ABORTING = 2; // IDLE command aborting + private int idleState = RUNNING; + private IMAPProtocol idleProtocol; // protocol object when IDLE + + ConnectionPool(String name, MailLogger plogger, Session session) { + lastTimePruned = System.currentTimeMillis(); + Properties props = session.getProperties(); + + boolean debug = PropUtil.getBooleanProperty(props, + "mail." + name + ".connectionpool.debug", false); + logger = plogger.getSubLogger("connectionpool", + "DEBUG IMAP CP", debug); + + // check if the default connection pool size is overridden + int size = PropUtil.getIntProperty(props, + "mail." + name + ".connectionpoolsize", -1); + if (size > 0) { + poolSize = size; + if (logger.isLoggable(Level.CONFIG)) + logger.config("mail.imap.connectionpoolsize: " + poolSize); + } else + poolSize = 1; + + // check if the default client-side timeout value is overridden + int connectionPoolTimeout = PropUtil.getIntProperty(props, + "mail." + name + ".connectionpooltimeout", -1); + if (connectionPoolTimeout > 0) { + clientTimeoutInterval = connectionPoolTimeout; + if (logger.isLoggable(Level.CONFIG)) + logger.config("mail.imap.connectionpooltimeout: " + + clientTimeoutInterval); + } else + clientTimeoutInterval = 45 * 1000; // 45 seconds + + // check if the default server-side timeout value is overridden + int serverTimeout = PropUtil.getIntProperty(props, + "mail." + name + ".servertimeout", -1); + if (serverTimeout > 0) { + serverTimeoutInterval = serverTimeout; + if (logger.isLoggable(Level.CONFIG)) + logger.config("mail.imap.servertimeout: " + + serverTimeoutInterval); + } else + serverTimeoutInterval = 30 * 60 * 1000; // 30 minutes + + // check if the default server-side timeout value is overridden + int pruning = PropUtil.getIntProperty(props, + "mail." + name + ".pruninginterval", -1); + if (pruning > 0) { + pruningInterval = pruning; + if (logger.isLoggable(Level.CONFIG)) + logger.config("mail.imap.pruninginterval: " + + pruningInterval); + } else + pruningInterval = 60 * 1000; // 1 minute + + // check to see if we should use a separate (i.e. dedicated) + // store connection + separateStoreConnection = + PropUtil.getBooleanProperty(props, + "mail." + name + ".separatestoreconnection", false); + if (separateStoreConnection) + logger.config("dedicate a store connection"); + + } + } + + private final ConnectionPool pool; + + /** + * A special response handler for connections that are being used + * to perform operations on behalf of an object other than the Store. + * It DOESN'T cause the Store to be cleaned up if a BYE is seen. + * The BYE may be real or synthetic and in either case just indicates + * that the connection is dead. + */ + private ResponseHandler nonStoreResponseHandler = new ResponseHandler() { + @Override + public void handleResponse(Response r) { + // Any of these responses may have a response code. + if (r.isOK() || r.isNO() || r.isBAD() || r.isBYE()) + handleResponseCode(r); + if (r.isBYE()) + logger.fine("IMAPStore non-store connection dead"); + } + }; + + /** + * Constructor that takes a Session object and a URLName that + * represents a specific IMAP server. + * + * @param session the Session + * @param url the URLName of this store + */ + public IMAPStore(Session session, URLName url) { + this(session, url, "imap", false); + } + + /** + * Constructor used by this class and by IMAPSSLStore subclass. + * + * @param session the Session + * @param url the URLName of this store + * @param name the protocol name for this store + * @param isSSL use SSL? + */ + protected IMAPStore(Session session, URLName url, + String name, boolean isSSL) { + super(session, url); // call super constructor + Properties props = session.getProperties(); + + if (url != null) + name = url.getProtocol(); + this.name = name; + if (!isSSL) + isSSL = PropUtil.getBooleanProperty(props, + "mail." + name + ".ssl.enable", false); + if (isSSL) + this.defaultPort = 993; + else + this.defaultPort = 143; + this.isSSL = isSSL; + + debug = session.getDebug(); + debugusername = PropUtil.getBooleanProperty(props, + "mail.debug.auth.username", true); + debugpassword = PropUtil.getBooleanProperty(props, + "mail.debug.auth.password", false); + logger = new MailLogger(this.getClass(), + "DEBUG " + name.toUpperCase(Locale.ENGLISH), + session.getDebug(), session.getDebugOut()); + + boolean partialFetch = PropUtil.getBooleanProperty(props, + "mail." + name + ".partialfetch", true); + if (!partialFetch) { + blksize = -1; + logger.config("mail.imap.partialfetch: false"); + } else { + blksize = PropUtil.getIntProperty(props, + "mail." + name +".fetchsize", 1024 * 16); + if (logger.isLoggable(Level.CONFIG)) + logger.config("mail.imap.fetchsize: " + blksize); + } + + ignoreSize = PropUtil.getBooleanProperty(props, + "mail." + name +".ignorebodystructuresize", false); + if (logger.isLoggable(Level.CONFIG)) + logger.config("mail.imap.ignorebodystructuresize: " + ignoreSize); + + statusCacheTimeout = PropUtil.getIntProperty(props, + "mail." + name + ".statuscachetimeout", 1000); + if (logger.isLoggable(Level.CONFIG)) + logger.config("mail.imap.statuscachetimeout: " + + statusCacheTimeout); + + appendBufferSize = PropUtil.getIntProperty(props, + "mail." + name + ".appendbuffersize", -1); + if (logger.isLoggable(Level.CONFIG)) + logger.config("mail.imap.appendbuffersize: " + appendBufferSize); + + minIdleTime = PropUtil.getIntProperty(props, + "mail." + name + ".minidletime", 10); + if (logger.isLoggable(Level.CONFIG)) + logger.config("mail.imap.minidletime: " + minIdleTime); + + // check if we should do a PROXYAUTH login + String s = session.getProperty("mail." + name + ".proxyauth.user"); + if (s != null) { + proxyAuthUser = s; + if (logger.isLoggable(Level.CONFIG)) + logger.config("mail.imap.proxyauth.user: " + proxyAuthUser); + } + + // check if STARTTLS is enabled + enableStartTLS = PropUtil.getBooleanProperty(props, + "mail." + name + ".starttls.enable", false); + if (enableStartTLS) + logger.config("enable STARTTLS"); + + // check if STARTTLS is required + requireStartTLS = PropUtil.getBooleanProperty(props, + "mail." + name + ".starttls.required", false); + if (requireStartTLS) + logger.config("require STARTTLS"); + + // check if SASL is enabled + enableSASL = PropUtil.getBooleanProperty(props, + "mail." + name + ".sasl.enable", false); + if (enableSASL) + logger.config("enable SASL"); + + // check if SASL mechanisms are specified + if (enableSASL) { + s = session.getProperty("mail." + name + ".sasl.mechanisms"); + if (s != null && s.length() > 0) { + if (logger.isLoggable(Level.CONFIG)) + logger.config("SASL mechanisms allowed: " + s); + List<String> v = new ArrayList<>(5); + StringTokenizer st = new StringTokenizer(s, " ,"); + while (st.hasMoreTokens()) { + String m = st.nextToken(); + if (m.length() > 0) + v.add(m); + } + saslMechanisms = new String[v.size()]; + v.toArray(saslMechanisms); + } + } + + // check if an authorization ID has been specified + s = session.getProperty("mail." + name + ".sasl.authorizationid"); + if (s != null) { + authorizationID = s; + logger.log(Level.CONFIG, "mail.imap.sasl.authorizationid: {0}", + authorizationID); + } + + // check if a SASL realm has been specified + s = session.getProperty("mail." + name + ".sasl.realm"); + if (s != null) { + saslRealm = s; + logger.log(Level.CONFIG, "mail.imap.sasl.realm: {0}", saslRealm); + } + + // check if forcePasswordRefresh is enabled + forcePasswordRefresh = PropUtil.getBooleanProperty(props, + "mail." + name + ".forcepasswordrefresh", false); + if (forcePasswordRefresh) + logger.config("enable forcePasswordRefresh"); + + // check if enableimapevents is enabled + enableResponseEvents = PropUtil.getBooleanProperty(props, + "mail." + name + ".enableresponseevents", false); + if (enableResponseEvents) + logger.config("enable IMAP response events"); + + // check if enableresponseevents is enabled + enableImapEvents = PropUtil.getBooleanProperty(props, + "mail." + name + ".enableimapevents", false); + if (enableImapEvents) + logger.config("enable IMAP IDLE events"); + + // check if message cache debugging set + messageCacheDebug = PropUtil.getBooleanProperty(props, + "mail." + name + ".messagecache.debug", false); + + guid = session.getProperty("mail." + name + ".yahoo.guid"); + if (guid != null) + logger.log(Level.CONFIG, "mail.imap.yahoo.guid: {0}", guid); + + // check if throwsearchexception is enabled + throwSearchException = PropUtil.getBooleanProperty(props, + "mail." + name + ".throwsearchexception", false); + if (throwSearchException) + logger.config("throw SearchException"); + + // check if peek is set + peek = PropUtil.getBooleanProperty(props, + "mail." + name + ".peek", false); + if (peek) + logger.config("peek"); + + // check if closeFoldersOnStoreFailure is set + closeFoldersOnStoreFailure = PropUtil.getBooleanProperty(props, + "mail." + name + ".closefoldersonstorefailure", true); + if (closeFoldersOnStoreFailure) + logger.config("closeFoldersOnStoreFailure"); + + // check if COMPRESS is enabled + enableCompress = PropUtil.getBooleanProperty(props, + "mail." + name + ".compress.enable", false); + if (enableCompress) + logger.config("enable COMPRESS"); + + // check if finalizeCleanClose is enabled + finalizeCleanClose = PropUtil.getBooleanProperty(props, + "mail." + name + ".finalizecleanclose", false); + if (finalizeCleanClose) + logger.config("close connection cleanly in finalize"); + + s = session.getProperty("mail." + name + ".folder.class"); + if (s != null) { + logger.log(Level.CONFIG, "IMAP: folder class: {0}", s); + try { + ClassLoader cl = this.getClass().getClassLoader(); + + // now load the class + Class<?> folderClass = null; + try { + // First try the "application's" class loader. + // This should eventually be replaced by + // Thread.currentThread().getContextClassLoader(). + folderClass = Class.forName(s, false, cl); + } catch (ClassNotFoundException ex1) { + // That didn't work, now try the "system" class loader. + // (Need both of these because JDK 1.1 class loaders + // may not delegate to their parent class loader.) + folderClass = Class.forName(s); + } + + Class<?>[] c = { String.class, char.class, IMAPStore.class, + Boolean.class }; + folderConstructor = folderClass.getConstructor(c); + Class<?>[] c2 = { ListInfo.class, IMAPStore.class }; + folderConstructorLI = folderClass.getConstructor(c2); + } catch (Exception ex) { + logger.log(Level.CONFIG, + "IMAP: failed to load folder class", ex); + } + } + + pool = new ConnectionPool(name, logger, session); + } + + /** + * Implementation of protocolConnect(). Will create a connection + * to the server and authenticate the user using the mechanisms + * specified by various properties. <p> + * + * The <code>host</code>, <code>user</code>, and <code>password</code> + * parameters must all be non-null. If the authentication mechanism + * being used does not require a password, an empty string or other + * suitable dummy password should be used. + */ + @Override + protected synchronized boolean + protocolConnect(String host, int pport, String user, String password) + throws MessagingException { + + IMAPProtocol protocol = null; + + // check for non-null values of host, password, user + if (host == null || password == null || user == null) { + if (logger.isLoggable(Level.FINE)) + logger.fine("protocolConnect returning false" + + ", host=" + host + + ", user=" + traceUser(user) + + ", password=" + tracePassword(password)); + return false; + } + + // set the port correctly + if (pport != -1) { + port = pport; + } else { + port = PropUtil.getIntProperty(session.getProperties(), + "mail." + name + ".port", port); + } + + // use the default if needed + if (port == -1) { + port = defaultPort; + } + + try { + boolean poolEmpty; + synchronized (pool) { + poolEmpty = pool.authenticatedConnections.isEmpty(); + } + + if (poolEmpty) { + if (logger.isLoggable(Level.FINE)) + logger.fine("trying to connect to host \"" + host + + "\", port " + port + ", isSSL " + isSSL); + protocol = newIMAPProtocol(host, port); + if (logger.isLoggable(Level.FINE)) + logger.fine("protocolConnect login" + + ", host=" + host + + ", user=" + traceUser(user) + + ", password=" + tracePassword(password)); + protocol.addResponseHandler(nonStoreResponseHandler); + login(protocol, user, password); + protocol.removeResponseHandler(nonStoreResponseHandler); + protocol.addResponseHandler(this); + + usingSSL = protocol.isSSL(); // in case anyone asks + + this.host = host; + this.user = user; + this.password = password; + + synchronized (pool) { + pool.authenticatedConnections.addElement(protocol); + } + } + } catch (IMAPReferralException ex) { + // login failure due to IMAP REFERRAL, close connection to server + if (protocol != null) + protocol.disconnect(); + protocol = null; + throw new ReferralException(ex.getUrl(), ex.getMessage()); + } catch (CommandFailedException cex) { + // login failure, close connection to server + if (protocol != null) + protocol.disconnect(); + protocol = null; + Response r = cex.getResponse(); + throw new AuthenticationFailedException( + r != null ? r.getRest() : cex.getMessage()); + } catch (ProtocolException pex) { // any other exception + // failure in login command, close connection to server + if (protocol != null) + protocol.disconnect(); + protocol = null; + throw new MessagingException(pex.getMessage(), pex); + } catch (SocketConnectException scex) { + throw new MailConnectException(scex); + } catch (IOException ioex) { + throw new MessagingException(ioex.getMessage(), ioex); + } + + return true; + } + + /** + * Create an IMAPProtocol object connected to the host and port. + * Subclasses of IMAPStore may override this method to return a + * subclass of IMAPProtocol that supports product-specific extensions. + * + * @param host the host name + * @param port the port number + * @return the new IMAPProtocol object + * @exception IOException for I/O errors + * @exception ProtocolException for protocol errors + * @since JavaMail 1.4.6 + */ + protected IMAPProtocol newIMAPProtocol(String host, int port) + throws IOException, ProtocolException { + return new IMAPProtocol(name, host, port, + session.getProperties(), + isSSL, + logger + ); + } + + private void login(IMAPProtocol p, String u, String pw) + throws ProtocolException { + // turn on TLS if it's been enabled or required and is supported + // and we're not already using SSL + if ((enableStartTLS || requireStartTLS) && !p.isSSL()) { + if (p.hasCapability("STARTTLS")) { + p.startTLS(); + // if startTLS succeeds, refresh capabilities + p.capability(); + } else if (requireStartTLS) { + logger.fine("STARTTLS required but not supported by server"); + throw new ProtocolException( + "STARTTLS required but not supported by server"); + } + } + if (p.isAuthenticated()) + return; // no need to login + + // allow subclasses to issue commands before login + preLogin(p); + + // issue special ID command to Yahoo! Mail IMAP server + // http://en.wikipedia.org/wiki/Yahoo%21_Mail#Free_IMAP_and_SMTPs_access + if (guid != null) { + Map<String,String> gmap = new HashMap<>(); + gmap.put("GUID", guid); + p.id(gmap); + } + + /* + * Put a special "marker" in the capabilities list so we can + * detect if the server refreshed the capabilities in the OK + * response. + */ + p.getCapabilities().put("__PRELOGIN__", ""); + String authzid; + if (authorizationID != null) + authzid = authorizationID; + else if (proxyAuthUser != null) + authzid = proxyAuthUser; + else + authzid = null; + + if (enableSASL) { + try { + p.sasllogin(saslMechanisms, saslRealm, authzid, u, pw); + if (!p.isAuthenticated()) + throw new CommandFailedException( + "SASL authentication failed"); + } catch (UnsupportedOperationException ex) { + // continue to try other authentication methods below + } + } + + if (!p.isAuthenticated()) + authenticate(p, authzid, u, pw); + + if (proxyAuthUser != null) + p.proxyauth(proxyAuthUser); + + /* + * If marker is still there, capabilities haven't been refreshed, + * refresh them now. + */ + if (p.hasCapability("__PRELOGIN__")) { + try { + p.capability(); + } catch (ConnectionException cex) { + throw cex; // rethrow connection failures + // XXX - assume connection has been closed + } catch (ProtocolException pex) { + // ignore other exceptions that "should never happen" + } + } + + if (enableCompress) { + if (p.hasCapability("COMPRESS=DEFLATE")) { + p.compress(); + } + } + + // if server supports UTF-8, enable it for client use + // note that this is safe to enable even if mail.mime.allowutf8=false + if (p.hasCapability("UTF8=ACCEPT") || p.hasCapability("UTF8=ONLY")) + p.enable("UTF8=ACCEPT"); + } + + /** + * Authenticate using one of the non-SASL mechanisms. + * + * @param p the IMAPProtocol object + * @param authzid the authorization ID + * @param user the user name + * @param password the password + * @exception ProtocolException on failures + */ + private void authenticate(IMAPProtocol p, String authzid, + String user, String password) + throws ProtocolException { + // this list must match the "if" statements below + String defaultAuthenticationMechanisms = "PLAIN LOGIN NTLM XOAUTH2"; + + // setting mail.imap.auth.mechanisms controls which mechanisms will + // be used, and in what order they'll be considered. only the first + // match is used. + String mechs = session.getProperty("mail." + name + ".auth.mechanisms"); + + if (mechs == null) + mechs = defaultAuthenticationMechanisms; + + /* + * Loop through the list of mechanisms supplied by the user + * (or defaulted) and try each in turn. If the server supports + * the mechanism and we have an authenticator for the mechanism, + * and it hasn't been disabled, use it. + */ + StringTokenizer st = new StringTokenizer(mechs); + while (st.hasMoreTokens()) { + String m = st.nextToken(); + m = m.toUpperCase(Locale.ENGLISH); + + /* + * If using the default mechanisms, check if this one is disabled. + */ + if (mechs == defaultAuthenticationMechanisms) { + String dprop = "mail." + name + ".auth." + + m.toLowerCase(Locale.ENGLISH) + ".disable"; + boolean disabled = PropUtil.getBooleanProperty( + session.getProperties(), + dprop, m.equals("XOAUTH2")); + if (disabled) { + if (logger.isLoggable(Level.FINE)) + logger.fine("mechanism " + m + + " disabled by property: " + dprop); + continue; + } + } + + if (!(p.hasCapability("AUTH=" + m) || + (m.equals("LOGIN") && p.hasCapability("AUTH-LOGIN")))) { + logger.log(Level.FINE, "mechanism {0} not supported by server", + m); + continue; + } + + if (m.equals("PLAIN")) + p.authplain(authzid, user, password); + else if (m.equals("LOGIN")) + p.authlogin(user, password); + else if (m.equals("NTLM")) + p.authntlm(authzid, user, password); + else if (m.equals("XOAUTH2")) + p.authoauth2(user, password); + else { + logger.log(Level.FINE, "no authenticator for mechanism {0}", m); + continue; + } + return; + } + + if (!p.hasCapability("LOGINDISABLED")) { + p.login(user, password); + return; + } + + throw new ProtocolException("No login methods supported!"); + } + + /** + * This method is called after the connection is made and + * TLS is started (if needed), but before any authentication + * is attempted. Subclasses can override this method to + * issue commands that are needed in the "not authenticated" + * state. Note that if the connection is pre-authenticated, + * this method won't be called. <p> + * + * The implementation of this method in this class does nothing. + * + * @param p the IMAPProtocol connection + * @exception ProtocolException for protocol errors + * @since JavaMail 1.4.4 + */ + protected void preLogin(IMAPProtocol p) throws ProtocolException { + } + + /** + * Does this IMAPStore use SSL when connecting to the server? + * + * @return true if using SSL + * @since JavaMail 1.4.6 + */ + public synchronized boolean isSSL() { + return usingSSL; + } + + /** + * Set the user name that will be used for subsequent connections + * after this Store is first connected (for example, when creating + * a connection to open a Folder). This value is overridden + * by any call to the Store's connect method. <p> + * + * Some IMAP servers may provide an authentication ID that can + * be used for more efficient authentication for future connections. + * This authentication ID is provided in a server-specific manner + * not described here. <p> + * + * Most applications will never need to use this method. + * + * @param user the user name for the store + * @since JavaMail 1.3.3 + */ + public synchronized void setUsername(String user) { + this.user = user; + } + + /** + * Set the password that will be used for subsequent connections + * after this Store is first connected (for example, when creating + * a connection to open a Folder). This value is overridden + * by any call to the Store's connect method. <p> + * + * Most applications will never need to use this method. + * + * @param password the password for the store + * @since JavaMail 1.3.3 + */ + public synchronized void setPassword(String password) { + this.password = password; + } + + /* + * Get a new authenticated protocol object for this Folder. + * Also store a reference to this folder in our list of + * open folders. + */ + IMAPProtocol getProtocol(IMAPFolder folder) + throws MessagingException { + IMAPProtocol p = null; + + // keep looking for a connection until we get a good one + while (p == null) { + + // New authenticated protocol objects are either acquired + // from the connection pool, or created when the pool is + // empty or no connections are available. None are available + // if the current pool size is one and the separate store + // property is set or the connection is in use. + + synchronized (pool) { + + // If there's none available in the pool, + // create a new one. + if (pool.authenticatedConnections.isEmpty() || + (pool.authenticatedConnections.size() == 1 && + (pool.separateStoreConnection || pool.storeConnectionInUse))) { + + logger.fine("no connections in the pool, creating a new one"); + try { + if (forcePasswordRefresh) + refreshPassword(); + // Use cached host, port and timeout values. + p = newIMAPProtocol(host, port); + p.addResponseHandler(nonStoreResponseHandler); + // Use cached auth info + login(p, user, password); + p.removeResponseHandler(nonStoreResponseHandler); + } catch(Exception ex1) { + if (p != null) + try { + p.disconnect(); + } catch (Exception ex2) { } + p = null; + } + + if (p == null) + throw new MessagingException("connection failure"); + } else { + if (logger.isLoggable(Level.FINE)) + logger.fine("connection available -- size: " + + pool.authenticatedConnections.size()); + + // remove the available connection from the Authenticated queue + p = pool.authenticatedConnections.lastElement(); + pool.authenticatedConnections.removeElement(p); + + // check if the connection is still live + long lastUsed = System.currentTimeMillis() - p.getTimestamp(); + if (lastUsed > pool.serverTimeoutInterval) { + try { + /* + * Swap in a special response handler that will handle + * alerts, but won't cause the store to be closed and + * cleaned up if the connection is dead. + */ + p.removeResponseHandler(this); + p.addResponseHandler(nonStoreResponseHandler); + p.noop(); + p.removeResponseHandler(nonStoreResponseHandler); + p.addResponseHandler(this); + } catch (ProtocolException pex) { + try { + p.removeResponseHandler(nonStoreResponseHandler); + p.disconnect(); + } catch (RuntimeException ignored) { + // don't let any exception stop us + } + p = null; + continue; // try again, from the top + } + } + + // if proxyAuthUser has changed, switch to new user + if (proxyAuthUser != null && + !proxyAuthUser.equals(p.getProxyAuthUser()) && + p.hasCapability("X-UNAUTHENTICATE")) { + try { + /* + * Swap in a special response handler that will handle + * alerts, but won't cause the store to be closed and + * cleaned up if the connection is dead. + */ + p.removeResponseHandler(this); + p.addResponseHandler(nonStoreResponseHandler); + p.unauthenticate(); + login(p, user, password); + p.removeResponseHandler(nonStoreResponseHandler); + p.addResponseHandler(this); + } catch (ProtocolException pex) { + try { + p.removeResponseHandler(nonStoreResponseHandler); + p.disconnect(); + } catch (RuntimeException ignored) { + // don't let any exception stop us + } + p = null; + continue; // try again, from the top + } + } + + // remove the store as a response handler. + p.removeResponseHandler(this); + } + + // check if we need to look for client-side timeouts + timeoutConnections(); + + // Add folder to folder-list + if (folder != null) { + if (pool.folders == null) + pool.folders = new Vector<>(); + pool.folders.addElement(folder); + } + } + + } + + return p; + } + + /** + * Get this Store's protocol connection. + * + * When acquiring a store protocol object, it is important to + * use the following steps: + * + * IMAPProtocol p = null; + * try { + * p = getStoreProtocol(); + * // perform the command + * } catch (ConnectionException cex) { + * throw new StoreClosedException(this, cex.getMessage()); + * } catch (WhateverException ex) { + * // handle it + * } finally { + * releaseStoreProtocol(p); + * } + */ + private IMAPProtocol getStoreProtocol() throws ProtocolException { + IMAPProtocol p = null; + + while (p == null) { + synchronized (pool) { + waitIfIdle(); + + // If there's no authenticated connections available create a + // new one and place it in the authenticated queue. + if (pool.authenticatedConnections.isEmpty()) { + pool.logger.fine("getStoreProtocol() - no connections " + + "in the pool, creating a new one"); + try { + if (forcePasswordRefresh) + refreshPassword(); + // Use cached host, port and timeout values. + p = newIMAPProtocol(host, port); + // Use cached auth info + login(p, user, password); + } catch(Exception ex1) { + if (p != null) + try { + p.logout(); + } catch (Exception ex2) { } + p = null; + } + + if (p == null) + throw new ConnectionException( + "failed to create new store connection"); + + p.addResponseHandler(this); + pool.authenticatedConnections.addElement(p); + + } else { + // Always use the first element in the Authenticated queue. + if (pool.logger.isLoggable(Level.FINE)) + pool.logger.fine("getStoreProtocol() - " + + "connection available -- size: " + + pool.authenticatedConnections.size()); + p = pool.authenticatedConnections.firstElement(); + + // if proxyAuthUser has changed, switch to new user + if (proxyAuthUser != null && + !proxyAuthUser.equals(p.getProxyAuthUser()) && + p.hasCapability("X-UNAUTHENTICATE")) { + p.unauthenticate(); + login(p, user, password); + } + } + + if (pool.storeConnectionInUse) { + try { + // someone else is using the connection, give up + // and wait until they're done + p = null; + pool.wait(); + } catch (InterruptedException ex) { + // restore the interrupted state, which callers might + // depend on + Thread.currentThread().interrupt(); + // don't keep looking for a connection if we've been + // interrupted + throw new ProtocolException( + "Interrupted getStoreProtocol", ex); + } + } else { + pool.storeConnectionInUse = true; + + pool.logger.fine("getStoreProtocol() -- storeConnectionInUse"); + } + + timeoutConnections(); + } + } + return p; + } + + /** + * Get a store protocol object for use by a folder. + */ + IMAPProtocol getFolderStoreProtocol() throws ProtocolException { + IMAPProtocol p = getStoreProtocol(); + p.removeResponseHandler(this); + p.addResponseHandler(nonStoreResponseHandler); + return p; + } + + /* + * Some authentication systems use one time passwords + * or tokens, so each authentication request requires + * a new password. This "kludge" allows a callback + * to application code to get a new password. + * + * XXX - remove this when SASL support is added + */ + private void refreshPassword() { + if (logger.isLoggable(Level.FINE)) + logger.fine("refresh password, user: " + traceUser(user)); + InetAddress addr; + try { + addr = InetAddress.getByName(host); + } catch (UnknownHostException e) { + addr = null; + } + PasswordAuthentication pa = + session.requestPasswordAuthentication(addr, port, + name, null, user); + if (pa != null) { + user = pa.getUserName(); + password = pa.getPassword(); + } + } + + /** + * If a SELECT succeeds, but indicates that the folder is + * READ-ONLY, and the user asked to open the folder READ_WRITE, + * do we allow the open to succeed? + */ + boolean allowReadOnlySelect() { + return PropUtil.getBooleanProperty(session.getProperties(), + "mail." + name + ".allowreadonlyselect", false); + } + + /** + * Report whether the separateStoreConnection is set. + */ + boolean hasSeparateStoreConnection() { + return pool.separateStoreConnection; + } + + /** + * Return the connection pool logger. + */ + MailLogger getConnectionPoolLogger() { + return pool.logger; + } + + /** + * Report whether message cache debugging is enabled. + */ + boolean getMessageCacheDebug() { + return messageCacheDebug; + } + + /** + * Report whether the connection pool is full. + */ + boolean isConnectionPoolFull() { + + synchronized (pool) { + if (pool.logger.isLoggable(Level.FINE)) + pool.logger.fine("connection pool current size: " + + pool.authenticatedConnections.size() + + " pool size: " + pool.poolSize); + + return (pool.authenticatedConnections.size() >= pool.poolSize); + + } + } + + /** + * Release the protocol object back to the connection pool. + */ + void releaseProtocol(IMAPFolder folder, IMAPProtocol protocol) { + + synchronized (pool) { + if (protocol != null) { + // If the pool is not full, add the store as a response handler + // and return the protocol object to the connection pool. + if (!isConnectionPoolFull()) { + protocol.addResponseHandler(this); + pool.authenticatedConnections.addElement(protocol); + + if (logger.isLoggable(Level.FINE)) + logger.fine( + "added an Authenticated connection -- size: " + + pool.authenticatedConnections.size()); + } else { + logger.fine( + "pool is full, not adding an Authenticated connection"); + try { + protocol.logout(); + } catch (ProtocolException pex) {}; + } + } + + if (pool.folders != null) + pool.folders.removeElement(folder); + + timeoutConnections(); + } + } + + /** + * Release the store connection. + */ + private void releaseStoreProtocol(IMAPProtocol protocol) { + + // will be called from idle() without the Store lock held, + // but cleanup is synchronized and will acquire the Store lock + + if (protocol == null) { + cleanup(); // failed to ever get the connection + return; // nothing to release + } + + /* + * Read out the flag that says whether this connection failed + * before releasing the protocol object for others to use. + */ + boolean failed; + synchronized (connectionFailedLock) { + failed = connectionFailed; + connectionFailed = false; // reset for next use + } + + // now free the store connection + synchronized (pool) { + pool.storeConnectionInUse = false; + pool.notifyAll(); // in case anyone waiting + + pool.logger.fine("releaseStoreProtocol()"); + + timeoutConnections(); + } + + /* + * If the connection died while we were using it, clean up. + * It's critical that the store connection be freed and the + * connection pool not be locked while we do this. + */ + assert !Thread.holdsLock(pool); + if (failed) + cleanup(); + } + + /** + * Release a store protocol object that was being used by a folder. + */ + void releaseFolderStoreProtocol(IMAPProtocol protocol) { + if (protocol == null) + return; // should never happen + protocol.removeResponseHandler(nonStoreResponseHandler); + protocol.addResponseHandler(this); + synchronized (pool) { + pool.storeConnectionInUse = false; + pool.notifyAll(); // in case anyone waiting + + pool.logger.fine("releaseFolderStoreProtocol()"); + + timeoutConnections(); + } + } + + /** + * Empty the connection pool. + */ + private void emptyConnectionPool(boolean force) { + + synchronized (pool) { + for (int index = pool.authenticatedConnections.size() - 1; + index >= 0; --index) { + try { + IMAPProtocol p = + pool.authenticatedConnections.elementAt(index); + p.removeResponseHandler(this); + if (force) + p.disconnect(); + else + p.logout(); + } catch (ProtocolException pex) {}; + } + + pool.authenticatedConnections.removeAllElements(); + } + + pool.logger.fine("removed all authenticated connections from pool"); + } + + /** + * Check to see if it's time to shrink the connection pool. + */ + private void timeoutConnections() { + + synchronized (pool) { + + // If we've exceeded the pruning interval, look for stale + // connections to logout. + if (System.currentTimeMillis() - pool.lastTimePruned > + pool.pruningInterval && + pool.authenticatedConnections.size() > 1) { + + if (pool.logger.isLoggable(Level.FINE)) { + pool.logger.fine("checking for connections to prune: " + + (System.currentTimeMillis() - pool.lastTimePruned)); + pool.logger.fine("clientTimeoutInterval: " + + pool.clientTimeoutInterval); + } + + IMAPProtocol p; + + // Check the timestamp of the protocol objects in the pool and + // logout if the interval exceeds the client timeout value + // (leave the first connection). + for (int index = pool.authenticatedConnections.size() - 1; + index > 0; index--) { + p = pool.authenticatedConnections. + elementAt(index); + if (pool.logger.isLoggable(Level.FINE)) + pool.logger.fine("protocol last used: " + + (System.currentTimeMillis() - p.getTimestamp())); + if (System.currentTimeMillis() - p.getTimestamp() > + pool.clientTimeoutInterval) { + + pool.logger.fine( + "authenticated connection timed out, " + + "logging out the connection"); + + p.removeResponseHandler(this); + pool.authenticatedConnections.removeElementAt(index); + + try { + p.logout(); + } catch (ProtocolException pex) {} + } + } + pool.lastTimePruned = System.currentTimeMillis(); + } + } + } + + /** + * Get the block size to use for fetch requests on this Store. + */ + int getFetchBlockSize() { + return blksize; + } + + /** + * Ignore the size reported in the BODYSTRUCTURE when fetching data? + */ + boolean ignoreBodyStructureSize() { + return ignoreSize; + } + + /** + * Get a reference to the session. + */ + Session getSession() { + return session; + } + + /** + * Get the number of milliseconds to cache STATUS response. + */ + int getStatusCacheTimeout() { + return statusCacheTimeout; + } + + /** + * Get the maximum size of a message to buffer for append. + */ + int getAppendBufferSize() { + return appendBufferSize; + } + + /** + * Get the minimum amount of time to delay when returning from idle. + */ + int getMinIdleTime() { + return minIdleTime; + } + + /** + * Throw a SearchException if the search expression is too complex? + */ + boolean throwSearchException() { + return throwSearchException; + } + + /** + * Get the default "peek" value. + */ + boolean getPeek() { + return peek; + } + + /** + * Return true if the specified capability string is in the list + * of capabilities the server announced. + * + * @param capability the capability string + * @return true if the server supports this capability + * @exception MessagingException for failures + * @since JavaMail 1.3.3 + */ + public synchronized boolean hasCapability(String capability) + throws MessagingException { + IMAPProtocol p = null; + try { + p = getStoreProtocol(); + return p.hasCapability(capability); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } finally { + releaseStoreProtocol(p); + } + } + + /** + * Set the user name to be used with the PROXYAUTH command. + * The PROXYAUTH user name can also be set using the + * <code>mail.imap.proxyauth.user</code> property when this + * Store is created. + * + * @param user the user name to set + * @since JavaMail 1.5.1 + */ + public void setProxyAuthUser(String user) { + proxyAuthUser = user; + } + + /** + * Get the user name to be used with the PROXYAUTH command. + * + * @return the user name + * @since JavaMail 1.5.1 + */ + public String getProxyAuthUser() { + return proxyAuthUser; + } + + /** + * Check whether this store is connected. Override superclass + * method, to actually ping our server connection. + */ + @Override + public synchronized boolean isConnected() { + if (!super.isConnected()) { + // if we haven't been connected at all, don't bother with + // the NOOP. + return false; + } + + /* + * The below noop() request can: + * (1) succeed - in which case all is fine. + * + * (2) fail because the server returns NO or BAD, in which + * case we ignore it since we can't really do anything. + * (2) fail because a BYE response is obtained from the + * server + * (3) fail because the socket.write() to the server fails, + * in which case the iap.protocol() code converts the + * IOException into a BYE response. + * + * Thus, our BYE handler will take care of closing the Store + * in case our connection is really gone. + */ + + IMAPProtocol p = null; + try { + p = getStoreProtocol(); + p.noop(); + } catch (ProtocolException pex) { + // will return false below + } finally { + releaseStoreProtocol(p); + } + + + return super.isConnected(); + } + + /** + * Close this Store. + */ + @Override + public synchronized void close() throws MessagingException { + cleanup(); + // do these again in case cleanup returned early + // because we were already closed due to a failure + closeAllFolders(false); + emptyConnectionPool(false); + } + + @Override + protected void finalize() throws Throwable { + if (!finalizeCleanClose) { + // when finalizing, close connections abruptly + synchronized (connectionFailedLock) { + connectionFailed = true; + forceClose = true; + } + closeFoldersOnStoreFailure = true; // make sure folders get closed + } + try { + close(); + } finally { + super.finalize(); + } + } + + /** + * Cleanup before dying. + */ + private synchronized void cleanup() { + // if we're not connected, someone beat us to it + if (!super.isConnected()) { + logger.fine("IMAPStore cleanup, not connected"); + return; + } + + /* + * If forceClose is true, some thread ran into an error that suggests + * the server might be dead, so we force the folders to close + * abruptly without waiting for the server. Used when + * the store connection times out, for example. + */ + boolean force; + synchronized (connectionFailedLock) { + force = forceClose; + forceClose = false; + connectionFailed = false; + } + if (logger.isLoggable(Level.FINE)) + logger.fine("IMAPStore cleanup, force " + force); + + if (!force || closeFoldersOnStoreFailure) { + closeAllFolders(force); + } + + emptyConnectionPool(force); + + // to set the state and send the closed connection event + try { + super.close(); + } catch (MessagingException mex) { + // ignore it + } + logger.fine("IMAPStore cleanup done"); + } + + /** + * Close all open Folders. If force is true, close them forcibly. + */ + private void closeAllFolders(boolean force) { + List<IMAPFolder> foldersCopy = null; + boolean done = true; + + // To avoid violating the locking hierarchy, there's no lock we + // can hold that prevents another thread from trying to open a + // folder at the same time we're trying to close all the folders. + // Thus, there's an inherent race condition here. We close all + // the folders we know about and then check whether any new folders + // have been opened in the mean time. We keep trying until we're + // successful in closing all the folders. + for (;;) { + // Make a copy of the folders list so we do not violate the + // folder-connection pool locking hierarchy. + synchronized (pool) { + if (pool.folders != null) { + done = false; + foldersCopy = pool.folders; + pool.folders = null; + } else { + done = true; + } + } + if (done) + break; + + // Close and remove any open folders under this Store. + for (int i = 0, fsize = foldersCopy.size(); i < fsize; i++) { + IMAPFolder f = foldersCopy.get(i); + + try { + if (force) { + logger.fine("force folder to close"); + // Don't want to wait for folder connection to timeout + // (if, for example, the server is down) so we close + // folders abruptly. + f.forceClose(); + } else { + logger.fine("close folder"); + f.close(false); + } + } catch (MessagingException mex) { + // Who cares ?! Ignore 'em. + } catch (IllegalStateException ex) { + // Ditto + } + } + + } + } + + /** + * Get the default folder, representing the root of this user's + * namespace. Returns a closed DefaultFolder object. + */ + @Override + public synchronized Folder getDefaultFolder() throws MessagingException { + checkConnected(); + return new DefaultFolder(this); + } + + /** + * Get named folder. Returns a new, closed IMAPFolder. + */ + @Override + public synchronized Folder getFolder(String name) + throws MessagingException { + checkConnected(); + return newIMAPFolder(name, IMAPFolder.UNKNOWN_SEPARATOR); + } + + /** + * Get named folder. Returns a new, closed IMAPFolder. + */ + @Override + public synchronized Folder getFolder(URLName url) + throws MessagingException { + checkConnected(); + return newIMAPFolder(url.getFile(), IMAPFolder.UNKNOWN_SEPARATOR); + } + + /** + * Create an IMAPFolder object. If user supplied their own class, + * use it. Otherwise, call the constructor. + * + * @param fullName the full name of the folder + * @param separator the separator character for the folder hierarchy + * @param isNamespace does this name represent a namespace? + * @return the new IMAPFolder object + */ + protected IMAPFolder newIMAPFolder(String fullName, char separator, + Boolean isNamespace) { + IMAPFolder f = null; + if (folderConstructor != null) { + try { + Object[] o = + { fullName, Character.valueOf(separator), this, isNamespace }; + f = (IMAPFolder)folderConstructor.newInstance(o); + } catch (Exception ex) { + logger.log(Level.FINE, + "exception creating IMAPFolder class", ex); + } + } + if (f == null) + f = new IMAPFolder(fullName, separator, this, isNamespace); + return f; + } + + /** + * Create an IMAPFolder object. Call the newIMAPFolder method + * above with a null isNamespace. + * + * @param fullName the full name of the folder + * @param separator the separator character for the folder hierarchy + * @return the new IMAPFolder object + */ + protected IMAPFolder newIMAPFolder(String fullName, char separator) { + return newIMAPFolder(fullName, separator, null); + } + + /** + * Create an IMAPFolder object. If user supplied their own class, + * use it. Otherwise, call the constructor. + * + * @param li the ListInfo for the folder + * @return the new IMAPFolder object + */ + protected IMAPFolder newIMAPFolder(ListInfo li) { + IMAPFolder f = null; + if (folderConstructorLI != null) { + try { + Object[] o = { li, this }; + f = (IMAPFolder)folderConstructorLI.newInstance(o); + } catch (Exception ex) { + logger.log(Level.FINE, + "exception creating IMAPFolder class LI", ex); + } + } + if (f == null) + f = new IMAPFolder(li, this); + return f; + } + + /** + * Using the IMAP NAMESPACE command (RFC 2342), return a set + * of folders representing the Personal namespaces. + */ + @Override + public Folder[] getPersonalNamespaces() throws MessagingException { + Namespaces ns = getNamespaces(); + if (ns == null || ns.personal == null) + return super.getPersonalNamespaces(); + return namespaceToFolders(ns.personal, null); + } + + /** + * Using the IMAP NAMESPACE command (RFC 2342), return a set + * of folders representing the User's namespaces. + */ + @Override + public Folder[] getUserNamespaces(String user) + throws MessagingException { + Namespaces ns = getNamespaces(); + if (ns == null || ns.otherUsers == null) + return super.getUserNamespaces(user); + return namespaceToFolders(ns.otherUsers, user); + } + + /** + * Using the IMAP NAMESPACE command (RFC 2342), return a set + * of folders representing the Shared namespaces. + */ + @Override + public Folder[] getSharedNamespaces() throws MessagingException { + Namespaces ns = getNamespaces(); + if (ns == null || ns.shared == null) + return super.getSharedNamespaces(); + return namespaceToFolders(ns.shared, null); + } + + private synchronized Namespaces getNamespaces() throws MessagingException { + checkConnected(); + + IMAPProtocol p = null; + + if (namespaces == null) { + try { + p = getStoreProtocol(); + namespaces = p.namespace(); + } catch (BadCommandException bex) { + // NAMESPACE not supported, ignore it + } catch (ConnectionException cex) { + throw new StoreClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } finally { + releaseStoreProtocol(p); + } + } + return namespaces; + } + + private Folder[] namespaceToFolders(Namespaces.Namespace[] ns, + String user) { + Folder[] fa = new Folder[ns.length]; + for (int i = 0; i < fa.length; i++) { + String name = ns[i].prefix; + if (user == null) { + // strip trailing delimiter + int len = name.length(); + if ( len > 0 && name.charAt(len - 1) == ns[i].delimiter) + name = name.substring(0, len - 1); + } else { + // add user + name += user; + } + fa[i] = newIMAPFolder(name, ns[i].delimiter, + Boolean.valueOf(user == null)); + } + return fa; + } + + /** + * Get the quotas for the named quota root. + * Quotas are controlled on the basis of a quota root, not + * (necessarily) a folder. The relationship between folders + * and quota roots depends on the IMAP server. Some servers + * might implement a single quota root for all folders owned by + * a user. Other servers might implement a separate quota root + * for each folder. A single folder can even have multiple + * quota roots, perhaps controlling quotas for different + * resources. + * + * @param root the name of the quota root + * @return array of Quota objects + * @exception MessagingException if the server doesn't support the + * QUOTA extension + */ + @Override + public synchronized Quota[] getQuota(String root) + throws MessagingException { + checkConnected(); + Quota[] qa = null; + + IMAPProtocol p = null; + try { + p = getStoreProtocol(); + qa = p.getQuotaRoot(root); + } catch (BadCommandException bex) { + throw new MessagingException("QUOTA not supported", bex); + } catch (ConnectionException cex) { + throw new StoreClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } finally { + releaseStoreProtocol(p); + } + return qa; + } + + /** + * Set the quotas for the quota root specified in the quota argument. + * Typically this will be one of the quota roots obtained from the + * <code>getQuota</code> method, but it need not be. + * + * @param quota the quota to set + * @exception MessagingException if the server doesn't support the + * QUOTA extension + */ + @Override + public synchronized void setQuota(Quota quota) throws MessagingException { + checkConnected(); + IMAPProtocol p = null; + try { + p = getStoreProtocol(); + p.setQuota(quota); + } catch (BadCommandException bex) { + throw new MessagingException("QUOTA not supported", bex); + } catch (ConnectionException cex) { + throw new StoreClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } finally { + releaseStoreProtocol(p); + } + } + + private void checkConnected() { + assert Thread.holdsLock(this); + if (!super.isConnected()) + throw new IllegalStateException("Not connected"); + } + + /** + * Response handler method. + */ + @Override + public void handleResponse(Response r) { + // Any of these responses may have a response code. + if (r.isOK() || r.isNO() || r.isBAD() || r.isBYE()) + handleResponseCode(r); + if (r.isBYE()) { + logger.fine("IMAPStore connection dead"); + // Store's IMAP connection is dead, save the response so that + // releaseStoreProtocol will cleanup later. + synchronized (connectionFailedLock) { + connectionFailed = true; + if (r.isSynthetic()) + forceClose = true; + } + return; + } + } + + /** + * Use the IMAP IDLE command (see + * <A HREF="http://www.ietf.org/rfc/rfc2177.txt">RFC 2177</A>), + * if supported by the server, to enter idle mode so that the server + * can send unsolicited notifications + * without the need for the client to constantly poll the server. + * Use a <code>ConnectionListener</code> to be notified of + * events. When another thread (e.g., the listener thread) + * needs to issue an IMAP comand for this Store, the idle mode will + * be terminated and this method will return. Typically the caller + * will invoke this method in a loop. <p> + * + * If the mail.imap.enableimapevents property is set, notifications + * received while the IDLE command is active will be delivered to + * <code>ConnectionListener</code>s as events with a type of + * <code>IMAPStore.RESPONSE</code>. The event's message will be + * the raw IMAP response string. + * Note that most IMAP servers will not deliver any events when + * using the IDLE command on a connection with no mailbox selected + * (i.e., this method). In most cases you'll want to use the + * <code>idle</code> method on <code>IMAPFolder</code>. <p> + * + * NOTE: This capability is highly experimental and likely will change + * in future releases. <p> + * + * The mail.imap.minidletime property enforces a minimum delay + * before returning from this method, to ensure that other threads + * have a chance to issue commands before the caller invokes this + * method again. The default delay is 10 milliseconds. + * + * @exception MessagingException if the server doesn't support the + * IDLE extension + * @exception IllegalStateException if the store isn't connected + * + * @since JavaMail 1.4.1 + */ + public void idle() throws MessagingException { + IMAPProtocol p = null; + // ASSERT: Must NOT be called with the connection pool + // synchronization lock held. + assert !Thread.holdsLock(pool); + synchronized (this) { + checkConnected(); + } + boolean needNotification = false; + try { + synchronized (pool) { + p = getStoreProtocol(); + if (pool.idleState != ConnectionPool.RUNNING) { + // some other thread must be running the IDLE + // command, we'll just wait for it to finish + // without aborting it ourselves + try { + // give up lock and wait to be not idle + pool.wait(); + } catch (InterruptedException ex) { + // restore the interrupted state, which callers might + // depend on + Thread.currentThread().interrupt(); + // stop waiting and return to caller + throw new MessagingException("idle interrupted", ex); + } + return; + } + p.idleStart(); + needNotification = true; + pool.idleState = ConnectionPool.IDLE; + pool.idleProtocol = p; + } + + /* + * We gave up the pool lock so that other threads + * can get into the pool far enough to see that we're + * in IDLE and abort the IDLE. + * + * Now we read responses from the IDLE command, especially + * including unsolicited notifications from the server. + * We don't hold the pool lock while reading because + * it protects the idleState and other threads need to be + * able to examine the state. + * + * We hold the pool lock while processing the responses. + */ + for (;;) { + Response r = p.readIdleResponse(); + synchronized (pool) { + if (r == null || !p.processIdleResponse(r)) { + pool.idleState = ConnectionPool.RUNNING; + pool.idleProtocol = null; + pool.notifyAll(); + needNotification = false; + break; + } + } + if (enableImapEvents && r.isUnTagged()) { + notifyStoreListeners(IMAPStore.RESPONSE, r.toString()); + } + } + + /* + * Enforce a minimum delay to give time to threads + * processing the responses that came in while we + * were idle. + */ + int minidle = getMinIdleTime(); + if (minidle > 0) { + try { + Thread.sleep(minidle); + } catch (InterruptedException ex) { + // restore the interrupted state, which callers might + // depend on + Thread.currentThread().interrupt(); + } + } + + } catch (BadCommandException bex) { + throw new MessagingException("IDLE not supported", bex); + } catch (ConnectionException cex) { + throw new StoreClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } finally { + if (needNotification) { + synchronized (pool) { + pool.idleState = ConnectionPool.RUNNING; + pool.idleProtocol = null; + pool.notifyAll(); + } + } + releaseStoreProtocol(p); + } + } + + /* + * If an IDLE command is in progress, abort it if necessary, + * and wait until it completes. + * ASSERT: Must be called with the pool's lock held. + */ + private void waitIfIdle() throws ProtocolException { + assert Thread.holdsLock(pool); + while (pool.idleState != ConnectionPool.RUNNING) { + if (pool.idleState == ConnectionPool.IDLE) { + pool.idleProtocol.idleAbort(); + pool.idleState = ConnectionPool.ABORTING; + } + try { + // give up lock and wait to be not idle + pool.wait(); + } catch (InterruptedException ex) { + // If someone is trying to interrupt us we can't keep going + // around the loop waiting for IDLE to complete, but we can't + // just return because callers expect the idleState to be + // RUNNING when we return. Throwing this exception seems + // like the best choice. + throw new ProtocolException("Interrupted waitIfIdle", ex); + } + } + } + + /** + * Send the IMAP ID command (if supported by the server) and return + * the result from the server. The ID command identfies the client + * to the server and returns information about the server to the client. + * See <A HREF="http://www.ietf.org/rfc/rfc2971.txt">RFC 2971</A>. + * The returned Map is unmodifiable. + * + * @param clientParams a Map of keys and values identifying the client + * @return a Map of keys and values identifying the server + * @exception MessagingException if the server doesn't support the + * ID extension + * @since JavaMail 1.5.1 + */ + public synchronized Map<String, String> id(Map<String, String> clientParams) + throws MessagingException { + checkConnected(); + Map<String, String> serverParams = null; + + IMAPProtocol p = null; + try { + p = getStoreProtocol(); + serverParams = p.id(clientParams); + } catch (BadCommandException bex) { + throw new MessagingException("ID not supported", bex); + } catch (ConnectionException cex) { + throw new StoreClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } finally { + releaseStoreProtocol(p); + } + return serverParams; + } + + /** + * Handle notifications and alerts. + * Response must be an OK, NO, BAD, or BYE response. + */ + void handleResponseCode(Response r) { + if (enableResponseEvents) + notifyStoreListeners(IMAPStore.RESPONSE, r.toString()); + String s = r.getRest(); // get the text after the response + boolean isAlert = false; + if (s.startsWith("[")) { // a response code + int i = s.indexOf(']'); + // remember if it's an alert + if (i > 0 && s.substring(0, i + 1).equalsIgnoreCase("[ALERT]")) + isAlert = true; + // strip off the response code in any event + s = s.substring(i + 1).trim(); + } + if (isAlert) + notifyStoreListeners(StoreEvent.ALERT, s); + else if (r.isUnTagged() && s.length() > 0) + // Only send notifications that come with untagged + // responses, and only if there is actually some + // text there. + notifyStoreListeners(StoreEvent.NOTICE, s); + } + + private String traceUser(String user) { + return debugusername ? user : "<user name suppressed>"; + } + + private String tracePassword(String password) { + return debugpassword ? password : + (password == null ? "<null>" : "<non-null>"); + } +}
diff --git a/mail/src/main/java/com/sun/mail/imap/IdleManager.java b/mail/src/main/java/com/sun/mail/imap/IdleManager.java new file mode 100644 index 0000000..781f58d --- /dev/null +++ b/mail/src/main/java/com/sun/mail/imap/IdleManager.java
@@ -0,0 +1,491 @@ +/* + * Copyright (c) 2014, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.io.IOException; +import java.io.InterruptedIOException; +import java.net.Socket; +import java.nio.*; +import java.nio.channels.*; +import java.util.*; +import java.util.logging.*; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Executor; + +import javax.mail.*; + +import com.sun.mail.imap.protocol.IMAPProtocol; +import com.sun.mail.util.MailLogger; + +/** + * IdleManager uses the optional IMAP IDLE command + * (<A HREF="http://www.ietf.org/rfc/rfc2177.txt">RFC 2177</A>) + * to watch multiple folders for new messages. + * IdleManager uses an Executor to execute tasks in separate threads. + * An Executor is typically provided by an ExecutorService. + * For example, for a Java SE application: + * <blockquote><pre> + * ExecutorService es = Executors.newCachedThreadPool(); + * final IdleManager idleManager = new IdleManager(session, es); + * </pre></blockquote> + * For a Java EE 7 application: + * <blockquote><pre> + * {@literal @}Resource + * ManagedExecutorService es; + * final IdleManager idleManager = new IdleManager(session, es); + * </pre></blockquote> + * To watch for new messages in a folder, open the folder, register a listener, + * and ask the IdleManager to watch the folder: + * <blockquote><pre> + * Folder folder = store.getFolder("INBOX"); + * folder.open(Folder.READ_WRITE); + * folder.addMessageCountListener(new MessageCountAdapter() { + * public void messagesAdded(MessageCountEvent ev) { + * Folder folder = (Folder)ev.getSource(); + * Message[] msgs = ev.getMessages(); + * System.out.println("Folder: " + folder + + * " got " + msgs.length + " new messages"); + * try { + * // process new messages + * idleManager.watch(folder); // keep watching for new messages + * } catch (MessagingException mex) { + * // handle exception related to the Folder + * } + * } + * }); + * idleManager.watch(folder); + * </pre></blockquote> + * This delivers the events for each folder in a separate thread, <b>NOT</b> + * using the Executor. To deliver all events in a single thread + * using the Executor, set the following properties for the Session + * (once), and then add listeners and watch the folder as above. + * <blockquote><pre> + * // the following should be done once... + * Properties props = session.getProperties(); + * props.put("mail.event.scope", "session"); // or "application" + * props.put("mail.event.executor", es); + * </pre></blockquote> + * Note that, after processing new messages in your listener, or doing any + * other operations on the folder in any other thread, you need to tell + * the IdleManager to watch for more new messages. Unless, of course, you + * close the folder. + * <p> + * The IdleManager is created with a Session, which it uses only to control + * debug output. A single IdleManager instance can watch multiple Folders + * from multiple Stores and multiple Sessions. + * <p> + * Due to limitations in the Java SE nio support, a + * {@link java.nio.channels.SocketChannel SocketChannel} must be used instead + * of a {@link java.net.Socket Socket} to connect to the server. However, + * SocketChannels don't support all the features of Sockets, such as connecting + * through a SOCKS proxy server. SocketChannels also don't support + * simultaneous read and write, which means that the + * {@link com.sun.mail.imap.IMAPFolder#idle idle} method can't be used if + * SocketChannels are being used; use this IdleManager instead. + * To enable support for SocketChannels instead of Sockets, set the + * <code>mail.imap.usesocketchannels</code> property in the Session used to + * access the IMAP Folder. (Or <code>mail.imaps.usesocketchannels</code> if + * you're using the "imaps" protocol.) This will effect all connections in + * that Session, but you can create another Session without this property set + * if you need to use the features that are incompatible with SocketChannels. + * <p> + * NOTE: The IdleManager, and all APIs and properties related to it, should + * be considered <strong>EXPERIMENTAL</strong>. They may be changed in the + * future in ways that are incompatible with applications using the + * current APIs. + * + * @since JavaMail 1.5.2 + */ +public class IdleManager { + private Executor es; + private Selector selector; + private MailLogger logger; + private volatile boolean die = false; + private volatile boolean running; + private Queue<IMAPFolder> toWatch = new ConcurrentLinkedQueue<>(); + private Queue<IMAPFolder> toAbort = new ConcurrentLinkedQueue<>(); + + /** + * Create an IdleManager. The Session is used only to configure + * debugging output. The Executor is used to create the + * "select" thread. + * + * @param session the Session containing configuration information + * @param es the Executor used to create threads + * @exception IOException for Selector failures + */ + public IdleManager(Session session, Executor es) throws IOException { + this.es = es; + logger = new MailLogger(this.getClass(), "DEBUG IMAP", + session.getDebug(), session.getDebugOut()); + selector = Selector.open(); + es.execute(new Runnable() { + @Override + public void run() { + logger.fine("IdleManager select starting"); + try { + running = true; + select(); + } finally { + running = false; + logger.fine("IdleManager select terminating"); + } + } + }); + } + + /** + * Is the IdleManager currently running? The IdleManager starts + * running when the Executor schedules its task. The IdleManager + * stops running after its task detects the stop request from the + * {@link #stop stop} method, or if it terminates abnormally due + * to an unexpected error. + * + * @return true if the IdleMaanger is running + * @since JavaMail 1.5.5 + */ + public boolean isRunning() { + return running; + } + + /** + * Watch the Folder for new messages and other events using the IMAP IDLE + * command. + * + * @param folder the folder to watch + * @exception MessagingException for errors related to the folder + */ + public void watch(Folder folder) + throws MessagingException { + if (die) // XXX - should be IllegalStateException? + throw new MessagingException("IdleManager is not running"); + if (!(folder instanceof IMAPFolder)) + throw new MessagingException("Can only watch IMAP folders"); + IMAPFolder ifolder = (IMAPFolder)folder; + SocketChannel sc = ifolder.getChannel(); + if (sc == null) { + if (folder.isOpen()) + throw new MessagingException( + "Folder is not using SocketChannels"); + else + throw new MessagingException("Folder is not open"); + } + if (logger.isLoggable(Level.FINEST)) + logger.log(Level.FINEST, "IdleManager watching {0}", + folderName(ifolder)); + // keep trying to start the IDLE command until we're successful. + // may block if we're in the middle of aborting an IDLE command. + int tries = 0; + while (!ifolder.startIdle(this)) { + if (logger.isLoggable(Level.FINEST)) + logger.log(Level.FINEST, + "IdleManager.watch startIdle failed for {0}", + folderName(ifolder)); + tries++; + } + if (logger.isLoggable(Level.FINEST)) { + if (tries > 0) + logger.log(Level.FINEST, + "IdleManager.watch startIdle succeeded for {0}" + + " after " + tries + " tries", + folderName(ifolder)); + else + logger.log(Level.FINEST, + "IdleManager.watch startIdle succeeded for {0}", + folderName(ifolder)); + } + synchronized (this) { + toWatch.add(ifolder); + selector.wakeup(); + } + } + + /** + * Request that the specified folder abort an IDLE command. + * We can't do the abort directly because the DONE message needs + * to be sent through the (potentially) SSL socket, which means + * we need to be in blocking I/O mode. We can only switch to + * blocking I/O mode when not selecting, so wake up the selector, + * which will process this request when it wakes up. + */ + void requestAbort(IMAPFolder folder) { + toAbort.add(folder); + selector.wakeup(); + } + + /** + * Run the {@link java.nio.channels.Selector#select select} loop + * to poll each watched folder for events sent from the server. + */ + private void select() { + die = false; + try { + while (!die) { + watchAll(); + logger.finest("IdleManager waiting..."); + int ns = selector.select(); + if (logger.isLoggable(Level.FINEST)) + logger.log(Level.FINEST, + "IdleManager selected {0} channels", ns); + if (die || Thread.currentThread().isInterrupted()) + break; + + /* + * Process any selected folders. We cancel the + * selection key for any selected folder, so if we + * need to continue watching that folder it's added + * to the toWatch list again. We can't actually + * register that folder again until the previous + * selection key is cancelled, so we call selectNow() + * just for the side effect of cancelling the selection + * keys. But if selectNow() selects something, we + * process it before adding folders from the toWatch + * queue. And so on until there is nothing to do, at + * which point it's safe to register folders from the + * toWatch queue. This should be "fair" since each + * selection key is used only once before being added + * to the toWatch list. + */ + do { + processKeys(); + } while (selector.selectNow() > 0 || !toAbort.isEmpty()); + } + } catch (InterruptedIOException ex) { + logger.log(Level.FINEST, "IdleManager interrupted", ex); + } catch (IOException ex) { + logger.log(Level.FINEST, "IdleManager got I/O exception", ex); + } catch (Exception ex) { + logger.log(Level.FINEST, "IdleManager got exception", ex); + } finally { + die = true; // prevent new watches in case of exception + logger.finest("IdleManager unwatchAll"); + try { + unwatchAll(); + selector.close(); + } catch (IOException ex2) { + // nothing to do... + logger.log(Level.FINEST, "IdleManager unwatch exception", ex2); + } + logger.fine("IdleManager exiting"); + } + } + + /** + * Register all of the folders in the queue with the selector, + * switching them to nonblocking I/O mode first. + */ + private void watchAll() { + /* + * Pull each of the folders from the toWatch queue + * and register it. + */ + IMAPFolder folder; + while ((folder = toWatch.poll()) != null) { + if (logger.isLoggable(Level.FINEST)) + logger.log(Level.FINEST, + "IdleManager adding {0} to selector", folderName(folder)); + try { + SocketChannel sc = folder.getChannel(); + if (sc == null) + continue; + // has to be non-blocking to select + sc.configureBlocking(false); + sc.register(selector, SelectionKey.OP_READ, folder); + } catch (IOException ex) { + // oh well, nothing to do + logger.log(Level.FINEST, + "IdleManager can't register folder", ex); + } catch (CancelledKeyException ex) { + // this should never happen + logger.log(Level.FINEST, + "IdleManager can't register folder", ex); + } + } + } + + /** + * Process the selected keys. + */ + private void processKeys() throws IOException { + IMAPFolder folder; + + /* + * First, process any channels with data to read. + */ + Set<SelectionKey> selectedKeys = selector.selectedKeys(); + /* + * XXX - this is simpler, but it can fail with + * ConcurrentModificationException + * + for (SelectionKey sk : selectedKeys) { + selectedKeys.remove(sk); // only process each key once + ... + } + */ + Iterator<SelectionKey> it = selectedKeys.iterator(); + while (it.hasNext()) { + SelectionKey sk = it.next(); + it.remove(); // only process each key once + // have to cancel so we can switch back to blocking I/O mode + sk.cancel(); + folder = (IMAPFolder)sk.attachment(); + if (logger.isLoggable(Level.FINEST)) + logger.log(Level.FINEST, + "IdleManager selected folder: {0}", folderName(folder)); + SelectableChannel sc = sk.channel(); + // switch back to blocking to allow normal I/O + sc.configureBlocking(true); + try { + if (folder.handleIdle(false)) { + if (logger.isLoggable(Level.FINEST)) + logger.log(Level.FINEST, + "IdleManager continue watching folder {0}", + folderName(folder)); + // more to do with this folder, select on it again + toWatch.add(folder); + } else { + // done watching this folder, + if (logger.isLoggable(Level.FINEST)) + logger.log(Level.FINEST, + "IdleManager done watching folder {0}", + folderName(folder)); + } + } catch (MessagingException ex) { + // something went wrong, stop watching this folder + logger.log(Level.FINEST, + "IdleManager got exception for folder: " + + folderName(folder), ex); + } + } + + /* + * Now, process any folders that we need to abort. + */ + while ((folder = toAbort.poll()) != null) { + if (logger.isLoggable(Level.FINEST)) + logger.log(Level.FINEST, + "IdleManager aborting IDLE for folder: {0}", + folderName(folder)); + SocketChannel sc = folder.getChannel(); + if (sc == null) + continue; + SelectionKey sk = sc.keyFor(selector); + // have to cancel so we can switch back to blocking I/O mode + if (sk != null) + sk.cancel(); + // switch back to blocking to allow normal I/O + sc.configureBlocking(true); + + // if there's a read timeout, have to do the abort in a new thread + Socket sock = sc.socket(); + if (sock != null && sock.getSoTimeout() > 0) { + logger.finest("IdleManager requesting DONE with timeout"); + toWatch.remove(folder); + final IMAPFolder folder0 = folder; + es.execute(new Runnable() { + @Override + public void run() { + // send the DONE and wait for the response + folder0.idleAbortWait(); + } + }); + } else { + folder.idleAbort(); // send the DONE message + // watch for OK response to DONE + // XXX - what if we also added it above? should be a nop + toWatch.add(folder); + } + } + } + + /** + * Stop watching all folders. Cancel any selection keys and, + * most importantly, switch the channel back to blocking mode. + * If there's any folders waiting to be watched, need to abort + * them too. + */ + private void unwatchAll() { + IMAPFolder folder; + Set<SelectionKey> keys = selector.keys(); + for (SelectionKey sk : keys) { + // have to cancel so we can switch back to blocking I/O mode + sk.cancel(); + folder = (IMAPFolder)sk.attachment(); + if (logger.isLoggable(Level.FINEST)) + logger.log(Level.FINEST, + "IdleManager no longer watching folder: {0}", + folderName(folder)); + SelectableChannel sc = sk.channel(); + // switch back to blocking to allow normal I/O + try { + sc.configureBlocking(true); + folder.idleAbortWait(); // send the DONE message and wait + } catch (IOException ex) { + // ignore it, channel might be closed + logger.log(Level.FINEST, + "IdleManager exception while aborting idle for folder: " + + folderName(folder), ex); + } + } + + /* + * Finally, process any folders waiting to be watched. + */ + while ((folder = toWatch.poll()) != null) { + if (logger.isLoggable(Level.FINEST)) + logger.log(Level.FINEST, + "IdleManager aborting IDLE for unwatched folder: {0}", + folderName(folder)); + SocketChannel sc = folder.getChannel(); + if (sc == null) + continue; + try { + // channel should still be in blocking mode, but make sure + sc.configureBlocking(true); + folder.idleAbortWait(); // send the DONE message and wait + } catch (IOException ex) { + // ignore it, channel might be closed + logger.log(Level.FINEST, + "IdleManager exception while aborting idle for folder: " + + folderName(folder), ex); + } + } + } + + /** + * Stop the IdleManager. The IdleManager can not be restarted. + */ + public synchronized void stop() { + die = true; + logger.fine("IdleManager stopping"); + selector.wakeup(); + } + + /** + * Return the fully qualified name of the folder, for use in log messages. + * Essentially just the getURLName method, but ignoring the + * MessagingException that can never happen. + */ + private static String folderName(Folder folder) { + try { + return folder.getURLName().toString(); + } catch (MessagingException mex) { + // can't happen + return folder.getStore().toString() + "/" + folder.toString(); + } + } +}
diff --git a/mail/src/main/java/com/sun/mail/imap/MessageCache.java b/mail/src/main/java/com/sun/mail/imap/MessageCache.java new file mode 100644 index 0000000..86c7899 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/imap/MessageCache.java
@@ -0,0 +1,443 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.io.PrintStream; +import java.util.*; +import java.util.logging.Level; + +import javax.mail.*; +import com.sun.mail.util.PropUtil; +import com.sun.mail.util.MailLogger; + +/** + * A cache of IMAPMessage objects along with the + * mapping from message number to IMAP sequence number. + * + * All operations on this object are protected by the messageCacheLock + * in IMAPFolder. + */ +public class MessageCache { + /* + * The array of IMAPMessage objects. Elements of the array might + * be null if no one has asked for the message. The array expands + * as needed and might be larger than the number of messages in the + * folder. The "size" field indicates the number of entries that + * are valid. + */ + private IMAPMessage[] messages; + + /* + * A parallel array of sequence numbers for each message. If the + * array pointer is null, the sequence number of a message is just + * its message number. This is the common case, until a message is + * expunged. + */ + private int[] seqnums; + + /* + * The amount of the messages (and seqnum) array that is valid. + * Might be less than the actual size of the array. + */ + private int size; + + /** + * The folder these messages belong to. + */ + private IMAPFolder folder; + + // debugging logger + private MailLogger logger; + + /** + * Grow the array by at least this much, to avoid constantly + * reallocating the array. + */ + private static final int SLOP = 64; + + /** + * Construct a new message cache of the indicated size. + */ + MessageCache(IMAPFolder folder, IMAPStore store, int size) { + this.folder = folder; + logger = folder.logger.getSubLogger("messagecache", "DEBUG IMAP MC", + store.getMessageCacheDebug()); + if (logger.isLoggable(Level.CONFIG)) + logger.config("create cache of size " + size); + ensureCapacity(size, 1); + } + + /** + * Constructor for debugging and testing. + */ + MessageCache(int size, boolean debug) { + this.folder = null; + logger = new MailLogger( + this.getClass(), "messagecache", + "DEBUG IMAP MC", debug, System.out); + if (logger.isLoggable(Level.CONFIG)) + logger.config("create DEBUG cache of size " + size); + ensureCapacity(size, 1); + } + + /** + * Size of cache. + * + * @return the size of the cache + */ + public int size() { + return size; + } + + /** + * Get the message object for the indicated message number. + * If the message object hasn't been created, create it. + * + * @param msgnum the message number + * @return the message + */ + public IMAPMessage getMessage(int msgnum) { + // check range + if (msgnum < 1 || msgnum > size) + throw new ArrayIndexOutOfBoundsException( + "message number (" + msgnum + ") out of bounds (" + size + ")"); + IMAPMessage msg = messages[msgnum-1]; + if (msg == null) { + if (logger.isLoggable(Level.FINE)) + logger.fine("create message number " + msgnum); + msg = folder.newIMAPMessage(msgnum); + messages[msgnum-1] = msg; + // mark message expunged if no seqnum + if (seqnumOf(msgnum) <= 0) { + logger.fine("it's expunged!"); + msg.setExpunged(true); + } + } + return msg; + } + + /** + * Get the message object for the indicated sequence number. + * If the message object hasn't been created, create it. + * Return null if there's no message with that sequence number. + * + * @param seqnum the sequence number of the message + * @return the message + */ + public IMAPMessage getMessageBySeqnum(int seqnum) { + int msgnum = msgnumOf(seqnum); + if (msgnum < 0) { // XXX - < 1 ? + if (logger.isLoggable(Level.FINE)) + logger.fine("no message seqnum " + seqnum); + return null; + } else + return getMessage(msgnum); + } + + /** + * Expunge the message with the given sequence number. + * + * @param seqnum the sequence number of the message to expunge + */ + public void expungeMessage(int seqnum) { + int msgnum = msgnumOf(seqnum); + if (msgnum < 0) { + if (logger.isLoggable(Level.FINE)) + logger.fine("expunge no seqnum " + seqnum); + return; // XXX - should never happen + } + IMAPMessage msg = messages[msgnum-1]; + if (msg != null) { + if (logger.isLoggable(Level.FINE)) + logger.fine("expunge existing " + msgnum); + msg.setExpunged(true); + } + if (seqnums == null) { // time to fill it in + logger.fine("create seqnums array"); + seqnums = new int[messages.length]; + for (int i = 1; i < msgnum; i++) + seqnums[i-1] = i; + seqnums[msgnum - 1] = 0; + for (int i = msgnum + 1; i <= seqnums.length; i++) + seqnums[i-1] = i - 1; + } else { + seqnums[msgnum - 1] = 0; + for (int i = msgnum + 1; i <= seqnums.length; i++) { + assert seqnums[i-1] != 1; + if (seqnums[i-1] > 0) + seqnums[i-1]--; + } + } + } + + /** + * Remove all the expunged messages from the array, + * returning a list of removed message objects. + * + * @return the removed messages + */ + public IMAPMessage[] removeExpungedMessages() { + logger.fine("remove expunged messages"); + // list of expunged messages + List<IMAPMessage> mlist = new ArrayList<>(); + + /* + * Walk through the array compressing it by copying + * higher numbered messages further down in the array, + * effectively removing expunged messages from the array. + * oldnum is the index we use to walk through the array. + * newnum is the index where we copy the next valid message. + * oldnum == newnum until we encounter an expunged message. + */ + int oldnum = 1; + int newnum = 1; + while (oldnum <= size) { + // is message expunged? + if (seqnumOf(oldnum) <= 0) { + IMAPMessage m = getMessage(oldnum); + mlist.add(m); + } else { + // keep this message + if (newnum != oldnum) { + // move message down in the array (compact array) + messages[newnum-1] = messages[oldnum-1]; + if (messages[newnum-1] != null) + messages[newnum-1].setMessageNumber(newnum); + } + newnum++; + } + oldnum++; + } + seqnums = null; + shrink(newnum, oldnum); + + IMAPMessage[] rmsgs = new IMAPMessage[mlist.size()]; + if (logger.isLoggable(Level.FINE)) + logger.fine("return " + rmsgs.length); + mlist.toArray(rmsgs); + return rmsgs; + } + + /** + * Remove expunged messages in msgs from the array, + * returning a list of removed message objects. + * All messages in msgs must be IMAPMessage objects + * from this folder. + * + * @param msgs the messages + * @return the removed messages + */ + public IMAPMessage[] removeExpungedMessages(Message[] msgs) { + logger.fine("remove expunged messages"); + // list of expunged messages + List<IMAPMessage> mlist = new ArrayList<>(); + + /* + * Copy the message numbers of the expunged messages into + * a separate array and sort the array to make it easier to + * process later. + */ + int[] mnum = new int[msgs.length]; + for (int i = 0; i < msgs.length; i++) + mnum[i] = msgs[i].getMessageNumber(); + Arrays.sort(mnum); + + /* + * Walk through the array compressing it by copying + * higher numbered messages further down in the array, + * effectively removing expunged messages from the array. + * oldnum is the index we use to walk through the array. + * newnum is the index where we copy the next valid message. + * oldnum == newnum until we encounter an expunged message. + * + * Even though we know the message number of the first possibly + * expunged message, we still start scanning at message number 1 + * so that we can check whether there's any message whose + * sequence number is different than its message number. If there + * is, we can't throw away the seqnums array when we're done. + */ + int oldnum = 1; + int newnum = 1; + int mnumi = 0; // index into mnum + boolean keepSeqnums = false; + while (oldnum <= size) { + /* + * Are there still expunged messsages in msgs to consider, + * and is the message we're considering the next one in the + * list, and is it expunged? + */ + if (mnumi < mnum.length && + oldnum == mnum[mnumi] && + seqnumOf(oldnum) <= 0) { + IMAPMessage m = getMessage(oldnum); + mlist.add(m); + /* + * Just in case there are duplicate entries in the msgs array, + * we keep advancing mnumi past any duplicates, but of course + * stop when we get to the end of the array. + */ + while (mnumi < mnum.length && mnum[mnumi] <= oldnum) + mnumi++; // consider next message in array + } else { + // keep this message + if (newnum != oldnum) { + // move message down in the array (compact array) + messages[newnum-1] = messages[oldnum-1]; + if (messages[newnum-1] != null) + messages[newnum-1].setMessageNumber(newnum); + if (seqnums != null) + seqnums[newnum-1] = seqnums[oldnum-1]; + } + if (seqnums != null && seqnums[newnum-1] != newnum) + keepSeqnums = true; + newnum++; + } + oldnum++; + } + + if (!keepSeqnums) + seqnums = null; + shrink(newnum, oldnum); + + IMAPMessage[] rmsgs = new IMAPMessage[mlist.size()]; + if (logger.isLoggable(Level.FINE)) + logger.fine("return " + rmsgs.length); + mlist.toArray(rmsgs); + return rmsgs; + } + + /** + * Shrink the messages and seqnums arrays. newend is one past last + * valid element. oldend is one past the previous last valid element. + */ + private void shrink(int newend, int oldend) { + size = newend - 1; + if (logger.isLoggable(Level.FINE)) + logger.fine("size now " + size); + if (size == 0) { // no messages left + messages = null; + seqnums = null; + } else if (size > SLOP && size < messages.length / 2) { + // if array shrinks by too much, reallocate it + logger.fine("reallocate array"); + IMAPMessage[] newm = new IMAPMessage[size + SLOP]; + System.arraycopy(messages, 0, newm, 0, size); + messages = newm; + if (seqnums != null) { + int[] news = new int[size + SLOP]; + System.arraycopy(seqnums, 0, news, 0, size); + seqnums = news; + } + } else { + if (logger.isLoggable(Level.FINE)) + logger.fine("clean " + newend + " to " + oldend); + // clear out unused entries in array + for (int msgnum = newend; msgnum < oldend; msgnum++) { + messages[msgnum-1] = null; + if (seqnums != null) + seqnums[msgnum-1] = 0; + } + } + } + + /** + * Add count messages to the cache. + * newSeqNum is the sequence number of the first message added. + * + * @param count the number of messges + * @param newSeqNum sequence number of first message + */ + public void addMessages(int count, int newSeqNum) { + if (logger.isLoggable(Level.FINE)) + logger.fine("add " + count + " messages"); + // don't have to do anything other than making sure there's space + ensureCapacity(size + count, newSeqNum); + } + + /* + * Make sure the arrays are at least big enough to hold + * "newsize" messages. + */ + private void ensureCapacity(int newsize, int newSeqNum) { + if (messages == null) + messages = new IMAPMessage[newsize + SLOP]; + else if (messages.length < newsize) { + if (logger.isLoggable(Level.FINE)) + logger.fine("expand capacity to " + newsize); + IMAPMessage[] newm = new IMAPMessage[newsize + SLOP]; + System.arraycopy(messages, 0, newm, 0, messages.length); + messages = newm; + if (seqnums != null) { + int[] news = new int[newsize + SLOP]; + System.arraycopy(seqnums, 0, news, 0, seqnums.length); + for (int i = size; i < news.length; i++) + news[i] = newSeqNum++; + seqnums = news; + if (logger.isLoggable(Level.FINE)) + logger.fine("message " + newsize + + " has sequence number " + seqnums[newsize-1]); + } + } else if (newsize < size) { // shrinking? + // this should never happen + if (logger.isLoggable(Level.FINE)) + logger.fine("shrink capacity to " + newsize); + for (int msgnum = newsize + 1; msgnum <= size; msgnum++) { + messages[msgnum-1] = null; + if (seqnums != null) + seqnums[msgnum-1] = -1; + } + } + size = newsize; + } + + /** + * Return the sequence number for the given message number. + * + * @param msgnum the message number + * @return the sequence number + */ + public int seqnumOf(int msgnum) { + if (seqnums == null) + return msgnum; + else { + if (logger.isLoggable(Level.FINE)) + logger.fine("msgnum " + msgnum + " is seqnum " + + seqnums[msgnum-1]); + return seqnums[msgnum-1]; + } + } + + /** + * Return the message number for the given sequence number. + */ + private int msgnumOf(int seqnum) { + if (seqnums == null) + return seqnum; + if (seqnum < 1) { // should never happen + if (logger.isLoggable(Level.FINE)) + logger.fine("bad seqnum " + seqnum); + return -1; + } + for (int msgnum = seqnum; msgnum <= size; msgnum++) { + if (seqnums[msgnum-1] == seqnum) + return msgnum; + if (seqnums[msgnum-1] > seqnum) + break; // message doesn't exist + } + return -1; + } +}
diff --git a/mail/src/main/java/com/sun/mail/imap/MessageVanishedEvent.java b/mail/src/main/java/com/sun/mail/imap/MessageVanishedEvent.java new file mode 100644 index 0000000..056cabc --- /dev/null +++ b/mail/src/main/java/com/sun/mail/imap/MessageVanishedEvent.java
@@ -0,0 +1,62 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import javax.mail.Folder; +import javax.mail.Message; +import javax.mail.event.MessageCountEvent; + +/** + * This class provides notification of messages that have been removed + * since the folder was last synchronized. + * + * @since JavaMail 1.5.1 + * @author Bill Shannon + */ + +public class MessageVanishedEvent extends MessageCountEvent { + + /** + * The message UIDs. + */ + private long[] uids; + + // a reusable empty array + private static final Message[] noMessages = { }; + + private static final long serialVersionUID = 2142028010250024922L; + + /** + * Constructor. + * + * @param folder the containing folder + * @param uids the UIDs for the vanished messages + */ + public MessageVanishedEvent(Folder folder, long[] uids) { + super(folder, REMOVED, true, noMessages); + this.uids = uids; + } + + /** + * Return the UIDs for this event. + * + * @return the UIDs + */ + public long[] getUIDs() { + return uids; + } +}
diff --git a/mail/src/main/java/com/sun/mail/imap/ModifiedSinceTerm.java b/mail/src/main/java/com/sun/mail/imap/ModifiedSinceTerm.java new file mode 100644 index 0000000..7332266 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/imap/ModifiedSinceTerm.java
@@ -0,0 +1,94 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import javax.mail.Message; +import javax.mail.search.SearchTerm; + +/** + * Find messages that have been modified since a given MODSEQ value. + * Relies on the server implementing the CONDSTORE extension + * (<A HREF="http://www.ietf.org/rfc/rfc4551.txt">RFC 4551</A>). + * + * @since JavaMail 1.5.1 + * @author Bill Shannon + */ +public final class ModifiedSinceTerm extends SearchTerm { + + private long modseq; + + private static final long serialVersionUID = 5151457469634727992L; + + /** + * Constructor. + * + * @param modseq modification sequence number + */ + public ModifiedSinceTerm(long modseq) { + this.modseq = modseq; + } + + /** + * Return the modseq. + * + * @return the modseq + */ + public long getModSeq() { + return modseq; + } + + /** + * The match method. + * + * @param msg the date comparator is applied to this Message's + * MODSEQ + * @return true if the comparison succeeds, otherwise false + */ + @Override + public boolean match(Message msg) { + long m; + + try { + if (msg instanceof IMAPMessage) + m = ((IMAPMessage)msg).getModSeq(); + else + return false; + } catch (Exception e) { + return false; + } + + return m >= modseq; + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof ModifiedSinceTerm)) + return false; + return modseq == ((ModifiedSinceTerm)obj).modseq; + } + + /** + * Compute a hashCode for this object. + */ + @Override + public int hashCode() { + return (int)modseq; + } +}
diff --git a/mail/src/main/java/com/sun/mail/imap/OlderTerm.java b/mail/src/main/java/com/sun/mail/imap/OlderTerm.java new file mode 100644 index 0000000..19857c4 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/imap/OlderTerm.java
@@ -0,0 +1,96 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.util.Date; +import javax.mail.Message; +import javax.mail.search.SearchTerm; + +/** + * Find messages that are older than a given interval (in seconds). + * Relies on the server implementing the WITHIN search extension + * (<A HREF="http://www.ietf.org/rfc/rfc5032.txt">RFC 5032</A>). + * + * @since JavaMail 1.5.1 + * @author Bill Shannon + */ +public final class OlderTerm extends SearchTerm { + + private int interval; + + private static final long serialVersionUID = 3951078948727995682L; + + /** + * Constructor. + * + * @param interval number of seconds older + */ + public OlderTerm(int interval) { + this.interval = interval; + } + + /** + * Return the interval. + * + * @return the interval + */ + public int getInterval() { + return interval; + } + + /** + * The match method. + * + * @param msg the date comparator is applied to this Message's + * received date + * @return true if the comparison succeeds, otherwise false + */ + @Override + public boolean match(Message msg) { + Date d; + + try { + d = msg.getReceivedDate(); + } catch (Exception e) { + return false; + } + + if (d == null) + return false; + + return d.getTime() <= + System.currentTimeMillis() - ((long)interval * 1000); + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof OlderTerm)) + return false; + return interval == ((OlderTerm)obj).interval; + } + + /** + * Compute a hashCode for this object. + */ + @Override + public int hashCode() { + return interval; + } +}
diff --git a/mail/src/main/java/com/sun/mail/imap/ReferralException.java b/mail/src/main/java/com/sun/mail/imap/ReferralException.java new file mode 100644 index 0000000..f4a335c --- /dev/null +++ b/mail/src/main/java/com/sun/mail/imap/ReferralException.java
@@ -0,0 +1,65 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import javax.mail.AuthenticationFailedException; + +/** + * A special kind of AuthenticationFailedException that indicates that + * the reason for the failure was an IMAP REFERRAL in the response code. + * See <a href="http://www.ietf.org/rfc/rfc2221.txt">RFC 2221</a> for details. + * + * @since JavaMail 1.5.5 + */ + +public class ReferralException extends AuthenticationFailedException { + + private String url; + private String text; + + private static final long serialVersionUID = -3414063558596287683L; + + /** + * Constructs an ReferralException with the specified URL and text. + * + * @param text the detail message + * @param url the URL + */ + public ReferralException(String url, String text) { + super("[REFERRAL " + url + "] " + text); + this.url = url; + this.text = text; + } + + /** + * Return the IMAP URL in the referral. + * + * @return the IMAP URL + */ + public String getUrl() { + return url; + } + + /** + * Return the text sent by the server along with the referral. + * + * @return the text + */ + public String getText() { + return text; + } +}
diff --git a/mail/src/main/java/com/sun/mail/imap/ResyncData.java b/mail/src/main/java/com/sun/mail/imap/ResyncData.java new file mode 100644 index 0000000..f5c295e --- /dev/null +++ b/mail/src/main/java/com/sun/mail/imap/ResyncData.java
@@ -0,0 +1,118 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import com.sun.mail.imap.protocol.UIDSet; + +/** + * Resynchronization data as defined by the QRESYNC extension + * (<A HREF="http://www.ietf.org/rfc/rfc5162.txt">RFC 5162</A>). + * An instance of <CODE>ResyncData</CODE> is supplied to the + * {@link com.sun.mail.imap.IMAPFolder#open(int,com.sun.mail.imap.ResyncData) + * IMAPFolder open} method. + * The CONDSTORE <CODE>ResyncData</CODE> instance is used to enable the + * CONDSTORE extension + * (<A HREF="http://www.ietf.org/rfc/rfc4551.txt">RFC 4551</A>). + * A <CODE>ResyncData</CODE> instance with uidvalidity and modseq values + * is used to enable the QRESYNC extension. + * + * @since JavaMail 1.5.1 + * @author Bill Shannon + */ + +public class ResyncData { + private long uidvalidity = -1; + private long modseq = -1; + private UIDSet[] uids = null; + + /** + * Used to enable only the CONDSTORE extension. + */ + public static final ResyncData CONDSTORE = new ResyncData(-1, -1); + + /** + * Used to report on changes since the specified modseq. + * If the UIDVALIDITY of the folder has changed, no message + * changes will be reported. The application must check the + * UIDVALIDITY of the folder after open to make sure it's + * the expected folder. + * + * @param uidvalidity the UIDVALIDITY + * @param modseq the MODSEQ + */ + public ResyncData(long uidvalidity, long modseq) { + this.uidvalidity = uidvalidity; + this.modseq = modseq; + this.uids = null; + } + + /** + * Used to limit the reported message changes to those with UIDs + * in the specified range. + * + * @param uidvalidity the UIDVALIDITY + * @param modseq the MODSEQ + * @param uidFirst the first UID + * @param uidLast the last UID + */ + public ResyncData(long uidvalidity, long modseq, + long uidFirst, long uidLast) { + this.uidvalidity = uidvalidity; + this.modseq = modseq; + this.uids = new UIDSet[] { new UIDSet(uidFirst, uidLast) }; + } + + /** + * Used to limit the reported message changes to those with the + * specified UIDs. + * + * @param uidvalidity the UIDVALIDITY + * @param modseq the MODSEQ + * @param uids the UID values + */ + public ResyncData(long uidvalidity, long modseq, long[] uids) { + this.uidvalidity = uidvalidity; + this.modseq = modseq; + this.uids = UIDSet.createUIDSets(uids); + } + + /** + * Get the UIDVALIDITY value specified when this instance was created. + * + * @return the UIDVALIDITY value + */ + public long getUIDValidity() { + return uidvalidity; + } + + /** + * Get the MODSEQ value specified when this instance was created. + * + * @return the MODSEQ value + */ + public long getModSeq() { + return modseq; + } + + /* + * Package private. IMAPProtocol gets this data indirectly + * using Utility.getResyncUIDSet(). + */ + UIDSet[] getUIDSet() { + return uids; + } +}
diff --git a/mail/src/main/java/com/sun/mail/imap/Rights.java b/mail/src/main/java/com/sun/mail/imap/Rights.java new file mode 100644 index 0000000..d9c4ce0 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/imap/Rights.java
@@ -0,0 +1,444 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.util.*; + +/** + * The Rights class represents the set of rights for an authentication + * identifier (for instance, a user or a group). <p> + * + * A right is represented by the <code>Rights.Right</code> + * inner class. <p> + * + * A set of standard rights are predefined (see RFC 2086). Most folder + * implementations are expected to support these rights. Some + * implementations may also support site-defined rights. <p> + * + * The following code sample illustrates how to examine your + * rights for a folder. + * <pre> + * + * Rights rights = folder.myRights(); + * + * // Check if I can write this folder + * if (rights.contains(Rights.Right.WRITE)) + * System.out.println("Can write folder"); + * + * // Now give Joe all my rights, except the ability to write the folder + * rights.remove(Rights.Right.WRITE); + * ACL acl = new ACL("joe", rights); + * folder.setACL(acl); + * </pre> + * <p> + * + * @author Bill Shannon + */ + +public class Rights implements Cloneable { + + private boolean[] rights = new boolean[128]; // XXX + + /** + * This inner class represents an individual right. A set + * of standard rights objects are predefined here. + */ + public static final class Right { + private static Right[] cache = new Right[128]; + + // XXX - initialization order? + /** + * Lookup - mailbox is visible to LIST/LSUB commands. + */ + public static final Right LOOKUP = getInstance('l'); + + /** + * Read - SELECT the mailbox, perform CHECK, FETCH, PARTIAL, + * SEARCH, COPY from mailbox + */ + public static final Right READ = getInstance('r'); + + /** + * Keep seen/unseen information across sessions - STORE \SEEN flag. + */ + public static final Right KEEP_SEEN = getInstance('s'); + + /** + * Write - STORE flags other than \SEEN and \DELETED. + */ + public static final Right WRITE = getInstance('w'); + + /** + * Insert - perform APPEND, COPY into mailbox. + */ + public static final Right INSERT = getInstance('i'); + + /** + * Post - send mail to submission address for mailbox, + * not enforced by IMAP4 itself. + */ + public static final Right POST = getInstance('p'); + + /** + * Create - CREATE new sub-mailboxes in any implementation-defined + * hierarchy, RENAME or DELETE mailbox. + */ + public static final Right CREATE = getInstance('c'); + + /** + * Delete - STORE \DELETED flag, perform EXPUNGE. + */ + public static final Right DELETE = getInstance('d'); + + /** + * Administer - perform SETACL. + */ + public static final Right ADMINISTER = getInstance('a'); + + char right; // the right represented by this Right object + + /** + * Private constructor used only by getInstance. + */ + private Right(char right) { + if ((int)right >= 128) + throw new IllegalArgumentException("Right must be ASCII"); + this.right = right; + } + + /** + * Get a Right object representing the specified character. + * Characters are assigned per RFC 2086. + * + * @param right the character representing the right + * @return the Right object + */ + public static synchronized Right getInstance(char right) { + if ((int)right >= 128) + throw new IllegalArgumentException("Right must be ASCII"); + if (cache[(int)right] == null) + cache[(int)right] = new Right(right); + return cache[(int)right]; + } + + @Override + public String toString() { + return String.valueOf(right); + } + } + + + /** + * Construct an empty Rights object. + */ + public Rights() { } + + /** + * Construct a Rights object initialized with the given rights. + * + * @param rights the rights for initialization + */ + public Rights(Rights rights) { + System.arraycopy(rights.rights, 0, this.rights, 0, this.rights.length); + } + + /** + * Construct a Rights object initialized with the given rights. + * + * @param rights the rights for initialization + */ + public Rights(String rights) { + for (int i = 0; i < rights.length(); i++) + add(Right.getInstance(rights.charAt(i))); + } + + /** + * Construct a Rights object initialized with the given right. + * + * @param right the right for initialization + */ + public Rights(Right right) { + this.rights[(int)right.right] = true; + } + + /** + * Add the specified right to this Rights object. + * + * @param right the right to add + */ + public void add(Right right) { + this.rights[(int)right.right] = true; + } + + /** + * Add all the rights in the given Rights object to this + * Rights object. + * + * @param rights Rights object + */ + public void add(Rights rights) { + for (int i = 0; i < rights.rights.length; i++) + if (rights.rights[i]) + this.rights[i] = true; + } + + /** + * Remove the specified right from this Rights object. + * + * @param right the right to be removed + */ + public void remove(Right right) { + this.rights[(int)right.right] = false; + } + + /** + * Remove all rights in the given Rights object from this + * Rights object. + * + * @param rights the rights to be removed + */ + public void remove(Rights rights) { + for (int i = 0; i < rights.rights.length; i++) + if (rights.rights[i]) + this.rights[i] = false; + } + + /** + * Check whether the specified right is present in this Rights object. + * + * @param right the Right to check + * @return true of the given right is present, otherwise false. + */ + public boolean contains(Right right) { + return this.rights[(int)right.right]; + } + + /** + * Check whether all the rights in the specified Rights object are + * present in this Rights object. + * + * @param rights the Rights to check + * @return true if all rights in the given Rights object are present, + * otherwise false. + */ + public boolean contains(Rights rights) { + for (int i = 0; i < rights.rights.length; i++) + if (rights.rights[i] && !this.rights[i]) + return false; + + // If we've made it till here, return true + return true; + } + + /** + * Check whether the two Rights objects are equal. + * + * @return true if they're equal + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Rights)) + return false; + + Rights rights = (Rights)obj; + + for (int i = 0; i < rights.rights.length; i++) + if (rights.rights[i] != this.rights[i]) + return false; + + return true; + } + + /** + * Compute a hash code for this Rights object. + * + * @return the hash code + */ + @Override + public int hashCode() { + int hash = 0; + for (int i = 0; i < this.rights.length; i++) + if (this.rights[i]) + hash++; + return hash; + } + + /** + * Return all the rights in this Rights object. Returns + * an array of size zero if no rights are set. + * + * @return array of Rights.Right objects representing rights + */ + public Right[] getRights() { + List<Right> v = new ArrayList<>(); + for (int i = 0; i < this.rights.length; i++) + if (this.rights[i]) + v.add(Right.getInstance((char)i)); + return v.toArray(new Right[v.size()]); + } + + /** + * Returns a clone of this Rights object. + */ + @Override + public Object clone() { + Rights r = null; + try { + r = (Rights)super.clone(); + r.rights = new boolean[128]; + System.arraycopy(this.rights, 0, r.rights, 0, this.rights.length); + } catch (CloneNotSupportedException cex) { + // ignore, can't happen + } + return r; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < this.rights.length; i++) + if (this.rights[i]) + sb.append((char)i); + return sb.toString(); + } + + /***** + public static void main(String argv[]) throws Exception { + // a new rights object + Rights f1 = new Rights(); + f1.add(Rights.Right.READ); + f1.add(Rights.Right.WRITE); + f1.add(Rights.Right.CREATE); + f1.add(Rights.Right.DELETE); + + // check copy constructor + Rights fc = new Rights(f1); + if (f1.equals(fc) && fc.equals(f1)) + System.out.println("success"); + else + System.out.println("fail"); + + // check clone + fc = (Rights)f1.clone(); + if (f1.equals(fc) && fc.equals(f1)) + System.out.println("success"); + else + System.out.println("fail"); + + // add a right and make sure it still works right + f1.add(Rights.Right.ADMINISTER); + + // shouldn't be equal here + if (!f1.equals(fc) && !fc.equals(f1)) + System.out.println("success"); + else + System.out.println("fail"); + + // check clone + fc = (Rights)f1.clone(); + if (f1.equals(fc) && fc.equals(f1)) + System.out.println("success"); + else + System.out.println("fail"); + + fc.add(Rights.Right.INSERT); + if (!f1.equals(fc) && !fc.equals(f1)) + System.out.println("success"); + else + System.out.println("fail"); + + // check copy constructor + fc = new Rights(f1); + if (f1.equals(fc) && fc.equals(f1)) + System.out.println("success"); + else + System.out.println("fail"); + + // another new rights object + Rights f2 = new Rights(Rights.Right.READ); + f2.add(Rights.Right.WRITE); + + if (f1.contains(Rights.Right.READ)) + System.out.println("success"); + else + System.out.println("fail"); + + if (f1.contains(Rights.Right.WRITE)) + System.out.println("success"); + else + System.out.println("fail"); + + if (f1.contains(Rights.Right.CREATE)) + System.out.println("success"); + else + System.out.println("fail"); + + if (f1.contains(Rights.Right.DELETE)) + System.out.println("success"); + else + System.out.println("fail"); + + if (f2.contains(Rights.Right.WRITE)) + System.out.println("success"); + else + System.out.println("fail"); + + + System.out.println("----------------"); + + Right[] r = f1.getRights(); + for (int i = 0; i < r.length; i++) + System.out.println(r[i]); + System.out.println("----------------"); + + if (f1.contains(f2)) // this should be true + System.out.println("success"); + else + System.out.println("fail"); + + if (!f2.contains(f1)) // this should be false + System.out.println("success"); + else + System.out.println("fail"); + + Rights f3 = new Rights(); + f3.add(Rights.Right.READ); + f3.add(Rights.Right.WRITE); + f3.add(Rights.Right.CREATE); + f3.add(Rights.Right.DELETE); + f3.add(Rights.Right.ADMINISTER); + f3.add(Rights.Right.LOOKUP); + + f1.add(Rights.Right.LOOKUP); + + if (f1.equals(f3)) + System.out.println("equals success"); + else + System.out.println("fail"); + if (f3.equals(f1)) + System.out.println("equals success"); + else + System.out.println("fail"); + System.out.println("f1 hash code " + f1.hashCode()); + System.out.println("f3 hash code " + f3.hashCode()); + if (f1.hashCode() == f3.hashCode()) + System.out.println("success"); + else + System.out.println("fail"); + } + ****/ +}
diff --git a/mail/src/main/java/com/sun/mail/imap/SortTerm.java b/mail/src/main/java/com/sun/mail/imap/SortTerm.java new file mode 100644 index 0000000..6f624e8 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/imap/SortTerm.java
@@ -0,0 +1,81 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +/** + * A particular sort criteria, as defined by + * <A HREF="http://www.ietf.org/rfc/rfc5256.txt">RFC 5256</A>. + * Sort criteria are used with the + * {@link IMAPFolder#getSortedMessages getSortedMessages} method. + * Multiple sort criteria are specified in an array with the order in + * the array specifying the order in which the sort criteria are applied. + * + * @since JavaMail 1.4.4 + */ +public final class SortTerm { + /** + * Sort by message arrival date and time. + */ + public static final SortTerm ARRIVAL = new SortTerm("ARRIVAL"); + + /** + * Sort by email address of first Cc recipient. + */ + public static final SortTerm CC = new SortTerm("CC"); + + /** + * Sort by sent date and time. + */ + public static final SortTerm DATE = new SortTerm("DATE"); + + /** + * Sort by first From email address. + */ + public static final SortTerm FROM = new SortTerm("FROM"); + + /** + * Reverse the sort order of the following item. + */ + public static final SortTerm REVERSE = new SortTerm("REVERSE"); + + /** + * Sort by the message size. + */ + public static final SortTerm SIZE = new SortTerm("SIZE"); + + /** + * Sort by the base subject text. Note that the "base subject" + * is defined by RFC 5256 and doesn't include items such as "Re:" + * in the subject header. + */ + public static final SortTerm SUBJECT = new SortTerm("SUBJECT"); + + /** + * Sort by email address of first To recipient. + */ + public static final SortTerm TO = new SortTerm("TO"); + + private String term; + private SortTerm(String term) { + this.term = term; + } + + @Override + public String toString() { + return term; + } +}
diff --git a/mail/src/main/java/com/sun/mail/imap/Utility.java b/mail/src/main/java/com/sun/mail/imap/Utility.java new file mode 100644 index 0000000..43f1998 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/imap/Utility.java
@@ -0,0 +1,214 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.util.Arrays; +import java.util.Comparator; + +import javax.mail.*; + +import com.sun.mail.imap.protocol.MessageSet; +import com.sun.mail.imap.protocol.UIDSet; +import java.util.ArrayList; +import java.util.List; + +/** + * Holder for some static utility methods. + * + * @author John Mani + * @author Bill Shannon + */ + +public final class Utility { + + // Cannot be initialized + private Utility() { } + + /** + * Run thru the given array of messages, apply the given + * Condition on each message and generate sets of contiguous + * sequence-numbers for the successful messages. If a message + * in the given array is found to be expunged, it is ignored. + * + * ASSERT: Since this method uses and returns message sequence + * numbers, you should use this method only when holding the + * messageCacheLock. + * + * @param msgs the messages + * @param cond the condition to check + * @return the MessageSet array + */ + public static MessageSet[] toMessageSet(Message[] msgs, Condition cond) { + List<MessageSet> v = new ArrayList<>(1); + int current, next; + + IMAPMessage msg; + for (int i = 0; i < msgs.length; i++) { + msg = (IMAPMessage)msgs[i]; + if (msg.isExpunged()) // expunged message, skip it + continue; + + current = msg.getSequenceNumber(); + // Apply the condition. If it fails, skip it. + if ((cond != null) && !cond.test(msg)) + continue; + + MessageSet set = new MessageSet(); + set.start = current; + + // Look for contiguous sequence numbers + for (++i; i < msgs.length; i++) { + // get next message + msg = (IMAPMessage)msgs[i]; + + if (msg.isExpunged()) // expunged message, skip it + continue; + next = msg.getSequenceNumber(); + + // Does this message match our condition ? + if ((cond != null) && !cond.test(msg)) + continue; + + if (next == current+1) + current = next; + else { // break in sequence + // We need to reexamine this message at the top of + // the outer loop, so decrement 'i' to cancel the + // outer loop's autoincrement + i--; + break; + } + } + set.end = current; + v.add(set); + } + + if (v.isEmpty()) // No valid messages + return null; + else { + return v.toArray(new MessageSet[v.size()]); + } + } + /** + * Sort (a copy of) the given array of messages and then + * run thru the sorted array of messages, apply the given + * Condition on each message and generate sets of contiguous + * sequence-numbers for the successful messages. If a message + * in the given array is found to be expunged, it is ignored. + * + * ASSERT: Since this method uses and returns message sequence + * numbers, you should use this method only when holding the + * messageCacheLock. + * + * @param msgs the messages + * @param cond the condition to check + * @return the MessageSet array + * @since JavaMail 1.5.4 + */ + public static MessageSet[] toMessageSetSorted(Message[] msgs, + Condition cond) { + /* + * XXX - This is quick and dirty. A more efficient strategy would be + * to generate an array of message numbers by applying the condition + * (with zero indicating the message doesn't satisfy the condition), + * sort it, and then convert it to a MessageSet skipping all the zeroes. + */ + msgs = msgs.clone(); + Arrays.sort(msgs, + new Comparator<Message>() { + @Override + public int compare(Message msg1, Message msg2) { + return msg1.getMessageNumber() - msg2.getMessageNumber(); + } + }); + return toMessageSet(msgs, cond); + } + + /** + * Return UIDSets for the messages. Note that the UIDs + * must have already been fetched for the messages. + * + * @param msgs the messages + * @return the UIDSet array + */ + public static UIDSet[] toUIDSet(Message[] msgs) { + List<UIDSet> v = new ArrayList<>(1); + long current, next; + + IMAPMessage msg; + for (int i = 0; i < msgs.length; i++) { + msg = (IMAPMessage)msgs[i]; + if (msg.isExpunged()) // expunged message, skip it + continue; + + current = msg.getUID(); + + UIDSet set = new UIDSet(); + set.start = current; + + // Look for contiguous UIDs + for (++i; i < msgs.length; i++) { + // get next message + msg = (IMAPMessage)msgs[i]; + + if (msg.isExpunged()) // expunged message, skip it + continue; + next = msg.getUID(); + + if (next == current+1) + current = next; + else { // break in sequence + // We need to reexamine this message at the top of + // the outer loop, so decrement 'i' to cancel the + // outer loop's autoincrement + i--; + break; + } + } + set.end = current; + v.add(set); + } + + if (v.isEmpty()) // No valid messages + return null; + else { + return v.toArray(new UIDSet[v.size()]); + } + } + + /** + * Make the ResyncData UIDSet available to IMAPProtocol, + * which is in a different package. Note that this class + * is not included in the public javadocs, thus "hiding" + * this method. + * + * @param rd the ResyncData + * @return the UIDSet array + * @since JavaMail 1.5.1 + */ + public static UIDSet[] getResyncUIDSet(ResyncData rd) { + return rd.getUIDSet(); + } + + /** + * This interface defines the test to be executed in + * <code>toMessageSet()</code>. + */ + public static interface Condition { + public boolean test(IMAPMessage message); + } +}
diff --git a/mail/src/main/java/com/sun/mail/imap/YoungerTerm.java b/mail/src/main/java/com/sun/mail/imap/YoungerTerm.java new file mode 100644 index 0000000..e8264ae --- /dev/null +++ b/mail/src/main/java/com/sun/mail/imap/YoungerTerm.java
@@ -0,0 +1,96 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.util.Date; +import javax.mail.Message; +import javax.mail.search.SearchTerm; + +/** + * Find messages that are younger than a given interval (in seconds). + * Relies on the server implementing the WITHIN search extension + * (<A HREF="http://www.ietf.org/rfc/rfc5032.txt">RFC 5032</A>). + * + * @since JavaMail 1.5.1 + * @author Bill Shannon + */ +public final class YoungerTerm extends SearchTerm { + + private int interval; + + private static final long serialVersionUID = 1592714210688163496L; + + /** + * Constructor. + * + * @param interval number of seconds younger + */ + public YoungerTerm(int interval) { + this.interval = interval; + } + + /** + * Return the interval. + * + * @return the interval + */ + public int getInterval() { + return interval; + } + + /** + * The match method. + * + * @param msg the date comparator is applied to this Message's + * received date + * @return true if the comparison succeeds, otherwise false + */ + @Override + public boolean match(Message msg) { + Date d; + + try { + d = msg.getReceivedDate(); + } catch (Exception e) { + return false; + } + + if (d == null) + return false; + + return d.getTime() >= + System.currentTimeMillis() - ((long)interval * 1000); + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof YoungerTerm)) + return false; + return interval == ((YoungerTerm)obj).interval; + } + + /** + * Compute a hashCode for this object. + */ + @Override + public int hashCode() { + return interval; + } +}
diff --git a/mail/src/main/java/com/sun/mail/imap/package.html b/mail/src/main/java/com/sun/mail/imap/package.html new file mode 100644 index 0000000..fa5ccdd --- /dev/null +++ b/mail/src/main/java/com/sun/mail/imap/package.html
@@ -0,0 +1,1000 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> +<HTML> +<HEAD> +<meta http-equiv="Content-Type" content="text/html; charset=utf-8"> +<!-- + + Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<TITLE>com.sun.mail.imap package</TITLE> +</HEAD> +<BODY BGCOLOR="white"> + +<P> +An IMAP protocol provider for the JavaMail API +that provides access to an IMAP message store. +Both the IMAP4 and IMAP4rev1 protocols are supported. +Refer to <A HREF="http://www.ietf.org/rfc/rfc3501.txt" TARGET="_top"> +RFC 3501</A> +for more information. +The IMAP protocol provider also supports many IMAP extensions (described below). +Note that the server needs to support these extensions (and not all servers do) +in order to use the support in the IMAP provider. +You can query the server for support of these extensions using the +{@link com.sun.mail.imap.IMAPStore#hasCapability IMAPStore hasCapability} +method using the capability name defined by the extension +(see the appropriate RFC) after connecting to the server. +</P> +<STRONG>UIDPLUS Support</STRONG> +<P> +The IMAP UIDPLUS extension +(<A HREF="http://www.ietf.org/rfc/rfc4315.txt" TARGET="_top">RFC 4315</A>) +is supported via the IMAPFolder methods +{@link com.sun.mail.imap.IMAPFolder#addMessages addMessages}, +{@link com.sun.mail.imap.IMAPFolder#appendUIDMessages appendUIDMessages}, and +{@link com.sun.mail.imap.IMAPFolder#copyUIDMessages copyUIDMessages}. +</P> +<STRONG>MOVE Support</STRONG> +<P> +The IMAP MOVE extension +(<A HREF="http://www.ietf.org/rfc/rfc6851.txt" TARGET="_top">RFC 6851</A>) +is supported via the IMAPFolder methods +{@link com.sun.mail.imap.IMAPFolder#moveMessages moveMessages} and +{@link com.sun.mail.imap.IMAPFolder#moveUIDMessages moveUIDMessages}. +</P> +<STRONG>SASL Support</STRONG> +<P> +The IMAP protocol provider can use SASL +(<A HREF="http://www.ietf.org/rfc/rfc4422.txt" TARGET="_top">RFC 4422</A>) +authentication mechanisms on systems that support the +<CODE>javax.security.sasl</CODE> APIs. +The SASL-IR +(<A HREF="http://www.ietf.org/rfc/rfc4959.txt" TARGET="_top">RFC 4959</A>) +capability is also supported. +In addition to the SASL mechanisms that are built into +the SASL implementation, users can also provide additional +SASL mechanisms of their own design to support custom authentication +schemes. See the +<A HREF="http://download.oracle.com/javase/6/docs/technotes/guides/security/sasl/sasl-refguide.html" TARGET="_top"> +Java SASL API Programming and Deployment Guide</A> for details. +Note that the current implementation doesn't support SASL mechanisms +that provide their own integrity or confidentiality layer. +</P> +<STRONG>OAuth 2.0 Support</STRONG> +<P> +Support for OAuth 2.0 authentication via the +<A HREF="https://developers.google.com/gmail/xoauth2_protocol" TARGET="_top"> +XOAUTH2 authentication mechanism</A> is provided either through the SASL +support described above or as a built-in authentication mechanism in the +IMAP provider. +The OAuth 2.0 Access Token should be passed as the password for this mechanism. +See <A HREF="https://java.net/projects/javamail/pages/OAuth2" TARGET="_top"> +OAuth2 Support</A> for details. +</P> +<STRONG>Connection Pool</STRONG> +<P> +A connected IMAPStore maintains a pool of IMAP protocol objects for +use in communicating with the IMAP server. The IMAPStore will create +the initial AUTHENTICATED connection and seed the pool with this +connection. As folders are opened and new IMAP protocol objects are +needed, the IMAPStore will provide them from the connection pool, +or create them if none are available. When a folder is closed, +its IMAP protocol object is returned to the connection pool if the +pool is not over capacity. +</P> +<P> +A mechanism is provided for timing out idle connection pool IMAP +protocol objects. Timed out connections are closed and removed (pruned) +from the connection pool. +</P> +<P> +The connected IMAPStore object may or may not maintain a separate IMAP +protocol object that provides the store a dedicated connection to the +IMAP server. This is provided mainly for compatibility with previous +implementations of the IMAP protocol provider. +</P> +<STRONG>QUOTA Support</STRONG> +<P> +The IMAP QUOTA extension +(<A HREF="http://www.ietf.org/rfc/rfc2087.txt" TARGET="_top">RFC 2087</A>) +is supported via the +{@link javax.mail.QuotaAwareStore QuotaAwareStore} interface implemented by +{@link com.sun.mail.imap.IMAPStore IMAPStore}, and the +{@link com.sun.mail.imap.IMAPFolder#getQuota IMAPFolder getQuota} and +{@link com.sun.mail.imap.IMAPFolder#setQuota IMAPFolder setQuota} methods. +<STRONG>ACL Support</STRONG> +<P> +The IMAP ACL extension +(<A HREF="http://www.ietf.org/rfc/rfc2086.txt" TARGET="_top">RFC 2086</A>) +is supported via the +{@link com.sun.mail.imap.Rights Rights} class and the IMAPFolder methods +{@link com.sun.mail.imap.IMAPFolder#getACL getACL}, +{@link com.sun.mail.imap.IMAPFolder#addACL addACL}, +{@link com.sun.mail.imap.IMAPFolder#removeACL removeACL}, +{@link com.sun.mail.imap.IMAPFolder#addRights addRights}, +{@link com.sun.mail.imap.IMAPFolder#removeRights removeRights}, +{@link com.sun.mail.imap.IMAPFolder#listRights listRights}, and +{@link com.sun.mail.imap.IMAPFolder#myRights myRights}. +</P> +<STRONG>SORT Support</STRONG> +<P> +The IMAP SORT extension +(<A HREF="http://www.ietf.org/rfc/rfc5256.txt" TARGET="_top">RFC 5256</A>) +is supported via the +{@link com.sun.mail.imap.SortTerm SortTerm} class and the IMAPFolder +{@link com.sun.mail.imap.IMAPFolder#getSortedMessages getSortedMessages} +methods. +</P> +<STRONG>CONDSTORE and QRESYNC Support</STRONG> +<P> +Basic support is provided for the IMAP CONDSTORE +(<A HREF="http://www.ietf.org/rfc/rfc4551.txt" TARGET="_top">RFC 4551</A>) +and QRESYNC +(<A HREF="http://www.ietf.org/rfc/rfc5162.txt" TARGET="_top">RFC 5162</A>) +extensions for the purpose of resynchronizing a folder after offline operation. +Of course, the server must support these extensions. +Use of these extensions is enabled by using the new +{@link com.sun.mail.imap.IMAPFolder#open(int,com.sun.mail.imap.ResyncData) +IMAPFolder open} method and supplying an appropriate +{@link com.sun.mail.imap.ResyncData ResyncData} instance. +Using +{@link com.sun.mail.imap.ResyncData#CONDSTORE ResyncData.CONDSTORE} +enables the CONDSTORE extension, which allows you to discover the +modification sequence number (modseq) of messages using the +{@link com.sun.mail.imap.IMAPMessage#getModSeq IMAPMessage getModSeq} +method and the +{@link com.sun.mail.imap.IMAPFolder#getHighestModSeq +IMAPFolder getHighestModSeq} method. +Using a +{@link com.sun.mail.imap.ResyncData ResyncData} instance with appropriate +values also allows the server to report any changes in messages since the last +resynchronization. +The changes are reported as a list of +{@link javax.mail.event.MailEvent MailEvent} instances. +The special +{@link com.sun.mail.imap.MessageVanishedEvent MessageVanishedEvent} reports on +UIDs of messages that have been removed since the last resynchronization. +A +{@link javax.mail.event.MessageChangedEvent MessageChangedEvent} reports on +changes to flags of messages. +For example: +</P> +<PRE> + Folder folder = store.getFolder("whatever"); + IMAPFolder ifolder = (IMAPFolder)folder; + List<MailEvent> events = ifolder.open(Folder.READ_WRITE, + new ResyncData(prevUidValidity, prevModSeq)); + for (MailEvent ev : events) { + if (ev instanceOf MessageChangedEvent) { + // process flag changes + } else if (ev instanceof MessageVanishedEvent) { + // process messages that were removed + } + } +</PRE> +<P> +See the referenced RFCs for more details on these IMAP extensions. +</P> +<STRONG>WITHIN Search Support</STRONG> +<P> +The IMAP WITHIN search extension +(<A HREF="http://www.ietf.org/rfc/rfc5032.txt" TARGET="_top">RFC 5032</A>) +is supported via the +{@link com.sun.mail.imap.YoungerTerm YoungerTerm} and +{@link com.sun.mail.imap.OlderTerm OlderTerm} +{@link javax.mail.search.SearchTerm SearchTerms}, which can be used as follows: +</P> +<PRE> + // search for messages delivered in the last day + Message[] msgs = folder.search(new YoungerTerm(24 * 60 * 60)); +</PRE> +<STRONG>LOGIN-REFERRAL Support</STRONG> +<P> +The IMAP LOGIN-REFERRAL extension +(<A HREF="http://www.ietf.org/rfc/rfc2221.txt" TARGET="_top">RFC 2221</A>) +is supported. +If a login referral is received when connecting or when authentication fails, a +{@link com.sun.mail.imap.ReferralException ReferralException} is thrown. +A referral can also occur when login succeeds. By default, no exception is +thrown in this case. To force an exception to be thrown and the authentication +to fail, set the <code>mail.imap.referralexception</code> property to "true". +</P> +<STRONG>COMPRESS Support</STRONG> +<P> +The IMAP COMPRESS extension +(<A HREF="http://www.ietf.org/rfc/rfc4978.txt" TARGET="_top">RFC 4978</A>) +is supported. +If the server supports the extension and the +<code>mail.imap.compress.enable</code> property is set to "true", +compression will be enabled. +</P> +<STRONG>UTF-8 Support</STRONG> +<P> +The IMAP UTF8 extension +(<A HREF="http://www.ietf.org/rfc/rfc6855.txt" TARGET="_top">RFC 6855</A>) +is supported. +If the server supports the extension, the client will enable use of UTF-8, +allowing use of UTF-8 in IMAP protocol strings such as folder names. +</P> +<A NAME="properties"><STRONG>Properties</STRONG></A> +<P> +The IMAP protocol provider supports the following properties, +which may be set in the JavaMail <code>Session</code> object. +The properties are always set as strings; the Type column describes +how the string is interpreted. For example, use +</P> +<PRE> + props.put("mail.imap.port", "888"); +</PRE> +<P> +to set the <CODE>mail.imap.port</CODE> property, which is of type int. +</P> +<P> +Note that if you're using the "imaps" protocol to access IMAP over SSL, +all the properties would be named "mail.imaps.*". +</P> +<TABLE BORDER SUMMARY="IMAP properties"> +<TR> +<TH>Name</TH> +<TH>Type</TH> +<TH>Description</TH> +</TR> + +<TR> +<TD><A NAME="mail.imap.user">mail.imap.user</A></TD> +<TD>String</TD> +<TD>Default user name for IMAP.</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.host">mail.imap.host</A></TD> +<TD>String</TD> +<TD>The IMAP server to connect to.</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.port">mail.imap.port</A></TD> +<TD>int</TD> +<TD>The IMAP server port to connect to, if the connect() method doesn't +explicitly specify one. Defaults to 143.</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.partialfetch">mail.imap.partialfetch</A></TD> +<TD>boolean</TD> +<TD>Controls whether the IMAP partial-fetch capability should be used. +Defaults to true.</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.fetchsize">mail.imap.fetchsize</A></TD> +<TD>int</TD> +<TD>Partial fetch size in bytes. Defaults to 16K.</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.peek">mail.imap.peek</A></TD> +<TD>boolean</TD> +<TD> +If set to true, use the IMAP PEEK option when fetching body parts, +to avoid setting the SEEN flag on messages. +Defaults to false. +Can be overridden on a per-message basis by the +{@link com.sun.mail.imap.IMAPMessage#setPeek setPeek} +method on IMAPMessage. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.ignorebodystructuresize">mail.imap.ignorebodystructuresize</A></TD> +<TD>boolean</TD> +<TD>The IMAP BODYSTRUCTURE response includes the exact size of each body part. +Normally, this size is used to determine how much data to fetch for each +body part. +Some servers report this size incorrectly in some cases; this property can +be set to work around such server bugs. +If this property is set to true, this size is ignored and data is fetched +until the server reports the end of data. +This will result in an extra fetch if the data size is a multiple of the +block size. +Defaults to false.</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.connectiontimeout">mail.imap.connectiontimeout</A></TD> +<TD>int</TD> +<TD>Socket connection timeout value in milliseconds. +This timeout is implemented by java.net.Socket. +Default is infinite timeout.</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.timeout">mail.imap.timeout</A></TD> +<TD>int</TD> +<TD>Socket read timeout value in milliseconds. +This timeout is implemented by java.net.Socket. +Default is infinite timeout.</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.writetimeout">mail.imap.writetimeout</A></TD> +<TD>int</TD> +<TD>Socket write timeout value in milliseconds. +This timeout is implemented by using a +java.util.concurrent.ScheduledExecutorService per connection +that schedules a thread to close the socket if the timeout expires. +Thus, the overhead of using this timeout is one thread per connection. +Default is infinite timeout.</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.statuscachetimeout">mail.imap.statuscachetimeout</A></TD> +<TD>int</TD> +<TD>Timeout value in milliseconds for cache of STATUS command response. +Default is 1000 (1 second). Zero disables cache.</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.appendbuffersize">mail.imap.appendbuffersize</A></TD> +<TD>int</TD> +<TD> +Maximum size of a message to buffer in memory when appending to an IMAP +folder. If not set, or set to -1, there is no maximum and all messages +are buffered. If set to 0, no messages are buffered. If set to (e.g.) +8192, messages of 8K bytes or less are buffered, larger messages are +not buffered. Buffering saves cpu time at the expense of short term +memory usage. If you commonly append very large messages to IMAP +mailboxes you might want to set this to a moderate value (1M or less). +</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.connectionpoolsize">mail.imap.connectionpoolsize</A></TD> +<TD>int</TD> +<TD>Maximum number of available connections in the connection pool. +Default is 1.</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.connectionpooltimeout">mail.imap.connectionpooltimeout</A></TD> +<TD>int</TD> +<TD>Timeout value in milliseconds for connection pool connections. Default +is 45000 (45 seconds).</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.separatestoreconnection">mail.imap.separatestoreconnection</A></TD> +<TD>boolean</TD> +<TD>Flag to indicate whether to use a dedicated store connection for store +commands. Default is false.</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.allowreadonlyselect">mail.imap.allowreadonlyselect</A></TD> +<TD>boolean</TD> +<TD>If false, attempts to open a folder read/write will fail +if the SELECT command succeeds but indicates that the folder is READ-ONLY. +This sometimes indicates that the folder contents can'tbe changed, but +the flags are per-user and can be changed, such as might be the case for +public shared folders. If true, such open attempts will succeed, allowing +the flags to be changed. The <code>getMode</code> method on the +<code>Folder</code> object will return <code>Folder.READ_ONLY</code> +in this case even though the <code>open</code> method specified +<code>Folder.READ_WRITE</code>. Default is false.</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.auth.mechanisms">mail.imap.auth.mechanisms</A></TD> +<TD>String</TD> +<TD> +If set, lists the authentication mechanisms to consider, and the order +in which to consider them. Only mechanisms supported by the server and +supported by the current implementation will be used. +The default is <code>"PLAIN LOGIN NTLM"</code>, which includes all +the authentication mechanisms supported by the current implementation +except XOAUTH2. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.auth.login.disable">mail.imap.auth.login.disable</A></TD> +<TD>boolean</TD> +<TD>If true, prevents use of the non-standard <code>AUTHENTICATE LOGIN</code> +command, instead using the plain <code>LOGIN</code> command. +Default is false.</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.auth.plain.disable">mail.imap.auth.plain.disable</A></TD> +<TD>boolean</TD> +<TD>If true, prevents use of the <code>AUTHENTICATE PLAIN</code> command. +Default is false.</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.auth.ntlm.disable">mail.imap.auth.ntlm.disable</A></TD> +<TD>boolean</TD> +<TD>If true, prevents use of the <code>AUTHENTICATE NTLM</code> command. +Default is false.</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.auth.ntlm.domain">mail.imap.auth.ntlm.domain</A></TD> +<TD>String</TD> +<TD> +The NTLM authentication domain. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.auth.ntlm.flags">mail.imap.auth.ntlm.flags</A></TD> +<TD>int</TD> +<TD> +NTLM protocol-specific flags. +See <A HREF="http://curl.haxx.se/rfc/ntlm.html#theNtlmFlags" TARGET="_top"> +http://curl.haxx.se/rfc/ntlm.html#theNtlmFlags</A> for details. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.auth.xoauth2.disable">mail.imap.auth.xoauth2.disable</A></TD> +<TD>boolean</TD> +<TD>If true, prevents use of the <code>AUTHENTICATE XOAUTH2</code> command. +Because the OAuth 2.0 protocol requires a special access token instead of +a password, this mechanism is disabled by default. Enable it by explicitly +setting this property to "false" or by setting the "mail.imap.auth.mechanisms" +property to "XOAUTH2".</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.proxyauth.user">mail.imap.proxyauth.user</A></TD> +<TD>String</TD> +<TD>If the server supports the PROXYAUTH extension, this property +specifies the name of the user to act as. Authenticate to the +server using the administrator's credentials. After authentication, +the IMAP provider will issue the <code>PROXYAUTH</code> command with +the user name specified in this property. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.localaddress">mail.imap.localaddress</A></TD> +<TD>String</TD> +<TD> +Local address (host name) to bind to when creating the IMAP socket. +Defaults to the address picked by the Socket class. +Should not normally need to be set, but useful with multi-homed hosts +where it's important to pick a particular local address to bind to. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.localport">mail.imap.localport</A></TD> +<TD>int</TD> +<TD> +Local port number to bind to when creating the IMAP socket. +Defaults to the port number picked by the Socket class. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.sasl.enable">mail.imap.sasl.enable</A></TD> +<TD>boolean</TD> +<TD> +If set to true, attempt to use the javax.security.sasl package to +choose an authentication mechanism for login. +Defaults to false. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.sasl.mechanisms">mail.imap.sasl.mechanisms</A></TD> +<TD>String</TD> +<TD> +A space or comma separated list of SASL mechanism names to try +to use. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.sasl.authorizationid">mail.imap.sasl.authorizationid</A></TD> +<TD>String</TD> +<TD> +The authorization ID to use in the SASL authentication. +If not set, the authentication ID (user name) is used. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.sasl.realm">mail.imap.sasl.realm</A></TD> +<TD>String</TD> +<TD>The realm to use with SASL authentication mechanisms that +require a realm, such as DIGEST-MD5.</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.sasl.usecanonicalhostname">mail.imap.sasl.usecanonicalhostname</A></TD> +<TD>boolean</TD> +<TD> +If set to true, the canonical host name returned by +{@link java.net.InetAddress#getCanonicalHostName InetAddress.getCanonicalHostName} +is passed to the SASL mechanism, instead of the host name used to connect. +Defaults to false. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.sasl.xgwtrustedapphack.enable">mail.imap.sasl. xgwtrustedapphack.enable</A></TD> +<TD>boolean</TD> +<TD> +If set to true, enables a workaround for a bug in the Novell Groupwise +XGWTRUSTEDAPP SASL mechanism, when that mechanism is being used. +Defaults to true. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.socketFactory">mail.imap.socketFactory</A></TD> +<TD>SocketFactory</TD> +<TD> +If set to a class that implements the +<code>javax.net.SocketFactory</code> interface, this class +will be used to create IMAP sockets. Note that this is an +instance of a class, not a name, and must be set using the +<code>put</code> method, not the <code>setProperty</code> method. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.socketFactory.class">mail.imap.socketFactory.class</A></TD> +<TD>String</TD> +<TD> +If set, specifies the name of a class that implements the +<code>javax.net.SocketFactory</code> interface. This class +will be used to create IMAP sockets. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.socketFactory.fallback">mail.imap.socketFactory.fallback</A></TD> +<TD>boolean</TD> +<TD> +If set to true, failure to create a socket using the specified +socket factory class will cause the socket to be created using +the <code>java.net.Socket</code> class. +Defaults to true. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.socketFactory.port">mail.imap.socketFactory.port</A></TD> +<TD>int</TD> +<TD> +Specifies the port to connect to when using the specified socket +factory. +If not set, the default port will be used. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.usesocketchannels">mail.imap.usesocketchannels</A></TD> +<TD>boolean</TD> +<TD> +If set to true, use SocketChannels instead of Sockets for connecting +to the server. Required if using the IdleManager. +Ignored if a socket factory is set. +Defaults to false. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.ssl.enable">mail.imap.ssl.enable</A></TD> +<TD>boolean</TD> +<TD> +If set to true, use SSL to connect and use the SSL port by default. +Defaults to false for the "imap" protocol and true for the "imaps" protocol. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.ssl.checkserveridentity">mail.imap.ssl.checkserveridentity</A></TD> +<TD>boolean</TD> +<TD> +If set to true, check the server identity as specified by +<A HREF="http://www.ietf.org/rfc/rfc2595.txt" TARGET="_top">RFC 2595</A>. +These additional checks based on the content of the server's certificate +are intended to prevent man-in-the-middle attacks. +Defaults to false. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.ssl.trust">mail.imap.ssl.trust</A></TD> +<TD>String</TD> +<TD> +If set, and a socket factory hasn't been specified, enables use of a +{@link com.sun.mail.util.MailSSLSocketFactory MailSSLSocketFactory}. +If set to "*", all hosts are trusted. +If set to a whitespace separated list of hosts, those hosts are trusted. +Otherwise, trust depends on the certificate the server presents. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.ssl.socketFactory">mail.imap.ssl.socketFactory</A></TD> +<TD>SSLSocketFactory</TD> +<TD> +If set to a class that extends the +<code>javax.net.ssl.SSLSocketFactory</code> class, this class +will be used to create IMAP SSL sockets. Note that this is an +instance of a class, not a name, and must be set using the +<code>put</code> method, not the <code>setProperty</code> method. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.ssl.socketFactory.class">mail.imap.ssl.socketFactory.class</A></TD> +<TD>String</TD> +<TD> +If set, specifies the name of a class that extends the +<code>javax.net.ssl.SSLSocketFactory</code> class. This class +will be used to create IMAP SSL sockets. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.ssl.socketFactory.port">mail.imap.ssl.socketFactory.port</A></TD> +<TD>int</TD> +<TD> +Specifies the port to connect to when using the specified socket +factory. +If not set, the default port will be used. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.ssl.protocols">mail.imap.ssl.protocols</A></TD> +<TD>string</TD> +<TD> +Specifies the SSL protocols that will be enabled for SSL connections. +The property value is a whitespace separated list of tokens acceptable +to the <code>javax.net.ssl.SSLSocket.setEnabledProtocols</code> method. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.ssl.ciphersuites">mail.imap.ssl.ciphersuites</A></TD> +<TD>string</TD> +<TD> +Specifies the SSL cipher suites that will be enabled for SSL connections. +The property value is a whitespace separated list of tokens acceptable +to the <code>javax.net.ssl.SSLSocket.setEnabledCipherSuites</code> method. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.starttls.enable">mail.imap.starttls.enable</A></TD> +<TD>boolean</TD> +<TD>If true, enables the use of the <code>STARTTLS</code> command (if +supported by the server) to switch the connection to a TLS-protected +connection before issuing any login commands. +If the server does not support STARTTLS, the connection continues without +the use of TLS; see the +<A HREF="#mail.imap.starttls.required"><code>mail.imap.starttls.required</code></A> +property to fail if STARTTLS isn't supported. +Note that an appropriate trust store must configured so that the client +will trust the server's certificate. +Default is false.</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.starttls.required">mail.imap.starttls.required</A></TD> +<TD>boolean</TD> +<TD> +If true, requires the use of the <code>STARTTLS</code> command. +If the server doesn't support the STARTTLS command, or the command +fails, the connect method will fail. +Defaults to false. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.proxy.host">mail.imap.proxy.host</A></TD> +<TD>string</TD> +<TD> +Specifies the host name of an HTTP web proxy server that will be used for +connections to the mail server. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.proxy.port">mail.imap.proxy.port</A></TD> +<TD>string</TD> +<TD> +Specifies the port number for the HTTP web proxy server. +Defaults to port 80. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.proxy.user">mail.imap.proxy.user</A></TD> +<TD>string</TD> +<TD> +Specifies the user name to use to authenticate with the HTTP web proxy server. +By default, no authentication is done. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.proxy.password">mail.imap.proxy.password</A></TD> +<TD>string</TD> +<TD> +Specifies the password to use to authenticate with the HTTP web proxy server. +By default, no authentication is done. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.socks.host">mail.imap.socks.host</A></TD> +<TD>string</TD> +<TD> +Specifies the host name of a SOCKS5 proxy server that will be used for +connections to the mail server. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.socks.port">mail.imap.socks.port</A></TD> +<TD>string</TD> +<TD> +Specifies the port number for the SOCKS5 proxy server. +This should only need to be used if the proxy server is not using +the standard port number of 1080. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.minidletime">mail.imap.minidletime</A></TD> +<TD>int</TD> +<TD> +Applications typically call the idle method in a loop. If another +thread termiantes the IDLE command, it needs a chance to do its +work before another IDLE command is issued. The idle method enforces +a delay to prevent thrashing between the IDLE command and regular +commands. This property sets the delay in milliseconds. If not +set, the default is 10 milliseconds. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.enableresponseevents">mail.imap.enableresponseevents</A></TD> +<TD>boolean</TD> +<TD> +Enable special IMAP-specific events to be delivered to the Store's +<code>ConnectionListener</code>. If true, IMAP OK, NO, BAD, or BYE responses +will be sent as <code>ConnectionEvent</code>s with a type of +<code>IMAPStore.RESPONSE</code>. The event's message will be the +raw IMAP response string. +By default, these events are not sent. +NOTE: This capability is highly experimental and likely will change +in future releases. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.enableimapevents">mail.imap.enableimapevents</A></TD> +<TD>boolean</TD> +<TD> +Enable special IMAP-specific events to be delivered to the Store's +<code>ConnectionListener</code>. If true, unsolicited responses +received during the Store's <code>idle</code> method will be sent +as <code>ConnectionEvent</code>s with a type of +<code>IMAPStore.RESPONSE</code>. The event's message will be the +raw IMAP response string. +By default, these events are not sent. +NOTE: This capability is highly experimental and likely will change +in future releases. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.throwsearchexception">mail.imap.throwsearchexception</A></TD> +<TD>boolean</TD> +<TD> +If set to true and a {@link javax.mail.search.SearchTerm SearchTerm} +passed to the +{@link javax.mail.Folder#search Folder.search} +method is too complex for the IMAP protocol, throw a +{@link javax.mail.search.SearchException SearchException}. +For example, the IMAP protocol only supports less-than and greater-than +comparisons for a {@link javax.mail.search.SizeTerm SizeTerm}. +If false, the search will be done locally by fetching the required +message data and comparing it locally. +Defaults to false. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.folder.class">mail.imap.folder.class</A></TD> +<TD>String</TD> +<TD> +Class name of a subclass of <code>com.sun.mail.imap.IMAPFolder</code>. +The subclass can be used to provide support for additional IMAP commands. +The subclass must have public constructors of the form +<code>public MyIMAPFolder(String fullName, char separator, IMAPStore store, +Boolean isNamespace)</code> and +<code>public MyIMAPFolder(ListInfo li, IMAPStore store)</code> +</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.closefoldersonstorefailure">mail.imap.closefoldersonstorefailure</A></TD> +<TD>boolean</TD> +<TD> +In some cases, a failure of the Store connection indicates a failure of the +server, and all Folders associated with that Store should also be closed. +In other cases, a Store connection failure may be a transient failure, and +Folders may continue to operate normally. +If this property is true (the default), failures in the Store connection cause +all associated Folders to be closed. +Set this property to false to better handle transient failures in the Store +connection. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.finalizecleanclose">mail.imap.finalizecleanclose</A></TD> +<TD>boolean</TD> +<TD> +When the finalizer for IMAPStore is called, +should the connection to the server be closed cleanly, as if the +application called the close method? +Or should the connection to the server be closed without sending +any commands to the server? +Defaults to false, the connection is not closed cleanly. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.referralexception">mail.imap.referralexception</A></TD> +<TD>boolean</TD> +<TD> +If set to true and an IMAP login referral is returned when the authentication +succeeds, fail the connect request and throw a +{@link com.sun.mail.imap.ReferralException ReferralException}. +Defaults to false. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.compress.enable">mail.imap.compress.enable</A></TD> +<TD>boolean</TD> +<TD> +If set to true and the IMAP server supports the COMPRESS=DEFLATE extension, +compression will be enabled. +Defaults to false. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.compress.level">mail.imap.compress.level</A></TD> +<TD>int</TD> +<TD> +The compression level to be used, in the range -1 to 9. +See the {@link java.util.zip.Deflater Deflater} class for details. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.compress.strategy">mail.imap.compress.strategy</A></TD> +<TD>int</TD> +<TD> +The compression strategy to be used, in the range 0 to 2. +See the {@link java.util.zip.Deflater Deflater} class for details. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.imap.reusetagprefix">mail.imap.compress.strategy</A></TD> +<TD>boolean</TD> +<TD> +If true, always use "A" for the IMAP command tag prefix. +If false, the IMAP command tag prefix is different for each connection, +from "A" through "ZZZ" and then wrapping around to "A". +Applications should never need to set this. +Defaults to false. +</TD> +</TR> + +</TABLE> +<P> +In general, applications should not need to use the classes in this +package directly. Instead, they should use the APIs defined by the +<code>javax.mail</code> package (and subpackages). Applications should +never construct instances of <code>IMAPStore</code> or +<code>IMAPFolder</code> directly. Instead, they should use the +<code>Session</code> method <code>getStore</code> to acquire an +appropriate <code>Store</code> object, and from that acquire +<code>Folder</code> objects. +</P> +<STRONG>Loggers</STRONG> +<P> +In addition to printing debugging output as controlled by the +{@link javax.mail.Session Session} configuration, +the com.sun.mail.imap provider logs the same information using +{@link java.util.logging.Logger} as described in the following table: +</P> +<TABLE BORDER SUMMARY="IMAP Loggers"> +<TR> +<TH>Logger Name</TH> +<TH>Logging Level</TH> +<TH>Purpose</TH> +</TR> + +<TR> +<TD>com.sun.mail.imap</TD> +<TD>CONFIG</TD> +<TD>Configuration of the IMAPStore</TD> +</TR> + +<TR> +<TD>com.sun.mail.imap</TD> +<TD>FINE</TD> +<TD>General debugging output</TD> +</TR> + +<TR> +<TD>com.sun.mail.imap.connectionpool</TD> +<TD>CONFIG</TD> +<TD>Configuration of the IMAP connection pool</TD> +</TR> + +<TR> +<TD>com.sun.mail.imap.connectionpool</TD> +<TD>FINE</TD> +<TD>Debugging output related to the IMAP connection pool</TD> +</TR> + +<TR> +<TD>com.sun.mail.imap.messagecache</TD> +<TD>CONFIG</TD> +<TD>Configuration of the IMAP message cache</TD> +</TR> + +<TR> +<TD>com.sun.mail.imap.messagecache</TD> +<TD>FINE</TD> +<TD>Debugging output related to the IMAP message cache</TD> +</TR> + +<TR> +<TD>com.sun.mail.imap.protocol</TD> +<TD>FINEST</TD> +<TD>Complete protocol trace</TD> +</TR> +</TABLE> + +<STRONG>WARNING</STRONG> +<P> +<strong>WARNING:</strong> The APIs unique to this package should be +considered <strong>EXPERIMENTAL</strong>. They may be changed in the +future in ways that are incompatible with applications using the +current APIs. +</P> + +</BODY> +</HTML>
diff --git a/mail/src/main/java/com/sun/mail/imap/protocol/BASE64MailboxDecoder.java b/mail/src/main/java/com/sun/mail/imap/protocol/BASE64MailboxDecoder.java new file mode 100644 index 0000000..af35020 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/imap/protocol/BASE64MailboxDecoder.java
@@ -0,0 +1,170 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import java.text.StringCharacterIterator; +import java.text.CharacterIterator; + +/** + * See the BASE64MailboxEncoder for a description of the RFC2060 and how + * mailbox names should be encoded. This class will do the correct decoding + * for mailbox names. + * + * @author Christopher Cotton + */ + +public class BASE64MailboxDecoder { + + public static String decode(String original) { + if (original == null || original.length() == 0) + return original; + + boolean changedString = false; + int copyTo = 0; + // it will always be less than the original + char[] chars = new char[original.length()]; + StringCharacterIterator iter = new StringCharacterIterator(original); + + for(char c = iter.first(); c != CharacterIterator.DONE; + c = iter.next()) { + + if (c == '&') { + changedString = true; + copyTo = base64decode(chars, copyTo, iter); + } else { + chars[copyTo++] = c; + } + } + + // now create our string from the char array + if (changedString) { + return new String(chars, 0, copyTo); + } else { + return original; + } + } + + + protected static int base64decode(char[] buffer, int offset, + CharacterIterator iter) { + boolean firsttime = true; + int leftover = -1; + + while(true) { + // get the first byte + byte orig_0 = (byte) iter.next(); + if (orig_0 == -1) break; // no more chars + if (orig_0 == '-') { + if (firsttime) { + // means we got the string "&-" which is turned into a "&" + buffer[offset++] = '&'; + } + // we are done now + break; + } + firsttime = false; + + // next byte + byte orig_1 = (byte) iter.next(); + if (orig_1 == -1 || orig_1 == '-') + break; // no more chars, invalid base64 + + byte a, b, current; + a = pem_convert_array[orig_0 & 0xff]; + b = pem_convert_array[orig_1 & 0xff]; + // The first decoded byte + current = (byte)(((a << 2) & 0xfc) | ((b >>> 4) & 3)); + + // use the leftover to create a Unicode Character (2 bytes) + if (leftover != -1) { + buffer[offset++] = (char)(leftover << 8 | (current & 0xff)); + leftover = -1; + } else { + leftover = current & 0xff; + } + + byte orig_2 = (byte) iter.next(); + if (orig_2 == '=') { // End of this BASE64 encoding + continue; + } else if (orig_2 == -1 || orig_2 == '-') { + break; // no more chars + } + + // second decoded byte + a = b; + b = pem_convert_array[orig_2 & 0xff]; + current = (byte)(((a << 4) & 0xf0) | ((b >>> 2) & 0xf)); + + // use the leftover to create a Unicode Character (2 bytes) + if (leftover != -1) { + buffer[offset++] = (char)(leftover << 8 | (current & 0xff)); + leftover = -1; + } else { + leftover = current & 0xff; + } + + byte orig_3 = (byte) iter.next(); + if (orig_3 == '=') { // End of this BASE64 encoding + continue; + } else if (orig_3 == -1 || orig_3 == '-') { + break; // no more chars + } + + // The third decoded byte + a = b; + b = pem_convert_array[orig_3 & 0xff]; + current = (byte)(((a << 6) & 0xc0) | (b & 0x3f)); + + // use the leftover to create a Unicode Character (2 bytes) + if (leftover != -1) { + buffer[offset++] = (char)(leftover << 8 | (current & 0xff)); + leftover = -1; + } else { + leftover = current & 0xff; + } + } + + return offset; + } + + /** + * This character array provides the character to value map + * based on RFC1521, but with the modification from RFC2060 + * which changes the '/' to a ','. + */ + + // shared with BASE64MailboxEncoder + static final char pem_array[] = { + 'A','B','C','D','E','F','G','H', // 0 + 'I','J','K','L','M','N','O','P', // 1 + 'Q','R','S','T','U','V','W','X', // 2 + 'Y','Z','a','b','c','d','e','f', // 3 + 'g','h','i','j','k','l','m','n', // 4 + 'o','p','q','r','s','t','u','v', // 5 + 'w','x','y','z','0','1','2','3', // 6 + '4','5','6','7','8','9','+',',' // 7 + }; + + private static final byte pem_convert_array[] = new byte[256]; + + static { + for (int i = 0; i < 255; i++) + pem_convert_array[i] = -1; + for(int i = 0; i < pem_array.length; i++) + pem_convert_array[pem_array[i]] = (byte) i; + } +}
diff --git a/mail/src/main/java/com/sun/mail/imap/protocol/BASE64MailboxEncoder.java b/mail/src/main/java/com/sun/mail/imap/protocol/BASE64MailboxEncoder.java new file mode 100644 index 0000000..1013baf --- /dev/null +++ b/mail/src/main/java/com/sun/mail/imap/protocol/BASE64MailboxEncoder.java
@@ -0,0 +1,230 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import java.io.*; + + +/** + * From RFC2060: + * + * <blockquote><pre> + * + * 5.1.3. Mailbox International Naming Convention + * + * By convention, international mailbox names are specified using a + * modified version of the UTF-7 encoding described in [UTF-7]. The + * purpose of these modifications is to correct the following problems + * with UTF-7: + * + * 1) UTF-7 uses the "+" character for shifting; this conflicts with + * the common use of "+" in mailbox names, in particular USENET + * newsgroup names. + * + * 2) UTF-7's encoding is BASE64 which uses the "/" character; this + * conflicts with the use of "/" as a popular hierarchy delimiter. + * + * 3) UTF-7 prohibits the unencoded usage of "\"; this conflicts with + * the use of "\" as a popular hierarchy delimiter. + * + * 4) UTF-7 prohibits the unencoded usage of "~"; this conflicts with + * the use of "~" in some servers as a home directory indicator. + * + * 5) UTF-7 permits multiple alternate forms to represent the same + * string; in particular, printable US-ASCII chararacters can be + * represented in encoded form. + * + * In modified UTF-7, printable US-ASCII characters except for "&" + * represent themselves; that is, characters with octet values 0x20-0x25 + * and 0x27-0x7e. The character "&" (0x26) is represented by the two- + * octet sequence "&-". + * + * All other characters (octet values 0x00-0x1f, 0x7f-0xff, and all + * Unicode 16-bit octets) are represented in modified BASE64, with a + * further modification from [UTF-7] that "," is used instead of "/". + * Modified BASE64 MUST NOT be used to represent any printing US-ASCII + * character which can represent itself. + * + * "&" is used to shift to modified BASE64 and "-" to shift back to US- + * ASCII. All names start in US-ASCII, and MUST end in US-ASCII (that + * is, a name that ends with a Unicode 16-bit octet MUST end with a "- + * "). + * + * For example, here is a mailbox name which mixes English, Japanese, + * and Chinese text: ~peter/mail/&ZeVnLIqe-/&U,BTFw- + * + * </pre></blockquote> + * + * This class will do the correct Encoding for the IMAP mailboxes. + * + * @author Christopher Cotton + */ + +public class BASE64MailboxEncoder { + protected byte[] buffer = new byte[4]; + protected int bufsize = 0; + protected boolean started = false; + protected Writer out = null; + + + public static String encode(String original) { + BASE64MailboxEncoder base64stream = null; + char origchars[] = original.toCharArray(); + int length = origchars.length; + boolean changedString = false; + CharArrayWriter writer = new CharArrayWriter(length); + + // loop over all the chars + for(int index = 0; index < length; index++) { + char current = origchars[index]; + + // octets in the range 0x20-0x25,0x27-0x7e are themselves + // 0x26 "&" is represented as "&-" + if (current >= 0x20 && current <= 0x7e) { + if (base64stream != null) { + base64stream.flush(); + } + + if (current == '&') { + changedString = true; + writer.write('&'); + writer.write('-'); + } else { + writer.write(current); + } + } else { + + // use a B64MailboxEncoder to write out the other bytes + // as a modified BASE64. The stream will write out + // the beginning '&' and the ending '-' which is part + // of every encoding. + + if (base64stream == null) { + base64stream = new BASE64MailboxEncoder(writer); + changedString = true; + } + + base64stream.write(current); + } + } + + + if (base64stream != null) { + base64stream.flush(); + } + + if (changedString) { + return writer.toString(); + } else { + return original; + } + } + + + /** + * Create a BASE64 encoder + * + * @param what where to write the encoded name + */ + public BASE64MailboxEncoder(Writer what) { + out = what; + } + + public void write(int c) { + try { + // write out the initial character if this is the first time + if (!started) { + started = true; + out.write('&'); + } + + // we write each character as a 2 byte unicode character + buffer[bufsize++] = (byte) (c >> 8); + buffer[bufsize++] = (byte) (c & 0xff); + + if (bufsize >= 3) { + encode(); + bufsize -= 3; + } + } catch (IOException e) { + //e.printStackTrace(); + } + } + + + public void flush() { + try { + // flush any bytes we have + if (bufsize > 0) { + encode(); + bufsize = 0; + } + + // write the terminating character of the encoding + if (started) { + out.write('-'); + started = false; + } + } catch (IOException e) { + //e.printStackTrace(); + } + } + + + protected void encode() throws IOException { + byte a, b, c; + if (bufsize == 1) { + a = buffer[0]; + b = 0; + c = 0; + out.write(pem_array[(a >>> 2) & 0x3F]); + out.write(pem_array[((a << 4) & 0x30) + ((b >>> 4) & 0xf)]); + // no padding characters are written + } else if (bufsize == 2) { + a = buffer[0]; + b = buffer[1]; + c = 0; + out.write(pem_array[(a >>> 2) & 0x3F]); + out.write(pem_array[((a << 4) & 0x30) + ((b >>> 4) & 0xf)]); + out.write(pem_array[((b << 2) & 0x3c) + ((c >>> 6) & 0x3)]); + // no padding characters are written + } else { + a = buffer[0]; + b = buffer[1]; + c = buffer[2]; + out.write(pem_array[(a >>> 2) & 0x3F]); + out.write(pem_array[((a << 4) & 0x30) + ((b >>> 4) & 0xf)]); + out.write(pem_array[((b << 2) & 0x3c) + ((c >>> 6) & 0x3)]); + out.write(pem_array[c & 0x3F]); + + // copy back the extra byte + if (bufsize == 4) + buffer[0] = buffer[3]; + } + } + + private final static char pem_array[] = { + 'A','B','C','D','E','F','G','H', // 0 + 'I','J','K','L','M','N','O','P', // 1 + 'Q','R','S','T','U','V','W','X', // 2 + 'Y','Z','a','b','c','d','e','f', // 3 + 'g','h','i','j','k','l','m','n', // 4 + 'o','p','q','r','s','t','u','v', // 5 + 'w','x','y','z','0','1','2','3', // 6 + '4','5','6','7','8','9','+',',' // 7 + }; +}
diff --git a/mail/src/main/java/com/sun/mail/imap/protocol/BODY.java b/mail/src/main/java/com/sun/mail/imap/protocol/BODY.java new file mode 100644 index 0000000..abaed61 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/imap/protocol/BODY.java
@@ -0,0 +1,87 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import java.io.ByteArrayInputStream; +import com.sun.mail.iap.*; +import com.sun.mail.util.ASCIIUtility; + +/** + * The BODY fetch response item. + * + * @author John Mani + * @author Bill Shannon + */ + +public class BODY implements Item { + + static final char[] name = {'B','O','D','Y'}; + + private final int msgno; + private final ByteArray data; + private final String section; + private final int origin; + private final boolean isHeader; + + /** + * Constructor + * + * @param r the FetchResponse + * @exception ParsingException for parsing failures + */ + public BODY(FetchResponse r) throws ParsingException { + msgno = r.getNumber(); + + r.skipSpaces(); + + if (r.readByte() != '[') + throw new ParsingException( + "BODY parse error: missing ``['' at section start"); + section = r.readString(']'); + if (r.readByte() != ']') + throw new ParsingException( + "BODY parse error: missing ``]'' at section end"); + isHeader = section.regionMatches(true, 0, "HEADER", 0, 6); + + if (r.readByte() == '<') { // origin + origin = r.readNumber(); + r.skip(1); // skip '>'; + } else + origin = 0; + + data = r.readByteArray(); + } + + public ByteArray getByteArray() { + return data; + } + + public ByteArrayInputStream getByteArrayInputStream() { + if (data != null) + return data.toByteArrayInputStream(); + else + return null; + } + + public boolean isHeader() { + return isHeader; + } + + public String getSection() { + return section; + } +}
diff --git a/mail/src/main/java/com/sun/mail/imap/protocol/BODYSTRUCTURE.java b/mail/src/main/java/com/sun/mail/imap/protocol/BODYSTRUCTURE.java new file mode 100644 index 0000000..3a5945c --- /dev/null +++ b/mail/src/main/java/com/sun/mail/imap/protocol/BODYSTRUCTURE.java
@@ -0,0 +1,450 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import java.util.List; +import java.util.ArrayList; +import javax.mail.internet.ParameterList; +import com.sun.mail.iap.*; +import com.sun.mail.util.PropUtil; + +/** + * A BODYSTRUCTURE response. + * + * @author John Mani + * @author Bill Shannon + */ + +public class BODYSTRUCTURE implements Item { + + static final char[] name = + {'B','O','D','Y','S','T','R','U','C','T','U','R','E'}; + public int msgno; + + public String type; // Type + public String subtype; // Subtype + public String encoding; // Encoding + public int lines = -1; // Size in lines + public int size = -1; // Size in bytes + public String disposition; // Disposition + public String id; // Content-ID + public String description; // Content-Description + public String md5; // MD-5 checksum + public String attachment; // Attachment name + public ParameterList cParams; // Body parameters + public ParameterList dParams; // Disposition parameters + public String[] language; // Language + public BODYSTRUCTURE[] bodies; // array of BODYSTRUCTURE objects + // for multipart & message/rfc822 + public ENVELOPE envelope; // for message/rfc822 + + private static int SINGLE = 1; + private static int MULTI = 2; + private static int NESTED = 3; + private int processedType; // MULTI | SINGLE | NESTED + + // special debugging output to debug parsing errors + private static final boolean parseDebug = + PropUtil.getBooleanSystemProperty("mail.imap.parse.debug", false); + + + public BODYSTRUCTURE(FetchResponse r) throws ParsingException { + if (parseDebug) + System.out.println("DEBUG IMAP: parsing BODYSTRUCTURE"); + msgno = r.getNumber(); + if (parseDebug) + System.out.println("DEBUG IMAP: msgno " + msgno); + + r.skipSpaces(); + + if (r.readByte() != '(') + throw new ParsingException( + "BODYSTRUCTURE parse error: missing ``('' at start"); + + if (r.peekByte() == '(') { // multipart + if (parseDebug) + System.out.println("DEBUG IMAP: parsing multipart"); + type = "multipart"; + processedType = MULTI; + List<BODYSTRUCTURE> v = new ArrayList<>(1); + int i = 1; + do { + v.add(new BODYSTRUCTURE(r)); + /* + * Even though the IMAP spec says there can't be any spaces + * between parts, some servers erroneously put a space in + * here. In the spirit of "be liberal in what you accept", + * we skip it. + */ + r.skipSpaces(); + } while (r.peekByte() == '('); + + // setup bodies. + bodies = v.toArray(new BODYSTRUCTURE[v.size()]); + + subtype = r.readString(); // subtype + if (parseDebug) + System.out.println("DEBUG IMAP: subtype " + subtype); + + if (r.isNextNonSpace(')')) { // done + if (parseDebug) + System.out.println("DEBUG IMAP: parse DONE"); + return; + } + + // Else, we have extension data + + if (parseDebug) + System.out.println("DEBUG IMAP: parsing extension data"); + // Body parameters + cParams = parseParameters(r); + if (r.isNextNonSpace(')')) { // done + if (parseDebug) + System.out.println("DEBUG IMAP: body parameters DONE"); + return; + } + + // Disposition + byte b = r.peekByte(); + if (b == '(') { + if (parseDebug) + System.out.println("DEBUG IMAP: parse disposition"); + r.readByte(); + disposition = r.readString(); + if (parseDebug) + System.out.println("DEBUG IMAP: disposition " + + disposition); + dParams = parseParameters(r); + if (!r.isNextNonSpace(')')) // eat the end ')' + throw new ParsingException( + "BODYSTRUCTURE parse error: " + + "missing ``)'' at end of disposition in multipart"); + if (parseDebug) + System.out.println("DEBUG IMAP: disposition DONE"); + } else if (b == 'N' || b == 'n') { + if (parseDebug) + System.out.println("DEBUG IMAP: disposition NIL"); + r.skip(3); // skip 'NIL' + } else { + /* + throw new ParsingException( + "BODYSTRUCTURE parse error: " + + type + "/" + subtype + ": " + + "bad multipart disposition, b " + b); + */ + if (parseDebug) + System.out.println("DEBUG IMAP: bad multipart disposition" + + ", applying Exchange bug workaround"); + description = r.readString(); + if (parseDebug) + System.out.println("DEBUG IMAP: multipart description " + + description); + // Throw away whatever comes after it, since we have no + // idea what it's supposed to be + while (r.readByte() == ' ') + parseBodyExtension(r); + return; + } + + // RFC3501 allows no body-fld-lang after body-fld-disp, + // even though RFC2060 required it + if (r.isNextNonSpace(')')) { + if (parseDebug) + System.out.println("DEBUG IMAP: no body-fld-lang"); + return; // done + } + + // Language + if (r.peekByte() == '(') { // a list follows + language = r.readStringList(); + if (parseDebug) + System.out.println( + "DEBUG IMAP: language len " + language.length); + } else { + String l = r.readString(); + if (l != null) { + String[] la = { l }; + language = la; + if (parseDebug) + System.out.println("DEBUG IMAP: language " + l); + } + } + + // RFC3501 defines an optional "body location" next, + // but for now we ignore it along with other extensions. + + // Throw away any further extension data + while (r.readByte() == ' ') + parseBodyExtension(r); + } else if (r.peekByte() == ')') { // (illegal) empty body + /* + * Domino will fail to return the body structure of nested messages. + * Fake it by providing an empty message. Could probably do better + * with more work... + */ + /* + * XXX - this prevents the exception, but without the exception + * the application has no way to know the data from the message + * is missing. + * + if (parseDebug) + System.out.println("DEBUG IMAP: empty body, fake it"); + r.readByte(); + type = "text"; + subtype = "plain"; + lines = 0; + size = 0; + */ + throw new ParsingException( + "BODYSTRUCTURE parse error: missing body content"); + } else { // Single part + if (parseDebug) + System.out.println("DEBUG IMAP: single part"); + type = r.readString(); + if (parseDebug) + System.out.println("DEBUG IMAP: type " + type); + processedType = SINGLE; + subtype = r.readString(); + if (parseDebug) + System.out.println("DEBUG IMAP: subtype " + subtype); + + // SIMS 4.0 returns NIL for a Content-Type of "binary", fix it here + if (type == null) { + type = "application"; + subtype = "octet-stream"; + } + cParams = parseParameters(r); + if (parseDebug) + System.out.println("DEBUG IMAP: cParams " + cParams); + id = r.readString(); + if (parseDebug) + System.out.println("DEBUG IMAP: id " + id); + description = r.readString(); + if (parseDebug) + System.out.println("DEBUG IMAP: description " + description); + /* + * XXX - Work around bug in Exchange 2010 that + * returns unquoted string. + */ + encoding = r.readAtomString(); + if (encoding != null && encoding.equalsIgnoreCase("NIL")) { + if (parseDebug) + System.out.println("DEBUG IMAP: NIL encoding" + + ", applying Exchange bug workaround"); + encoding = null; + } + /* + * XXX - Work around bug in office365.com that returns + * a string with a trailing space in some cases. + */ + if (encoding != null) + encoding = encoding.trim(); + if (parseDebug) + System.out.println("DEBUG IMAP: encoding " + encoding); + size = r.readNumber(); + if (parseDebug) + System.out.println("DEBUG IMAP: size " + size); + if (size < 0) + throw new ParsingException( + "BODYSTRUCTURE parse error: bad ``size'' element"); + + // "text/*" & "message/rfc822" types have additional data .. + if (type.equalsIgnoreCase("text")) { + lines = r.readNumber(); + if (parseDebug) + System.out.println("DEBUG IMAP: lines " + lines); + if (lines < 0) + throw new ParsingException( + "BODYSTRUCTURE parse error: bad ``lines'' element"); + } else if (type.equalsIgnoreCase("message") && + subtype.equalsIgnoreCase("rfc822")) { + // Nested message + processedType = NESTED; + // The envelope comes next, but sadly Gmail handles nested + // messages just like simple body parts and fails to return + // the envelope and body structure of the message (sort of + // like IMAP4 before rev1). + r.skipSpaces(); + if (r.peekByte() == '(') { // the envelope follows + envelope = new ENVELOPE(r); + if (parseDebug) + System.out.println( + "DEBUG IMAP: got envelope of nested message"); + BODYSTRUCTURE[] bs = { new BODYSTRUCTURE(r) }; + bodies = bs; + lines = r.readNumber(); + if (parseDebug) + System.out.println("DEBUG IMAP: lines " + lines); + if (lines < 0) + throw new ParsingException( + "BODYSTRUCTURE parse error: bad ``lines'' element"); + } else { + if (parseDebug) + System.out.println("DEBUG IMAP: " + + "missing envelope and body of nested message"); + } + } else { + // Detect common error of including lines element on other types + r.skipSpaces(); + byte bn = r.peekByte(); + if (Character.isDigit((char)bn)) // number + throw new ParsingException( + "BODYSTRUCTURE parse error: server erroneously " + + "included ``lines'' element with type " + + type + "/" + subtype); + } + + if (r.isNextNonSpace(')')) { + if (parseDebug) + System.out.println("DEBUG IMAP: parse DONE"); + return; // done + } + + // Optional extension data + + // MD5 + md5 = r.readString(); + if (r.isNextNonSpace(')')) { + if (parseDebug) + System.out.println("DEBUG IMAP: no MD5 DONE"); + return; // done + } + + // Disposition + byte b = r.readByte(); + if (b == '(') { + disposition = r.readString(); + if (parseDebug) + System.out.println("DEBUG IMAP: disposition " + + disposition); + dParams = parseParameters(r); + if (parseDebug) + System.out.println("DEBUG IMAP: dParams " + dParams); + if (!r.isNextNonSpace(')')) // eat the end ')' + throw new ParsingException( + "BODYSTRUCTURE parse error: " + + "missing ``)'' at end of disposition"); + } else if (b == 'N' || b == 'n') { + if (parseDebug) + System.out.println("DEBUG IMAP: disposition NIL"); + r.skip(2); // skip 'NIL' + } else { + throw new ParsingException( + "BODYSTRUCTURE parse error: " + + type + "/" + subtype + ": " + + "bad single part disposition, b " + b); + } + + if (r.isNextNonSpace(')')) { + if (parseDebug) + System.out.println("DEBUG IMAP: disposition DONE"); + return; // done + } + + // Language + if (r.peekByte() == '(') { // a list follows + language = r.readStringList(); + if (parseDebug) + System.out.println("DEBUG IMAP: language len " + + language.length); + } else { // protocol is unnessarily complex here + String l = r.readString(); + if (l != null) { + String[] la = { l }; + language = la; + if (parseDebug) + System.out.println("DEBUG IMAP: language " + l); + } + } + + // RFC3501 defines an optional "body location" next, + // but for now we ignore it along with other extensions. + + // Throw away any further extension data + while (r.readByte() == ' ') + parseBodyExtension(r); + if (parseDebug) + System.out.println("DEBUG IMAP: all DONE"); + } + } + + public boolean isMulti() { + return processedType == MULTI; + } + + public boolean isSingle() { + return processedType == SINGLE; + } + + public boolean isNested() { + return processedType == NESTED; + } + + private ParameterList parseParameters(Response r) + throws ParsingException { + r.skipSpaces(); + + ParameterList list = null; + byte b = r.readByte(); + if (b == '(') { + list = new ParameterList(); + do { + String name = r.readString(); + if (parseDebug) + System.out.println("DEBUG IMAP: parameter name " + name); + if (name == null) + throw new ParsingException( + "BODYSTRUCTURE parse error: " + + type + "/" + subtype + ": " + + "null name in parameter list"); + String value = r.readString(); + if (parseDebug) + System.out.println("DEBUG IMAP: parameter value " + value); + if (value == null) { // work around buggy servers + if (parseDebug) + System.out.println("DEBUG IMAP: NIL parameter value" + + ", applying Exchange bug workaround"); + value = ""; + } + list.set(name, value); + } while (!r.isNextNonSpace(')')); + list.combineSegments(); + } else if (b == 'N' || b == 'n') { + if (parseDebug) + System.out.println("DEBUG IMAP: parameter list NIL"); + r.skip(2); + } else + throw new ParsingException("Parameter list parse error"); + + return list; + } + + private void parseBodyExtension(Response r) throws ParsingException { + r.skipSpaces(); + + byte b = r.peekByte(); + if (b == '(') { + r.skip(1); // skip '(' + do { + parseBodyExtension(r); + } while (!r.isNextNonSpace(')')); + } else if (Character.isDigit((char)b)) // number + r.readNumber(); + else // nstring + r.readString(); + } +}
diff --git a/mail/src/main/java/com/sun/mail/imap/protocol/ENVELOPE.java b/mail/src/main/java/com/sun/mail/imap/protocol/ENVELOPE.java new file mode 100644 index 0000000..eeafd14 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/imap/protocol/ENVELOPE.java
@@ -0,0 +1,224 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import java.util.List; +import java.util.ArrayList; +import java.util.Date; +import java.io.UnsupportedEncodingException; +import java.text.ParseException; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.AddressException; +import javax.mail.internet.MailDateFormat; +import javax.mail.internet.MimeUtility; +import com.sun.mail.iap.*; +import com.sun.mail.util.PropUtil; + +/** + * The ENEVELOPE item of an IMAP FETCH response. + * + * @author John Mani + * @author Bill Shannon + */ + +public class ENVELOPE implements Item { + + // IMAP item name + static final char[] name = {'E','N','V','E','L','O','P','E'}; + public int msgno; + + public Date date = null; + public String subject; + public InternetAddress[] from; + public InternetAddress[] sender; + public InternetAddress[] replyTo; + public InternetAddress[] to; + public InternetAddress[] cc; + public InternetAddress[] bcc; + public String inReplyTo; + public String messageId; + + // Used to parse dates + private static final MailDateFormat mailDateFormat = new MailDateFormat(); + + // special debugging output to debug parsing errors + private static final boolean parseDebug = + PropUtil.getBooleanSystemProperty("mail.imap.parse.debug", false); + + public ENVELOPE(FetchResponse r) throws ParsingException { + if (parseDebug) + System.out.println("parse ENVELOPE"); + msgno = r.getNumber(); + + r.skipSpaces(); + + if (r.readByte() != '(') + throw new ParsingException("ENVELOPE parse error"); + + String s = r.readString(); + if (s != null) { + try { + synchronized (mailDateFormat) { + date = mailDateFormat.parse(s); + } + } catch (ParseException pex) { + } + } + if (parseDebug) + System.out.println(" Date: " + date); + + subject = r.readString(); + if (parseDebug) + System.out.println(" Subject: " + subject); + if (parseDebug) + System.out.println(" From addresses:"); + from = parseAddressList(r); + if (parseDebug) + System.out.println(" Sender addresses:"); + sender = parseAddressList(r); + if (parseDebug) + System.out.println(" Reply-To addresses:"); + replyTo = parseAddressList(r); + if (parseDebug) + System.out.println(" To addresses:"); + to = parseAddressList(r); + if (parseDebug) + System.out.println(" Cc addresses:"); + cc = parseAddressList(r); + if (parseDebug) + System.out.println(" Bcc addresses:"); + bcc = parseAddressList(r); + inReplyTo = r.readString(); + if (parseDebug) + System.out.println(" In-Reply-To: " + inReplyTo); + messageId = r.readString(); + if (parseDebug) + System.out.println(" Message-ID: " + messageId); + + if (!r.isNextNonSpace(')')) + throw new ParsingException("ENVELOPE parse error"); + } + + private InternetAddress[] parseAddressList(Response r) + throws ParsingException { + r.skipSpaces(); // skip leading spaces + + byte b = r.readByte(); + if (b == '(') { + /* + * Some broken servers (e.g., Yahoo Mail) return an empty + * list instead of NIL. Handle that here even though it + * doesn't conform to the IMAP spec. + */ + if (r.isNextNonSpace(')')) + return null; + + List<InternetAddress> v = new ArrayList<>(); + + do { + IMAPAddress a = new IMAPAddress(r); + if (parseDebug) + System.out.println(" Address: " + a); + // if we see an end-of-group address at the top, ignore it + if (!a.isEndOfGroup()) + v.add(a); + } while (!r.isNextNonSpace(')')); + + return v.toArray(new InternetAddress[v.size()]); + } else if (b == 'N' || b == 'n') { // NIL + r.skip(2); // skip 'NIL' + return null; + } else + throw new ParsingException("ADDRESS parse error"); + } +} + +class IMAPAddress extends InternetAddress { + private boolean group = false; + private InternetAddress[] grouplist; + private String groupname; + + private static final long serialVersionUID = -3835822029483122232L; + + IMAPAddress(Response r) throws ParsingException { + r.skipSpaces(); // skip leading spaces + + if (r.readByte() != '(') + throw new ParsingException("ADDRESS parse error"); + + encodedPersonal = r.readString(); + + r.readString(); // throw away address_list + String mb = r.readString(); + String host = r.readString(); + // skip bogus spaces inserted by Yahoo IMAP server if + // "undisclosed-recipients" is a recipient + r.skipSpaces(); + if (!r.isNextNonSpace(')')) // skip past terminating ')' + throw new ParsingException("ADDRESS parse error"); + + if (host == null) { + // it's a group list, start or end + group = true; + groupname = mb; + if (groupname == null) // end of group list + return; + // Accumulate a group list. The members of the group + // are accumulated in a List and the corresponding string + // representation of the group is accumulated in a StringBuilder. + StringBuilder sb = new StringBuilder(); + sb.append(groupname).append(':'); + List<InternetAddress> v = new ArrayList<>(); + while (r.peekByte() != ')') { + IMAPAddress a = new IMAPAddress(r); + if (a.isEndOfGroup()) // reached end of group + break; + if (v.size() != 0) // if not first element, need a comma + sb.append(','); + sb.append(a.toString()); + v.add(a); + } + sb.append(';'); + address = sb.toString(); + grouplist = v.toArray(new IMAPAddress[v.size()]); + } else { + if (mb == null || mb.length() == 0) + address = host; + else if (host.length() == 0) + address = mb; + else + address = mb + "@" + host; + } + + } + + boolean isEndOfGroup() { + return group && groupname == null; + } + + @Override + public boolean isGroup() { + return group; + } + + @Override + public InternetAddress[] getGroup(boolean strict) throws AddressException { + if (grouplist == null) + return null; + return grouplist.clone(); + } +}
diff --git a/mail/src/main/java/com/sun/mail/imap/protocol/FLAGS.java b/mail/src/main/java/com/sun/mail/imap/protocol/FLAGS.java new file mode 100644 index 0000000..5621aa8 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/imap/protocol/FLAGS.java
@@ -0,0 +1,86 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import javax.mail.Flags; +import com.sun.mail.iap.*; + +/** + * This class + * + * @author John Mani + */ + +public class FLAGS extends Flags implements Item { + + // IMAP item name + static final char[] name = {'F','L','A','G','S'}; + public int msgno; + + private static final long serialVersionUID = 439049847053756670L; + + /** + * Constructor. + * + * @param r the IMAPResponse + * @exception ParsingException for parsing failures + */ + public FLAGS(IMAPResponse r) throws ParsingException { + msgno = r.getNumber(); + + r.skipSpaces(); + String[] flags = r.readSimpleList(); + if (flags != null) { // if not empty flaglist + for (int i = 0; i < flags.length; i++) { + String s = flags[i]; + if (s.length() >= 2 && s.charAt(0) == '\\') { + switch (Character.toUpperCase(s.charAt(1))) { + case 'S': // \Seen + add(Flags.Flag.SEEN); + break; + case 'R': // \Recent + add(Flags.Flag.RECENT); + break; + case 'D': + if (s.length() >= 3) { + char c = s.charAt(2); + if (c == 'e' || c == 'E') // \Deleted + add(Flags.Flag.DELETED); + else if (c == 'r' || c == 'R') // \Draft + add(Flags.Flag.DRAFT); + } else + add(s); // unknown, treat it as a user flag + break; + case 'A': // \Answered + add(Flags.Flag.ANSWERED); + break; + case 'F': // \Flagged + add(Flags.Flag.FLAGGED); + break; + case '*': // \* + add(Flags.Flag.USER); + break; + default: + add(s); // unknown, treat it as a user flag + break; + } + } else + add(s); + } + } + } +}
diff --git a/mail/src/main/java/com/sun/mail/imap/protocol/FetchItem.java b/mail/src/main/java/com/sun/mail/imap/protocol/FetchItem.java new file mode 100644 index 0000000..1eaae6c --- /dev/null +++ b/mail/src/main/java/com/sun/mail/imap/protocol/FetchItem.java
@@ -0,0 +1,58 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import java.lang.reflect.*; + +import javax.mail.FetchProfile; +import com.sun.mail.iap.ParsingException; + +/** + * Metadata describing a FETCH item. + * Note that the "name" field MUST be in uppercase. <p> + * + * @author Bill Shannon + * @since JavaMail 1.4.6 + */ + +public abstract class FetchItem { + private String name; + private FetchProfile.Item fetchProfileItem; + + public FetchItem(String name, FetchProfile.Item fetchProfileItem) { + this.name = name; + this.fetchProfileItem = fetchProfileItem; + } + + public String getName() { + return name; + } + + public FetchProfile.Item getFetchProfileItem() { + return fetchProfileItem; + } + + /** + * Parse the item into some kind of object appropriate for the item. + * Note that the item name will have been parsed and skipped already. + * + * @param r the response + * @return the fetch item + * @exception ParsingException for parsing failures + */ + public abstract Object parseItem(FetchResponse r) throws ParsingException; +}
diff --git a/mail/src/main/java/com/sun/mail/imap/protocol/FetchResponse.java b/mail/src/main/java/com/sun/mail/imap/protocol/FetchResponse.java new file mode 100644 index 0000000..6477ba5 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/imap/protocol/FetchResponse.java
@@ -0,0 +1,320 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import java.io.*; +import java.util.*; +import com.sun.mail.util.ASCIIUtility; +import com.sun.mail.iap.*; + +/** + * This class represents a FETCH response obtained from the input stream + * of an IMAP server. + * + * @author John Mani + * @author Bill Shannon + */ + +public class FetchResponse extends IMAPResponse { + /* + * Regular Items are saved in the items array. + * Extension items (items handled by subclasses + * that extend the IMAP provider) are saved in the + * extensionItems map, indexed by the FETCH item name. + * The map is only created when needed. + * + * XXX - Should consider unifying the handling of + * regular items and extension items. + */ + private Item[] items; + private Map<String, Object> extensionItems; + private final FetchItem[] fitems; + + public FetchResponse(Protocol p) + throws IOException, ProtocolException { + super(p); + fitems = null; + parse(); + } + + public FetchResponse(IMAPResponse r) + throws IOException, ProtocolException { + this(r, null); + } + + /** + * Construct a FetchResponse that handles the additional FetchItems. + * + * @param r the IMAPResponse + * @param fitems the fetch items + * @exception IOException for I/O errors + * @exception ProtocolException for protocol failures + * @since JavaMail 1.4.6 + */ + public FetchResponse(IMAPResponse r, FetchItem[] fitems) + throws IOException, ProtocolException { + super(r); + this.fitems = fitems; + parse(); + } + + public int getItemCount() { + return items.length; + } + + public Item getItem(int index) { + return items[index]; + } + + public <T extends Item> T getItem(Class<T> c) { + for (int i = 0; i < items.length; i++) { + if (c.isInstance(items[i])) + return c.cast(items[i]); + } + + return null; + } + + /** + * Return the first fetch response item of the given class + * for the given message number. + * + * @param r the responses + * @param msgno the message number + * @param c the class + * @param <T> the type of fetch item + * @return the fetch item + */ + public static <T extends Item> T getItem(Response[] r, int msgno, + Class<T> c) { + if (r == null) + return null; + + for (int i = 0; i < r.length; i++) { + + if (r[i] == null || + !(r[i] instanceof FetchResponse) || + ((FetchResponse)r[i]).getNumber() != msgno) + continue; + + FetchResponse f = (FetchResponse)r[i]; + for (int j = 0; j < f.items.length; j++) { + if (c.isInstance(f.items[j])) + return c.cast(f.items[j]); + } + } + + return null; + } + + /** + * Return all fetch response items of the given class + * for the given message number. + * + * @param r the responses + * @param msgno the message number + * @param c the class + * @param <T> the type of fetch items + * @return the list of fetch items + * @since JavaMail 1.5.2 + */ + public static <T extends Item> List<T> getItems(Response[] r, int msgno, + Class<T> c) { + List<T> items = new ArrayList<>(); + + if (r == null) + return items; + + for (int i = 0; i < r.length; i++) { + + if (r[i] == null || + !(r[i] instanceof FetchResponse) || + ((FetchResponse)r[i]).getNumber() != msgno) + continue; + + FetchResponse f = (FetchResponse)r[i]; + for (int j = 0; j < f.items.length; j++) { + if (c.isInstance(f.items[j])) + items.add(c.cast(f.items[j])); + } + } + + return items; + } + + /** + * Return a map of the extension items found in this fetch response. + * The map is indexed by extension item name. Callers should not + * modify the map. + * + * @return Map of extension items, or null if none + * @since JavaMail 1.4.6 + */ + public Map<String, Object> getExtensionItems() { + return extensionItems; + } + + private final static char[] HEADER = {'.','H','E','A','D','E','R'}; + private final static char[] TEXT = {'.','T','E','X','T'}; + + private void parse() throws ParsingException { + if (!isNextNonSpace('(')) + throw new ParsingException( + "error in FETCH parsing, missing '(' at index " + index); + + List<Item> v = new ArrayList<>(); + Item i = null; + skipSpaces(); + do { + + if (index >= size) + throw new ParsingException( + "error in FETCH parsing, ran off end of buffer, size " + size); + + i = parseItem(); + if (i != null) + v.add(i); + else if (!parseExtensionItem()) + throw new ParsingException( + "error in FETCH parsing, unrecognized item at index " + + index + ", starts with \"" + next20() + "\""); + } while (!isNextNonSpace(')')); + + items = v.toArray(new Item[v.size()]); + } + + /** + * Return the next 20 characters in the buffer, for exception messages. + */ + private String next20() { + if (index + 20 > size) + return ASCIIUtility.toString(buffer, index, size); + else + return ASCIIUtility.toString(buffer, index, index + 20) + "..."; + } + + /** + * Parse the item at the current position in the buffer, + * skipping over the item if successful. Otherwise, return null + * and leave the buffer position unmodified. + */ + @SuppressWarnings("empty") + private Item parseItem() throws ParsingException { + switch (buffer[index]) { + case 'E': case 'e': + if (match(ENVELOPE.name)) + return new ENVELOPE(this); + break; + case 'F': case 'f': + if (match(FLAGS.name)) + return new FLAGS((IMAPResponse)this); + break; + case 'I': case 'i': + if (match(INTERNALDATE.name)) + return new INTERNALDATE(this); + break; + case 'B': case 'b': + if (match(BODYSTRUCTURE.name)) + return new BODYSTRUCTURE(this); + else if (match(BODY.name)) { + if (buffer[index] == '[') + return new BODY(this); + else + return new BODYSTRUCTURE(this); + } + break; + case 'R': case 'r': + if (match(RFC822SIZE.name)) + return new RFC822SIZE(this); + else if (match(RFC822DATA.name)) { + boolean isHeader = false; + if (match(HEADER)) + isHeader = true; // skip ".HEADER" + else if (match(TEXT)) + isHeader = false; // skip ".TEXT" + return new RFC822DATA(this, isHeader); + } + break; + case 'U': case 'u': + if (match(UID.name)) + return new UID(this); + break; + case 'M': case 'm': + if (match(MODSEQ.name)) + return new MODSEQ(this); + break; + default: + break; + } + return null; + } + + /** + * If this item is a known extension item, parse it. + */ + private boolean parseExtensionItem() throws ParsingException { + if (fitems == null) + return false; + for (int i = 0; i < fitems.length; i++) { + if (match(fitems[i].getName())) { + if (extensionItems == null) + extensionItems = new HashMap<>(); + extensionItems.put(fitems[i].getName(), + fitems[i].parseItem(this)); + return true; + } + } + return false; + } + + /** + * Does the current buffer match the given item name? + * itemName is the name of the IMAP item to compare against. + * NOTE that itemName *must* be all uppercase. + * If the match is successful, the buffer pointer (index) + * is incremented past the matched item. + */ + private boolean match(char[] itemName) { + int len = itemName.length; + for (int i = 0, j = index; i < len;) + // IMAP tokens are case-insensitive. We store itemNames in + // uppercase, so convert operand to uppercase before comparing. + if (Character.toUpperCase((char)buffer[j++]) != itemName[i++]) + return false; + index += len; + return true; + } + + /** + * Does the current buffer match the given item name? + * itemName is the name of the IMAP item to compare against. + * NOTE that itemName *must* be all uppercase. + * If the match is successful, the buffer pointer (index) + * is incremented past the matched item. + */ + private boolean match(String itemName) { + int len = itemName.length(); + for (int i = 0, j = index; i < len;) + // IMAP tokens are case-insensitive. We store itemNames in + // uppercase, so convert operand to uppercase before comparing. + if (Character.toUpperCase((char)buffer[j++]) != + itemName.charAt(i++)) + return false; + index += len; + return true; + } +}
diff --git a/mail/src/main/java/com/sun/mail/imap/protocol/ID.java b/mail/src/main/java/com/sun/mail/imap/protocol/ID.java new file mode 100644 index 0000000..66cb865 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/imap/protocol/ID.java
@@ -0,0 +1,97 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import java.util.*; +import com.sun.mail.iap.*; + +/** + * This class represents the response to the ID command. <p> + * + * See <A HREF="http://www.ietf.org/rfc/rfc2971.txt">RFC 2971</A>. + * + * @since JavaMail 1.5.1 + * @author Bill Shannon + */ + +public class ID { + + private Map<String, String> serverParams = null; + + /** + * Parse the server parameter list out of the response. + * + * @param r the response + * @exception ProtocolException for protocol failures + */ + public ID(Response r) throws ProtocolException { + // id_response ::= "ID" SPACE id_params_list + // id_params_list ::= "(" #(string SPACE nstring) ")" / nil + // ;; list of field value pairs + + r.skipSpaces(); + int c = r.peekByte(); + if (c == 'N' || c == 'n') // assume NIL + return; + + if (c != '(') + throw new ProtocolException("Missing '(' at start of ID"); + + serverParams = new HashMap<>(); + + String[] v = r.readStringList(); + if (v != null) { + for (int i = 0; i < v.length; i += 2) { + String name = v[i]; + if (name == null) + throw new ProtocolException("ID field name null"); + if (i + 1 >= v.length) + throw new ProtocolException("ID field without value: " + + name); + String value = v[i + 1]; + serverParams.put(name, value); + } + } + serverParams = Collections.unmodifiableMap(serverParams); + } + + /** + * Return the parsed server params. + */ + Map<String, String> getServerParams() { + return serverParams; + } + + /** + * Convert the client parameters into an argument list for the ID command. + */ + static Argument getArgumentList(Map<String,String> clientParams) { + Argument arg = new Argument(); + if (clientParams == null) { + arg.writeAtom("NIL"); + return arg; + } + Argument list = new Argument(); + // add params to list + for (Map.Entry<String, String> e : clientParams.entrySet()) { + list.writeNString(e.getKey()); // assume these are ASCII only + list.writeNString(e.getValue()); + } + arg.writeArgument(list); + return arg; + } +}
diff --git a/mail/src/main/java/com/sun/mail/imap/protocol/IMAPProtocol.java b/mail/src/main/java/com/sun/mail/imap/protocol/IMAPProtocol.java new file mode 100644 index 0000000..acd44c8 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/imap/protocol/IMAPProtocol.java
@@ -0,0 +1,3292 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import java.io.*; +import java.util.*; +import java.text.*; +import java.lang.reflect.*; +import java.util.logging.Level; +import java.nio.charset.StandardCharsets; + +import javax.mail.*; +import javax.mail.internet.*; +import javax.mail.search.*; + +import com.sun.mail.util.PropUtil; +import com.sun.mail.util.MailLogger; +import com.sun.mail.util.ASCIIUtility; +import com.sun.mail.util.BASE64EncoderStream; +import com.sun.mail.iap.*; +import com.sun.mail.auth.Ntlm; + +import com.sun.mail.imap.ACL; +import com.sun.mail.imap.Rights; +import com.sun.mail.imap.AppendUID; +import com.sun.mail.imap.CopyUID; +import com.sun.mail.imap.SortTerm; +import com.sun.mail.imap.ResyncData; +import com.sun.mail.imap.Utility; + +/** + * This class extends the iap.Protocol object and implements IMAP + * semantics. In general, there is a method corresponding to each + * IMAP protocol command. The typical implementation issues the + * appropriate protocol command, collects all responses, processes + * those responses that are specific to this command and then + * dispatches the rest (the unsolicited ones) to the dispatcher + * using the <code>notifyResponseHandlers(r)</code>. + * + * @author John Mani + * @author Bill Shannon + */ + +public class IMAPProtocol extends Protocol { + + private boolean connected = false; // did constructor succeed? + private boolean rev1 = false; // REV1 server ? + private boolean referralException; // throw exception for IMAP REFERRAL? + private boolean noauthdebug = true; // hide auth info in debug output + private boolean authenticated; // authenticated? + // WARNING: authenticated may be set to true in superclass + // constructor, don't initialize it here. + + private Map<String, String> capabilities; + // WARNING: capabilities may be initialized as a result of superclass + // constructor, don't initialize it here. + private List<String> authmechs; + // WARNING: authmechs may be initialized as a result of superclass + // constructor, don't initialize it here. + private boolean utf8; // UTF-8 support enabled? + + protected SearchSequence searchSequence; + protected String[] searchCharsets; // array of search charsets + + protected Set<String> enabled; // enabled capabilities - RFC 5161 + + private String name; + private SaslAuthenticator saslAuthenticator; // if SASL is being used + private String proxyAuthUser; // user name used with PROXYAUTH + + private ByteArray ba; // a buffer for fetchBody + + private static final byte[] CRLF = { (byte)'\r', (byte)'\n'}; + + private static final FetchItem[] fetchItems = { }; + + /** + * Constructor. + * Opens a connection to the given host at given port. + * + * @param name the protocol name + * @param host host to connect to + * @param port port number to connect to + * @param props Properties object used by this protocol + * @param isSSL true if SSL should be used + * @param logger the MailLogger to use for debug output + * @exception IOException for I/O errors + * @exception ProtocolException for protocol failures + */ + public IMAPProtocol(String name, String host, int port, + Properties props, boolean isSSL, MailLogger logger) + throws IOException, ProtocolException { + super(host, port, props, "mail." + name, isSSL, logger); + + try { + this.name = name; + noauthdebug = + !PropUtil.getBooleanProperty(props, "mail.debug.auth", false); + + // in case it was not initialized in processGreeting + referralException = PropUtil.getBooleanProperty(props, + prefix + ".referralexception", false); + + if (capabilities == null) + capability(); + + if (hasCapability("IMAP4rev1")) + rev1 = true; + + searchCharsets = new String[2]; // 2, for now. + searchCharsets[0] = "UTF-8"; + searchCharsets[1] = MimeUtility.mimeCharset( + MimeUtility.getDefaultJavaCharset() + ); + + connected = true; // must be last statement in constructor + } finally { + /* + * If we get here because an exception was thrown, we need + * to disconnect to avoid leaving a connected socket that + * no one will be able to use because this object was never + * completely constructed. + */ + if (!connected) + disconnect(); + } + } + + /** + * Constructor for debugging. + * + * @param in the InputStream from which to read + * @param out the PrintStream to which to write + * @param props Properties object used by this protocol + * @param debug true to enable debugging output + * @exception IOException for I/O errors + */ + public IMAPProtocol(InputStream in, PrintStream out, + Properties props, boolean debug) + throws IOException { + super(in, out, props, debug); + + this.name = "imap"; + noauthdebug = + !PropUtil.getBooleanProperty(props, "mail.debug.auth", false); + + if (capabilities == null) + capabilities = new HashMap<>(); + + searchCharsets = new String[2]; // 2, for now. + searchCharsets[0] = "UTF-8"; + searchCharsets[1] = MimeUtility.mimeCharset( + MimeUtility.getDefaultJavaCharset() + ); + + connected = true; // must be last statement in constructor + } + + /** + * Return an array of FetchItem objects describing the + * FETCH items supported by this protocol. Subclasses may + * override this method to combine their FetchItems with + * the FetchItems returned by the superclass. + * + * @return an array of FetchItem objects + * @since JavaMail 1.4.6 + */ + public FetchItem[] getFetchItems() { + return fetchItems; + } + + /** + * CAPABILITY command. + * + * @exception ProtocolException for protocol failures + * @see "RFC2060, section 6.1.1" + */ + public void capability() throws ProtocolException { + // Check CAPABILITY + Response[] r = command("CAPABILITY", null); + Response response = r[r.length-1]; + + if (response.isOK()) + handleCapabilityResponse(r); + handleResult(response); + } + + /** + * Handle any untagged CAPABILITY response in the Response array. + * + * @param r the responses + */ + public void handleCapabilityResponse(Response[] r) { + boolean first = true; + for (int i = 0, len = r.length; i < len; i++) { + if (!(r[i] instanceof IMAPResponse)) + continue; + + IMAPResponse ir = (IMAPResponse)r[i]; + + // Handle *all* untagged CAPABILITY responses. + // Though the spec seemingly states that only + // one CAPABILITY response string is allowed (6.1.1), + // some server vendors claim otherwise. + if (ir.keyEquals("CAPABILITY")) { + if (first) { + // clear out current when first response seen + capabilities = new HashMap<>(10); + authmechs = new ArrayList<>(5); + first = false; + } + parseCapabilities(ir); + } + } + } + + /** + * If the response contains a CAPABILITY response code, extract + * it and save the capabilities. + * + * @param r the response + */ + protected void setCapabilities(Response r) { + byte b; + while ((b = r.readByte()) > 0 && b != (byte)'[') + ; + if (b == 0) + return; + String s; + s = r.readAtom(); + if (!s.equalsIgnoreCase("CAPABILITY")) + return; + capabilities = new HashMap<>(10); + authmechs = new ArrayList<>(5); + parseCapabilities(r); + } + + /** + * Parse the capabilities from a CAPABILITY response or from + * a CAPABILITY response code attached to (e.g.) an OK response. + * + * @param r the CAPABILITY response + */ + protected void parseCapabilities(Response r) { + String s; + while ((s = r.readAtom()) != null) { + if (s.length() == 0) { + if (r.peekByte() == (byte)']') + break; + /* + * Probably found something here that's not an atom. + * Rather than loop forever or fail completely, we'll + * try to skip this bogus capability. This is known + * to happen with: + * Netscape Messaging Server 4.03 (built Apr 27 1999) + * that returns: + * * CAPABILITY * CAPABILITY IMAP4 IMAP4rev1 ... + * The "*" in the middle of the capability list causes + * us to loop forever here. + */ + r.skipToken(); + } else { + capabilities.put(s.toUpperCase(Locale.ENGLISH), s); + if (s.regionMatches(true, 0, "AUTH=", 0, 5)) { + authmechs.add(s.substring(5)); + if (logger.isLoggable(Level.FINE)) + logger.fine("AUTH: " + s.substring(5)); + } + } + } + } + + /** + * Check the greeting when first connecting; look for PREAUTH response. + * + * @param r the greeting response + * @exception ProtocolException for protocol failures + */ + @Override + protected void processGreeting(Response r) throws ProtocolException { + if (r.isBYE()) { + checkReferral(r); // may throw exception + throw new ConnectionException(this, r); + } + if (r.isOK()) { // check if it's OK + // XXX - is a REFERRAL response code really allowed here? + // XXX - referralException hasn't been initialized in c'tor yet + referralException = PropUtil.getBooleanProperty(props, + prefix + ".referralexception", false); + if (referralException) + checkReferral(r); + setCapabilities(r); + return; + } + // only other choice is PREAUTH + assert r instanceof IMAPResponse; + IMAPResponse ir = (IMAPResponse)r; + if (ir.keyEquals("PREAUTH")) { + authenticated = true; + setCapabilities(r); + } else { + disconnect(); + throw new ConnectionException(this, r); + } + } + + /** + * Check for an IMAP login REFERRAL response code. + * + * @exception IMAPReferralException if REFERRAL response code found + * @see "RFC 2221" + */ + private void checkReferral(Response r) throws IMAPReferralException { + String s = r.getRest(); // get the text after the response + if (s.startsWith("[")) { // a response code + int i = s.indexOf(' '); + if (i > 0 && s.substring(1, i).equalsIgnoreCase("REFERRAL")) { + String url, msg; + int j = s.indexOf(']'); + if (j > 0) { // should always be true; + url = s.substring(i + 1, j); + msg = s.substring(j + 1).trim(); + } else { + url = s.substring(i + 1); + msg = ""; + } + if (r.isBYE()) + disconnect(); + throw new IMAPReferralException(msg, url); + } + } + } + + /** + * Returns <code>true</code> if the connection has been authenticated, + * either due to a successful login, or due to a PREAUTH greeting response. + * + * @return true if the connection has been authenticated + */ + public boolean isAuthenticated() { + return authenticated; + } + + /** + * Returns <code>true</code> if this is an IMAP4rev1 server + * + * @return true if this is an IMAP4rev1 server + */ + public boolean isREV1() { + return rev1; + } + + /** + * Returns whether this Protocol supports non-synchronizing literals. + * + * @return true if non-synchronizing literals are supported + */ + @Override + protected boolean supportsNonSyncLiterals() { + return hasCapability("LITERAL+"); + } + + /** + * Read a response from the server. + * + * @return the response + * @exception IOException for I/O errors + * @exception ProtocolException for protocol failures + */ + @Override + public Response readResponse() throws IOException, ProtocolException { + // assert Thread.holdsLock(this); + // can't assert because it's called from constructor + IMAPResponse r = new IMAPResponse(this); + if (r.keyEquals("FETCH")) + r = new FetchResponse(r, getFetchItems()); + return r; + } + + /** + * Check whether the given capability is supported by + * this server. Returns <code>true</code> if so, otherwise + * returns false. + * + * @param c the capability name + * @return true if the server has the capability + */ + public boolean hasCapability(String c) { + if (c.endsWith("*")) { + c = c.substring(0, c.length() - 1).toUpperCase(Locale.ENGLISH); + Iterator<String> it = capabilities.keySet().iterator(); + while (it.hasNext()) { + if (it.next().startsWith(c)) + return true; + } + return false; + } + return capabilities.containsKey(c.toUpperCase(Locale.ENGLISH)); + } + + /** + * Return the map of capabilities returned by the server. + * + * @return the Map of capabilities + * @since JavaMail 1.4.1 + */ + public Map<String, String> getCapabilities() { + return capabilities; + } + + /** + * Does the server support UTF-8? + * + * @since JavaMail 1.6.0 + */ + public boolean supportsUtf8() { + return utf8; + } + + /** + * Close socket connection. + * + * This method just makes the Protocol.disconnect() method + * public. + */ + @Override + public void disconnect() { + super.disconnect(); + authenticated = false; // just in case + } + + /** + * The NOOP command. + * + * @exception ProtocolException for protocol failures + * @see "RFC2060, section 6.1.2" + */ + public void noop() throws ProtocolException { + logger.fine("IMAPProtocol noop"); + simpleCommand("NOOP", null); + } + + /** + * LOGOUT Command. + * + * @exception ProtocolException for protocol failures + * @see "RFC2060, section 6.1.3" + */ + public void logout() throws ProtocolException { + try { + Response[] r = command("LOGOUT", null); + + authenticated = false; + // dispatch any unsolicited responses. + // NOTE that the BYE response is dispatched here as well + notifyResponseHandlers(r); + } finally { + disconnect(); + } + } + + /** + * LOGIN Command. + * + * @param u the username + * @param p the password + * @throws ProtocolException as thrown by {@link Protocol#handleResult}. + * @see "RFC2060, section 6.2.2" + */ + public void login(String u, String p) throws ProtocolException { + Argument args = new Argument(); + args.writeString(u); + args.writeString(p); + + Response[] r = null; + try { + if (noauthdebug && isTracing()) { + logger.fine("LOGIN command trace suppressed"); + suspendTracing(); + } + r = command("LOGIN", args); + } finally { + resumeTracing(); + } + + // handle an illegal but not uncommon untagged CAPABILTY response + handleCapabilityResponse(r); + + // dispatch untagged responses + notifyResponseHandlers(r); + + // Handle result of this command + if (noauthdebug && isTracing()) + logger.fine("LOGIN command result: " + r[r.length-1]); + handleLoginResult(r[r.length-1]); + // If the response includes a CAPABILITY response code, process it + setCapabilities(r[r.length-1]); + // if we get this far without an exception, we're authenticated + authenticated = true; + } + + /** + * The AUTHENTICATE command with AUTH=LOGIN authenticate scheme + * + * @param u the username + * @param p the password + * @throws ProtocolException as thrown by {@link Protocol#handleResult}. + * @see "RFC2060, section 6.2.1" + */ + public synchronized void authlogin(String u, String p) + throws ProtocolException { + List<Response> v = new ArrayList<>(); + String tag = null; + Response r = null; + boolean done = false; + + try { + + if (noauthdebug && isTracing()) { + logger.fine("AUTHENTICATE LOGIN command trace suppressed"); + suspendTracing(); + } + + try { + tag = writeCommand("AUTHENTICATE LOGIN", null); + } catch (Exception ex) { + // Convert this into a BYE response + r = Response.byeResponse(ex); + done = true; + } + + OutputStream os = getOutputStream(); // stream to IMAP server + + /* Wrap a BASE64Encoder around a ByteArrayOutputstream + * to craft b64 encoded username and password strings + * + * Note that the encoded bytes should be sent "as-is" to the + * server, *not* as literals or quoted-strings. + * + * Also note that unlike the B64 definition in MIME, CRLFs + * should *not* be inserted during the encoding process. So, I + * use Integer.MAX_VALUE (0x7fffffff (> 1G)) as the bytesPerLine, + * which should be sufficiently large ! + * + * Finally, format the line in a buffer so it can be sent as + * a single packet, to avoid triggering a bug in SUN's SIMS 2.0 + * server caused by patch 105346. + */ + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + OutputStream b64os = new BASE64EncoderStream(bos, Integer.MAX_VALUE); + boolean first = true; + + while (!done) { // loop till we are done + try { + r = readResponse(); + if (r.isContinuation()) { + // Server challenge .. + String s; + if (first) { // Send encoded username + s = u; + first = false; + } else // Send encoded password + s = p; + + // obtain b64 encoded bytes + b64os.write(s.getBytes(StandardCharsets.UTF_8)); + b64os.flush(); // complete the encoding + + bos.write(CRLF); // CRLF termination + os.write(bos.toByteArray()); // write out line + os.flush(); // flush the stream + bos.reset(); // reset buffer + } else if (r.isTagged() && r.getTag().equals(tag)) + // Ah, our tagged response + done = true; + else if (r.isBYE()) // outta here + done = true; + // hmm .. unsolicited response here ?! + } catch (Exception ioex) { + // convert this into a BYE response + r = Response.byeResponse(ioex); + done = true; + } + v.add(r); + } + + } finally { + resumeTracing(); + } + + Response[] responses = v.toArray(new Response[v.size()]); + + // handle an illegal but not uncommon untagged CAPABILTY response + handleCapabilityResponse(responses); + + /* + * Dispatch untagged responses. + * NOTE: in our current upper level IMAP classes, we add the + * responseHandler to the Protocol object only *after* the + * connection has been authenticated. So, for now, the below + * code really ends up being just a no-op. + */ + notifyResponseHandlers(responses); + + // Handle the final OK, NO, BAD or BYE response + if (noauthdebug && isTracing()) + logger.fine("AUTHENTICATE LOGIN command result: " + r); + handleLoginResult(r); + // If the response includes a CAPABILITY response code, process it + setCapabilities(r); + // if we get this far without an exception, we're authenticated + authenticated = true; + } + + + /** + * The AUTHENTICATE command with AUTH=PLAIN authentication scheme. + * This is based heavly on the {@link #authlogin} method. + * + * @param authzid the authorization id + * @param u the username + * @param p the password + * @throws ProtocolException as thrown by {@link Protocol#handleResult}. + * @see "RFC3501, section 6.2.2" + * @see "RFC2595, section 6" + * @since JavaMail 1.3.2 + */ + public synchronized void authplain(String authzid, String u, String p) + throws ProtocolException { + List<Response> v = new ArrayList<>(); + String tag = null; + Response r = null; + boolean done = false; + + try { + + if (noauthdebug && isTracing()) { + logger.fine("AUTHENTICATE PLAIN command trace suppressed"); + suspendTracing(); + } + + try { + tag = writeCommand("AUTHENTICATE PLAIN", null); + } catch (Exception ex) { + // Convert this into a BYE response + r = Response.byeResponse(ex); + done = true; + } + + OutputStream os = getOutputStream(); // stream to IMAP server + + /* Wrap a BASE64Encoder around a ByteArrayOutputstream + * to craft b64 encoded username and password strings + * + * Note that the encoded bytes should be sent "as-is" to the + * server, *not* as literals or quoted-strings. + * + * Also note that unlike the B64 definition in MIME, CRLFs + * should *not* be inserted during the encoding process. So, I + * use Integer.MAX_VALUE (0x7fffffff (> 1G)) as the bytesPerLine, + * which should be sufficiently large ! + * + * Finally, format the line in a buffer so it can be sent as + * a single packet, to avoid triggering a bug in SUN's SIMS 2.0 + * server caused by patch 105346. + */ + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + OutputStream b64os = new BASE64EncoderStream(bos, Integer.MAX_VALUE); + + while (!done) { // loop till we are done + try { + r = readResponse(); + if (r.isContinuation()) { + // Server challenge .. + final String nullByte = "\0"; + String s = (authzid == null ? "" : authzid) + + nullByte + u + nullByte + p; + + // obtain b64 encoded bytes + b64os.write(s.getBytes(StandardCharsets.UTF_8)); + b64os.flush(); // complete the encoding + + bos.write(CRLF); // CRLF termination + os.write(bos.toByteArray()); // write out line + os.flush(); // flush the stream + bos.reset(); // reset buffer + } else if (r.isTagged() && r.getTag().equals(tag)) + // Ah, our tagged response + done = true; + else if (r.isBYE()) // outta here + done = true; + // hmm .. unsolicited response here ?! + } catch (Exception ioex) { + // convert this into a BYE response + r = Response.byeResponse(ioex); + done = true; + } + v.add(r); + } + + } finally { + resumeTracing(); + } + + Response[] responses = v.toArray(new Response[v.size()]); + + // handle an illegal but not uncommon untagged CAPABILTY response + handleCapabilityResponse(responses); + + /* + * Dispatch untagged responses. + * NOTE: in our current upper level IMAP classes, we add the + * responseHandler to the Protocol object only *after* the + * connection has been authenticated. So, for now, the below + * code really ends up being just a no-op. + */ + notifyResponseHandlers(responses); + + // Handle the final OK, NO, BAD or BYE response + if (noauthdebug && isTracing()) + logger.fine("AUTHENTICATE PLAIN command result: " + r); + handleLoginResult(r); + // If the response includes a CAPABILITY response code, process it + setCapabilities(r); + // if we get this far without an exception, we're authenticated + authenticated = true; + } + + /** + * The AUTHENTICATE command with AUTH=NTLM authentication scheme. + * This is based heavly on the {@link #authlogin} method. + * + * @param authzid the authorization id + * @param u the username + * @param p the password + * @throws ProtocolException as thrown by {@link Protocol#handleResult}. + * @see "RFC3501, section 6.2.2" + * @see "RFC2595, section 6" + * @since JavaMail 1.4.3 + */ + public synchronized void authntlm(String authzid, String u, String p) + throws ProtocolException { + List<Response> v = new ArrayList<>(); + String tag = null; + Response r = null; + boolean done = false; + + String type1Msg = null; + int flags = PropUtil.getIntProperty(props, + "mail." + name + ".auth.ntlm.flags", 0); + String domain = props.getProperty( + "mail." + name + ".auth.ntlm.domain", ""); + Ntlm ntlm = new Ntlm(domain, getLocalHost(), u, p, logger); + + try { + + if (noauthdebug && isTracing()) { + logger.fine("AUTHENTICATE NTLM command trace suppressed"); + suspendTracing(); + } + + try { + tag = writeCommand("AUTHENTICATE NTLM", null); + } catch (Exception ex) { + // Convert this into a BYE response + r = Response.byeResponse(ex); + done = true; + } + + OutputStream os = getOutputStream(); // stream to IMAP server + boolean first = true; + + while (!done) { // loop till we are done + try { + r = readResponse(); + if (r.isContinuation()) { + // Server challenge .. + String s; + if (first) { + s = ntlm.generateType1Msg(flags); + first = false; + } else { + s = ntlm.generateType3Msg(r.getRest()); + } + + os.write(s.getBytes(StandardCharsets.UTF_8)); + os.write(CRLF); // CRLF termination + os.flush(); // flush the stream + } else if (r.isTagged() && r.getTag().equals(tag)) + // Ah, our tagged response + done = true; + else if (r.isBYE()) // outta here + done = true; + // hmm .. unsolicited response here ?! + } catch (Exception ioex) { + // convert this into a BYE response + r = Response.byeResponse(ioex); + done = true; + } + v.add(r); + } + + } finally { + resumeTracing(); + } + + Response[] responses = v.toArray(new Response[v.size()]); + + // handle an illegal but not uncommon untagged CAPABILTY response + handleCapabilityResponse(responses); + + /* + * Dispatch untagged responses. + * NOTE: in our current upper level IMAP classes, we add the + * responseHandler to the Protocol object only *after* the + * connection has been authenticated. So, for now, the below + * code really ends up being just a no-op. + */ + notifyResponseHandlers(responses); + + // Handle the final OK, NO, BAD or BYE response + if (noauthdebug && isTracing()) + logger.fine("AUTHENTICATE NTLM command result: " + r); + handleLoginResult(r); + // If the response includes a CAPABILITY response code, process it + setCapabilities(r); + // if we get this far without an exception, we're authenticated + authenticated = true; + } + + /** + * The AUTHENTICATE command with AUTH=XOAUTH2 authentication scheme. + * This is based heavly on the {@link #authlogin} method. + * + * @param u the username + * @param p the password + * @throws ProtocolException as thrown by {@link Protocol#handleResult}. + * @see "RFC3501, section 6.2.2" + * @see "RFC2595, section 6" + * @since JavaMail 1.5.5 + */ + public synchronized void authoauth2(String u, String p) + throws ProtocolException { + List<Response> v = new ArrayList<>(); + String tag = null; + Response r = null; + boolean done = false; + + try { + + if (noauthdebug && isTracing()) { + logger.fine("AUTHENTICATE XOAUTH2 command trace suppressed"); + suspendTracing(); + } + + try { + Argument args = new Argument(); + args.writeAtom("XOAUTH2"); + if (hasCapability("SASL-IR")) { + String resp = "user=" + u + "\001auth=Bearer " + p + "\001\001"; + byte[] ba = BASE64EncoderStream.encode( + resp.getBytes(StandardCharsets.UTF_8)); + String irs = ASCIIUtility.toString(ba, 0, ba.length); + args.writeAtom(irs); + } + tag = writeCommand("AUTHENTICATE", args); + } catch (Exception ex) { + // Convert this into a BYE response + r = Response.byeResponse(ex); + done = true; + } + + OutputStream os = getOutputStream(); // stream to IMAP server + + while (!done) { // loop till we are done + try { + r = readResponse(); + if (r.isContinuation()) { + // Server challenge .. + String resp = "user=" + u + "\001auth=Bearer " + + p + "\001\001"; + byte[] b = BASE64EncoderStream.encode( + resp.getBytes(StandardCharsets.UTF_8)); + os.write(b); // write out response + os.write(CRLF); // CRLF termination + os.flush(); // flush the stream + } else if (r.isTagged() && r.getTag().equals(tag)) + // Ah, our tagged response + done = true; + else if (r.isBYE()) // outta here + done = true; + // hmm .. unsolicited response here ?! + } catch (Exception ioex) { + // convert this into a BYE response + r = Response.byeResponse(ioex); + done = true; + } + v.add(r); + } + + } finally { + resumeTracing(); + } + + Response[] responses = v.toArray(new Response[v.size()]); + + // handle an illegal but not uncommon untagged CAPABILTY response + handleCapabilityResponse(responses); + + /* + * Dispatch untagged responses. + * NOTE: in our current upper level IMAP classes, we add the + * responseHandler to the Protocol object only *after* the + * connection has been authenticated. So, for now, the below + * code really ends up being just a no-op. + */ + notifyResponseHandlers(responses); + + // Handle the final OK, NO, BAD or BYE response + if (noauthdebug && isTracing()) + logger.fine("AUTHENTICATE XOAUTH2 command result: " + r); + handleLoginResult(r); + // If the response includes a CAPABILITY response code, process it + setCapabilities(r); + // if we get this far without an exception, we're authenticated + authenticated = true; + } + + /** + * SASL-based login. + * + * @param allowed the SASL mechanisms we're allowed to use + * @param realm the SASL realm + * @param authzid the authorization id + * @param u the username + * @param p the password + * @exception ProtocolException for protocol failures + */ + public void sasllogin(String[] allowed, String realm, String authzid, + String u, String p) throws ProtocolException { + boolean useCanonicalHostName = PropUtil.getBooleanProperty(props, + "mail." + name + ".sasl.usecanonicalhostname", false); + String serviceHost; + if (useCanonicalHostName) + serviceHost = getInetAddress().getCanonicalHostName(); + else + serviceHost = host; + if (saslAuthenticator == null) { + try { + Class<?> sac = Class.forName( + "com.sun.mail.imap.protocol.IMAPSaslAuthenticator"); + Constructor<?> c = sac.getConstructor(new Class<?>[] { + IMAPProtocol.class, + String.class, + Properties.class, + MailLogger.class, + String.class + }); + saslAuthenticator = (SaslAuthenticator)c.newInstance( + new Object[] { + this, + name, + props, + logger, + serviceHost + }); + } catch (Exception ex) { + logger.log(Level.FINE, "Can't load SASL authenticator", ex); + // probably because we're running on a system without SASL + return; // not authenticated, try without SASL + } + } + + // were any allowed mechanisms specified? + List<String> v; + if (allowed != null && allowed.length > 0) { + // remove anything not supported by the server + v = new ArrayList<>(allowed.length); + for (int i = 0; i < allowed.length; i++) + if (authmechs.contains(allowed[i])) // XXX - case must match + v.add(allowed[i]); + } else { + // everything is allowed + v = authmechs; + } + String[] mechs = v.toArray(new String[v.size()]); + + try { + + if (noauthdebug && isTracing()) { + logger.fine("SASL authentication command trace suppressed"); + suspendTracing(); + } + + if (saslAuthenticator.authenticate(mechs, realm, authzid, u, p)) { + if (noauthdebug && isTracing()) + logger.fine("SASL authentication succeeded"); + authenticated = true; + } else { + if (noauthdebug && isTracing()) + logger.fine("SASL authentication failed"); + } + } finally { + resumeTracing(); + } + } + + // XXX - for IMAPSaslAuthenticator access to protected method + OutputStream getIMAPOutputStream() { + return getOutputStream(); + } + + /** + * Handle the result response for a LOGIN or AUTHENTICATE command. + * Look for IMAP login REFERRAL. + * + * @param r the response + * @exception ProtocolException for protocol failures + * @since JavaMail 1.5.5 + */ + protected void handleLoginResult(Response r) throws ProtocolException { + if (hasCapability("LOGIN-REFERRALS") && + (!r.isOK() || referralException)) + checkReferral(r); + handleResult(r); + } + + /** + * PROXYAUTH Command. + * + * @param u the PROXYAUTH user name + * @exception ProtocolException for protocol failures + * @see "Netscape/iPlanet/SunONE Messaging Server extension" + */ + public void proxyauth(String u) throws ProtocolException { + Argument args = new Argument(); + args.writeString(u); + + simpleCommand("PROXYAUTH", args); + proxyAuthUser = u; + } + + /** + * Get the user name used with the PROXYAUTH command. + * Returns null if PROXYAUTH was not used. + * + * @return the PROXYAUTH user name + * @since JavaMail 1.5.1 + */ + public String getProxyAuthUser() { + return proxyAuthUser; + } + + /** + * UNAUTHENTICATE Command. + * + * @exception ProtocolException for protocol failures + * @see "Netscape/iPlanet/SunONE Messaging Server extension" + * @since JavaMail 1.5.1 + */ + public void unauthenticate() throws ProtocolException { + if (!hasCapability("X-UNAUTHENTICATE")) + throw new BadCommandException("UNAUTHENTICATE not supported"); + simpleCommand("UNAUTHENTICATE", null); + authenticated = false; + } + + /** + * ID Command, for Yahoo! Mail IMAP server. + * + * @param guid the GUID + * @exception ProtocolException for protocol failures + * @deprecated As of JavaMail 1.5.1, replaced by + * {@link #id(Map) id(Map<String,String>)} + * @since JavaMail 1.4.4 + */ + @Deprecated + public void id(String guid) throws ProtocolException { + // support this for now, but remove it soon + Map<String,String> gmap = new HashMap<>(); + gmap.put("GUID", guid); + id(gmap); + } + + /** + * STARTTLS Command. + * + * @exception ProtocolException for protocol failures + * @see "RFC3501, section 6.2.1" + */ + public void startTLS() throws ProtocolException { + try { + super.startTLS("STARTTLS"); + } catch (ProtocolException pex) { + logger.log(Level.FINE, "STARTTLS ProtocolException", pex); + // ProtocolException just means the command wasn't recognized, + // or failed. This should never happen if we check the + // CAPABILITY first. + throw pex; + } catch (Exception ex) { + logger.log(Level.FINE, "STARTTLS Exception", ex); + // any other exception means we have to shut down the connection + // generate an artificial BYE response and disconnect + Response[] r = { Response.byeResponse(ex) }; + notifyResponseHandlers(r); + disconnect(); + throw new ProtocolException("STARTTLS failure", ex); + } + } + + /** + * COMPRESS Command. Only supports DEFLATE. + * + * @exception ProtocolException for protocol failures + * @see "RFC 4978" + */ + public void compress() throws ProtocolException { + try { + super.startCompression("COMPRESS DEFLATE"); + } catch (ProtocolException pex) { + logger.log(Level.FINE, "COMPRESS ProtocolException", pex); + // ProtocolException just means the command wasn't recognized, + // or failed. This should never happen if we check the + // CAPABILITY first. + throw pex; + } catch (Exception ex) { + logger.log(Level.FINE, "COMPRESS Exception", ex); + // any other exception means we have to shut down the connection + // generate an artificial BYE response and disconnect + Response[] r = { Response.byeResponse(ex) }; + notifyResponseHandlers(r); + disconnect(); + throw new ProtocolException("COMPRESS failure", ex); + } + } + + /** + * Encode a mailbox name appropriately depending on whether or not + * the server supports UTF-8, and add the encoded name to the + * Argument. + * + * @param args the arguments + * @param name the name to encode + * @since JavaMail 1.6.0 + */ + protected void writeMailboxName(Argument args, String name) { + if (utf8) + args.writeString(name, StandardCharsets.UTF_8); + else + // encode the mbox as per RFC2060 + args.writeString(BASE64MailboxEncoder.encode(name)); + } + + /** + * SELECT Command. + * + * @param mbox the mailbox name + * @return MailboxInfo if successful + * @exception ProtocolException for protocol failures + * @see "RFC2060, section 6.3.1" + */ + public MailboxInfo select(String mbox) throws ProtocolException { + return select(mbox, null); + } + + /** + * SELECT Command with QRESYNC data. + * + * @param mbox the mailbox name + * @param rd the ResyncData + * @return MailboxInfo if successful + * @exception ProtocolException for protocol failures + * @see "RFC2060, section 6.3.1" + * @see "RFC5162, section 3.1" + * @since JavaMail 1.5.1 + */ + public MailboxInfo select(String mbox, ResyncData rd) + throws ProtocolException { + Argument args = new Argument(); + writeMailboxName(args, mbox); + + if (rd != null) { + if (rd == ResyncData.CONDSTORE) { + if (!hasCapability("CONDSTORE")) + throw new BadCommandException("CONDSTORE not supported"); + args.writeArgument(new Argument().writeAtom("CONDSTORE")); + } else { + if (!hasCapability("QRESYNC")) + throw new BadCommandException("QRESYNC not supported"); + args.writeArgument(resyncArgs(rd)); + } + } + + Response[] r = command("SELECT", args); + + // Note that MailboxInfo also removes those responses + // it knows about + MailboxInfo minfo = new MailboxInfo(r); + + // dispatch any remaining untagged responses + notifyResponseHandlers(r); + + Response response = r[r.length-1]; + + if (response.isOK()) { // command succesful + if (response.toString().indexOf("READ-ONLY") != -1) + minfo.mode = Folder.READ_ONLY; + else + minfo.mode = Folder.READ_WRITE; + } + + handleResult(response); + return minfo; + } + + /** + * EXAMINE Command. + * + * @param mbox the mailbox name + * @return MailboxInfo if successful + * @exception ProtocolException for protocol failures + * @see "RFC2060, section 6.3.2" + */ + public MailboxInfo examine(String mbox) throws ProtocolException { + return examine(mbox, null); + } + + /** + * EXAMINE Command with QRESYNC data. + * + * @param mbox the mailbox name + * @param rd the ResyncData + * @return MailboxInfo if successful + * @exception ProtocolException for protocol failures + * @see "RFC2060, section 6.3.2" + * @see "RFC5162, section 3.1" + * @since JavaMail 1.5.1 + */ + public MailboxInfo examine(String mbox, ResyncData rd) + throws ProtocolException { + Argument args = new Argument(); + writeMailboxName(args, mbox); + + if (rd != null) { + if (rd == ResyncData.CONDSTORE) { + if (!hasCapability("CONDSTORE")) + throw new BadCommandException("CONDSTORE not supported"); + args.writeArgument(new Argument().writeAtom("CONDSTORE")); + } else { + if (!hasCapability("QRESYNC")) + throw new BadCommandException("QRESYNC not supported"); + args.writeArgument(resyncArgs(rd)); + } + } + + Response[] r = command("EXAMINE", args); + + // Note that MailboxInfo also removes those responses + // it knows about + MailboxInfo minfo = new MailboxInfo(r); + minfo.mode = Folder.READ_ONLY; // Obviously + + // dispatch any remaining untagged responses + notifyResponseHandlers(r); + + handleResult(r[r.length-1]); + return minfo; + } + + /** + * Generate a QRESYNC argument list based on the ResyncData. + */ + private static Argument resyncArgs(ResyncData rd) { + Argument cmd = new Argument(); + cmd.writeAtom("QRESYNC"); + Argument args = new Argument(); + args.writeNumber(rd.getUIDValidity()); + args.writeNumber(rd.getModSeq()); + UIDSet[] uids = Utility.getResyncUIDSet(rd); + if (uids != null) + args.writeString(UIDSet.toString(uids)); + cmd.writeArgument(args); + return cmd; + } + + /** + * ENABLE Command. + * + * @param cap the name of the capability to enable + * @exception ProtocolException for protocol failures + * @see "RFC 5161" + * @since JavaMail 1.5.1 + */ + public void enable(String cap) throws ProtocolException { + if (!hasCapability("ENABLE")) + throw new BadCommandException("ENABLE not supported"); + Argument args = new Argument(); + args.writeAtom(cap); + simpleCommand("ENABLE", args); + if (enabled == null) + enabled = new HashSet<>(); + enabled.add(cap.toUpperCase(Locale.ENGLISH)); + + // update the utf8 flag + utf8 = isEnabled("UTF8=ACCEPT"); + } + + /** + * Is the capability/extension enabled? + * + * @param cap the capability name + * @return true if enabled + * @see "RFC 5161" + * @since JavaMail 1.5.1 + */ + public boolean isEnabled(String cap) { + if (enabled == null) + return false; + else + return enabled.contains(cap.toUpperCase(Locale.ENGLISH)); + } + + /** + * UNSELECT Command. + * + * @exception ProtocolException for protocol failures + * @see "RFC 3691" + * @since JavaMail 1.4.4 + */ + public void unselect() throws ProtocolException { + if (!hasCapability("UNSELECT")) + throw new BadCommandException("UNSELECT not supported"); + simpleCommand("UNSELECT", null); + } + + /** + * STATUS Command. + * + * @param mbox the mailbox + * @param items the STATUS items to request + * @return STATUS results + * @exception ProtocolException for protocol failures + * @see "RFC2060, section 6.3.10" + */ + public Status status(String mbox, String[] items) + throws ProtocolException { + if (!isREV1() && !hasCapability("IMAP4SUNVERSION")) + // STATUS is rev1 only, however the non-rev1 SIMS2.0 + // does support this. + throw new BadCommandException("STATUS not supported"); + + Argument args = new Argument(); + writeMailboxName(args, mbox); + + Argument itemArgs = new Argument(); + if (items == null) + items = Status.standardItems; + + for (int i = 0, len = items.length; i < len; i++) + itemArgs.writeAtom(items[i]); + args.writeArgument(itemArgs); + + Response[] r = command("STATUS", args); + + Status status = null; + Response response = r[r.length-1]; + + // Grab all STATUS responses + if (response.isOK()) { // command succesful + for (int i = 0, len = r.length; i < len; i++) { + if (!(r[i] instanceof IMAPResponse)) + continue; + + IMAPResponse ir = (IMAPResponse)r[i]; + if (ir.keyEquals("STATUS")) { + if (status == null) + status = new Status(ir); + else // collect 'em all + Status.add(status, new Status(ir)); + r[i] = null; + } + } + } + + // dispatch remaining untagged responses + notifyResponseHandlers(r); + handleResult(response); + return status; + } + + /** + * CREATE Command. + * + * @param mbox the mailbox to create + * @exception ProtocolException for protocol failures + * @see "RFC2060, section 6.3.3" + */ + public void create(String mbox) throws ProtocolException { + Argument args = new Argument(); + writeMailboxName(args, mbox); + + simpleCommand("CREATE", args); + } + + /** + * DELETE Command. + * + * @param mbox the mailbox to delete + * @exception ProtocolException for protocol failures + * @see "RFC2060, section 6.3.4" + */ + public void delete(String mbox) throws ProtocolException { + Argument args = new Argument(); + writeMailboxName(args, mbox); + + simpleCommand("DELETE", args); + } + + /** + * RENAME Command. + * + * @param o old mailbox name + * @param n new mailbox name + * @exception ProtocolException for protocol failures + * @see "RFC2060, section 6.3.5" + */ + public void rename(String o, String n) throws ProtocolException { + Argument args = new Argument(); + writeMailboxName(args, o); + writeMailboxName(args, n); + + simpleCommand("RENAME", args); + } + + /** + * SUBSCRIBE Command. + * + * @param mbox the mailbox + * @exception ProtocolException for protocol failures + * @see "RFC2060, section 6.3.6" + */ + public void subscribe(String mbox) throws ProtocolException { + Argument args = new Argument(); + writeMailboxName(args, mbox); + + simpleCommand("SUBSCRIBE", args); + } + + /** + * UNSUBSCRIBE Command. + * + * @param mbox the mailbox + * @exception ProtocolException for protocol failures + * @see "RFC2060, section 6.3.7" + */ + public void unsubscribe(String mbox) throws ProtocolException { + Argument args = new Argument(); + writeMailboxName(args, mbox); + + simpleCommand("UNSUBSCRIBE", args); + } + + /** + * LIST Command. + * + * @param ref reference string + * @param pattern pattern to list + * @return LIST results + * @exception ProtocolException for protocol failures + * @see "RFC2060, section 6.3.8" + */ + public ListInfo[] list(String ref, String pattern) + throws ProtocolException { + return doList("LIST", ref, pattern); + } + + /** + * LSUB Command. + * + * @param ref reference string + * @param pattern pattern to list + * @return LSUB results + * @exception ProtocolException for protocol failures + * @see "RFC2060, section 6.3.9" + */ + public ListInfo[] lsub(String ref, String pattern) + throws ProtocolException { + return doList("LSUB", ref, pattern); + } + + /** + * Execute the specified LIST-like command (e.g., "LIST" or "LSUB"), + * using the reference and pattern. + * + * @param cmd the list command + * @param ref the reference string + * @param pat the pattern + * @return array of ListInfo results + * @exception ProtocolException for protocol failures + * @since JavaMail 1.4.6 + */ + protected ListInfo[] doList(String cmd, String ref, String pat) + throws ProtocolException { + Argument args = new Argument(); + writeMailboxName(args, ref); + writeMailboxName(args, pat); + + Response[] r = command(cmd, args); + + ListInfo[] linfo = null; + Response response = r[r.length-1]; + + if (response.isOK()) { // command succesful + List<ListInfo> v = new ArrayList<>(1); + for (int i = 0, len = r.length; i < len; i++) { + if (!(r[i] instanceof IMAPResponse)) + continue; + + IMAPResponse ir = (IMAPResponse)r[i]; + if (ir.keyEquals(cmd)) { + v.add(new ListInfo(ir)); + r[i] = null; + } + } + if (v.size() > 0) { + linfo = v.toArray(new ListInfo[v.size()]); + } + } + + // Dispatch remaining untagged responses + notifyResponseHandlers(r); + handleResult(response); + return linfo; + } + + /** + * APPEND Command. + * + * @param mbox the mailbox + * @param f the message Flags + * @param d the message date + * @param data the message data + * @exception ProtocolException for protocol failures + * @see "RFC2060, section 6.3.11" + */ + public void append(String mbox, Flags f, Date d, + Literal data) throws ProtocolException { + appenduid(mbox, f, d, data, false); // ignore return value + } + + /** + * APPEND Command, return uid from APPENDUID response code. + * + * @param mbox the mailbox + * @param f the message Flags + * @param d the message date + * @param data the message data + * @return APPENDUID data + * @exception ProtocolException for protocol failures + * @see "RFC2060, section 6.3.11" + */ + public AppendUID appenduid(String mbox, Flags f, Date d, + Literal data) throws ProtocolException { + return appenduid(mbox, f, d, data, true); + } + + public AppendUID appenduid(String mbox, Flags f, Date d, + Literal data, boolean uid) throws ProtocolException { + Argument args = new Argument(); + writeMailboxName(args, mbox); + + if (f != null) { // set Flags in appended message + // can't set the \Recent flag in APPEND + if (f.contains(Flags.Flag.RECENT)) { + f = new Flags(f); // copy, don't modify orig + f.remove(Flags.Flag.RECENT); // remove RECENT from copy + } + + /* + * HACK ALERT: We want the flag_list to be written out + * without any checking/processing of the bytes in it. If + * I use writeString(), the flag_list will end up being + * quoted since it contains "illegal" characters. So I + * am depending on implementation knowledge that writeAtom() + * does not do any checking/processing - it just writes out + * the bytes. What we really need is a writeFoo() that just + * dumps out its argument. + */ + args.writeAtom(createFlagList(f)); + } + if (d != null) // set INTERNALDATE in appended message + args.writeString(INTERNALDATE.format(d)); + + args.writeBytes(data); + + Response[] r = command("APPEND", args); + + // dispatch untagged responses + notifyResponseHandlers(r); + + // Handle result of this command + handleResult(r[r.length-1]); + + if (uid) + return getAppendUID(r[r.length-1]); + else + return null; + } + + /** + * If the response contains an APPENDUID response code, extract + * it and return an AppendUID object with the information. + */ + private AppendUID getAppendUID(Response r) { + if (!r.isOK()) + return null; + byte b; + while ((b = r.readByte()) > 0 && b != (byte)'[') + ; + if (b == 0) + return null; + String s; + s = r.readAtom(); + if (!s.equalsIgnoreCase("APPENDUID")) + return null; + + long uidvalidity = r.readLong(); + long uid = r.readLong(); + return new AppendUID(uidvalidity, uid); + } + + /** + * CHECK Command. + * + * @exception ProtocolException for protocol failures + * @see "RFC2060, section 6.4.1" + */ + public void check() throws ProtocolException { + simpleCommand("CHECK", null); + } + + /** + * CLOSE Command. + * + * @exception ProtocolException for protocol failures + * @see "RFC2060, section 6.4.2" + */ + public void close() throws ProtocolException { + simpleCommand("CLOSE", null); + } + + /** + * EXPUNGE Command. + * + * @exception ProtocolException for protocol failures + * @see "RFC2060, section 6.4.3" + */ + public void expunge() throws ProtocolException { + simpleCommand("EXPUNGE", null); + } + + /** + * UID EXPUNGE Command. + * + * @param set UIDs to expunge + * @exception ProtocolException for protocol failures + * @see "RFC4315, section 2" + */ + public void uidexpunge(UIDSet[] set) throws ProtocolException { + if (!hasCapability("UIDPLUS")) + throw new BadCommandException("UID EXPUNGE not supported"); + simpleCommand("UID EXPUNGE " + UIDSet.toString(set), null); + } + + /** + * Fetch the BODYSTRUCTURE of the specified message. + * + * @param msgno the message number + * @return the BODYSTRUCTURE item + * @exception ProtocolException for protocol failures + */ + public BODYSTRUCTURE fetchBodyStructure(int msgno) + throws ProtocolException { + Response[] r = fetch(msgno, "BODYSTRUCTURE"); + notifyResponseHandlers(r); + + Response response = r[r.length-1]; + if (response.isOK()) + return FetchResponse.getItem(r, msgno, BODYSTRUCTURE.class); + else if (response.isNO()) + return null; + else { + handleResult(response); + return null; + } + } + + /** + * Fetch given BODY section, without marking the message + * as SEEN. + * + * @param msgno the message number + * @param section the body section + * @return the BODY item + * @exception ProtocolException for protocol failures + */ + public BODY peekBody(int msgno, String section) + throws ProtocolException { + return fetchBody(msgno, section, true); + } + + /** + * Fetch given BODY section. + * + * @param msgno the message number + * @param section the body section + * @return the BODY item + * @exception ProtocolException for protocol failures + */ + public BODY fetchBody(int msgno, String section) + throws ProtocolException { + return fetchBody(msgno, section, false); + } + + protected BODY fetchBody(int msgno, String section, boolean peek) + throws ProtocolException { + Response[] r; + + if (section == null) + section = ""; + String body = (peek ? "BODY.PEEK[" : "BODY[") + section + "]"; + return fetchSectionBody(msgno, section, body); + } + + /** + * Partial FETCH of given BODY section, without setting SEEN flag. + * + * @param msgno the message number + * @param section the body section + * @param start starting byte count + * @param size number of bytes to fetch + * @return the BODY item + * @exception ProtocolException for protocol failures + */ + public BODY peekBody(int msgno, String section, int start, int size) + throws ProtocolException { + return fetchBody(msgno, section, start, size, true, null); + } + + /** + * Partial FETCH of given BODY section. + * + * @param msgno the message number + * @param section the body section + * @param start starting byte count + * @param size number of bytes to fetch + * @return the BODY item + * @exception ProtocolException for protocol failures + */ + public BODY fetchBody(int msgno, String section, int start, int size) + throws ProtocolException { + return fetchBody(msgno, section, start, size, false, null); + } + + /** + * Partial FETCH of given BODY section, without setting SEEN flag. + * + * @param msgno the message number + * @param section the body section + * @param start starting byte count + * @param size number of bytes to fetch + * @param ba the buffer into which to read the response + * @return the BODY item + * @exception ProtocolException for protocol failures + */ + public BODY peekBody(int msgno, String section, int start, int size, + ByteArray ba) throws ProtocolException { + return fetchBody(msgno, section, start, size, true, ba); + } + + /** + * Partial FETCH of given BODY section. + * + * @param msgno the message number + * @param section the body section + * @param start starting byte count + * @param size number of bytes to fetch + * @param ba the buffer into which to read the response + * @return the BODY item + * @exception ProtocolException for protocol failures + */ + public BODY fetchBody(int msgno, String section, int start, int size, + ByteArray ba) throws ProtocolException { + return fetchBody(msgno, section, start, size, false, ba); + } + + protected BODY fetchBody(int msgno, String section, int start, int size, + boolean peek, ByteArray ba) throws ProtocolException { + this.ba = ba; // save for later use by getResponseBuffer + if (section == null) + section = ""; + String body = (peek ? "BODY.PEEK[" : "BODY[") + section + "]<" + + String.valueOf(start) + "." + + String.valueOf(size) + ">"; + return fetchSectionBody(msgno, section, body); + } + + /** + * Fetch the given body section of the given message, using the + * body string "body". + * + * @param msgno the message number + * @param section the body section + * @param body the body string + * @return the BODY item + * @exception ProtocolException for protocol failures + */ + protected BODY fetchSectionBody(int msgno, String section, String body) + throws ProtocolException { + Response[] r; + + r = fetch(msgno, body); + notifyResponseHandlers(r); + + Response response = r[r.length-1]; + if (response.isOK()) { + List<BODY> bl = FetchResponse.getItems(r, msgno, BODY.class); + if (bl.size() == 1) + return bl.get(0); // the common case + if (logger.isLoggable(Level.FINEST)) + logger.finest("got " + bl.size() + + " BODY responses for section " + section); + // more then one BODY response, have to find the right one + for (BODY br : bl) { + if (logger.isLoggable(Level.FINEST)) + logger.finest("got BODY section " + br.getSection()); + if (br.getSection().equalsIgnoreCase(section)) + return br; // that's the one! + } + return null; // couldn't find it + } else if (response.isNO()) + return null; + else { + handleResult(response); + return null; + } + } + + /** + * Return a buffer to read a response into. + * The buffer is provided by fetchBody and is + * used only once. + * + * @return the buffer to use + */ + @Override + protected ByteArray getResponseBuffer() { + ByteArray ret = ba; + ba = null; + return ret; + } + + /** + * Fetch the specified RFC822 Data item. 'what' names + * the item to be fetched. 'what' can be <code>null</code> + * to fetch the whole message. + * + * @param msgno the message number + * @param what the item to fetch + * @return the RFC822DATA item + * @exception ProtocolException for protocol failures + */ + public RFC822DATA fetchRFC822(int msgno, String what) + throws ProtocolException { + Response[] r = fetch(msgno, + what == null ? "RFC822" : "RFC822." + what + ); + + // dispatch untagged responses + notifyResponseHandlers(r); + + Response response = r[r.length-1]; + if (response.isOK()) + return FetchResponse.getItem(r, msgno, RFC822DATA.class); + else if (response.isNO()) + return null; + else { + handleResult(response); + return null; + } + } + + /** + * Fetch the FLAGS for the given message. + * + * @param msgno the message number + * @return the Flags + * @exception ProtocolException for protocol failures + */ + public Flags fetchFlags(int msgno) throws ProtocolException { + Flags flags = null; + Response[] r = fetch(msgno, "FLAGS"); + + // Search for our FLAGS response + for (int i = 0, len = r.length; i < len; i++) { + if (r[i] == null || + !(r[i] instanceof FetchResponse) || + ((FetchResponse)r[i]).getNumber() != msgno) + continue; + + FetchResponse fr = (FetchResponse)r[i]; + if ((flags = fr.getItem(FLAGS.class)) != null) { + r[i] = null; // remove this response + break; + } + } + + // dispatch untagged responses + notifyResponseHandlers(r); + handleResult(r[r.length-1]); + return flags; + } + + /** + * Fetch the IMAP UID for the given message. + * + * @param msgno the message number + * @return the UID + * @exception ProtocolException for protocol failures + */ + public UID fetchUID(int msgno) throws ProtocolException { + Response[] r = fetch(msgno, "UID"); + + // dispatch untagged responses + notifyResponseHandlers(r); + + Response response = r[r.length-1]; + if (response.isOK()) + return FetchResponse.getItem(r, msgno, UID.class); + else if (response.isNO()) // XXX: Issue NOOP ? + return null; + else { + handleResult(response); + return null; // NOTREACHED + } + } + + /** + * Fetch the IMAP MODSEQ for the given message. + * + * @param msgno the message number + * @return the MODSEQ + * @exception ProtocolException for protocol failures + * @since JavaMail 1.5.1 + */ + public MODSEQ fetchMODSEQ(int msgno) throws ProtocolException { + Response[] r = fetch(msgno, "MODSEQ"); + + // dispatch untagged responses + notifyResponseHandlers(r); + + Response response = r[r.length-1]; + if (response.isOK()) + return FetchResponse.getItem(r, msgno, MODSEQ.class); + else if (response.isNO()) // XXX: Issue NOOP ? + return null; + else { + handleResult(response); + return null; // NOTREACHED + } + } + + /** + * Get the sequence number for the given UID. Nothing is returned; + * the FETCH UID response must be handled by the reponse handler, + * along with any possible EXPUNGE responses, to ensure that the + * UID is matched with the correct sequence number. + * + * @param uid the UID + * @exception ProtocolException for protocol failures + * @since JavaMail 1.5.3 + */ + public void fetchSequenceNumber(long uid) throws ProtocolException { + Response[] r = fetch(String.valueOf(uid), "UID", true); + + notifyResponseHandlers(r); + handleResult(r[r.length-1]); + } + + /** + * Get the sequence numbers for UIDs ranging from start till end. + * Since the range may be large and sparse, an array of the UIDs actually + * found is returned. The caller must map these to messages after + * the FETCH UID responses have been handled by the reponse handler, + * along with any possible EXPUNGE responses, to ensure that the + * UIDs are matched with the correct sequence numbers. + * + * @param start first UID + * @param end last UID + * @return array of sequence numbers + * @exception ProtocolException for protocol failures + * @since JavaMail 1.5.3 + */ + public long[] fetchSequenceNumbers(long start, long end) + throws ProtocolException { + Response[] r = fetch(String.valueOf(start) + ":" + + (end == UIDFolder.LASTUID ? "*" : + String.valueOf(end)), + "UID", true); + + UID u; + List<UID> v = new ArrayList<>(); + for (int i = 0, len = r.length; i < len; i++) { + if (r[i] == null || !(r[i] instanceof FetchResponse)) + continue; + + FetchResponse fr = (FetchResponse)r[i]; + if ((u = fr.getItem(UID.class)) != null) + v.add(u); + } + + notifyResponseHandlers(r); + handleResult(r[r.length-1]); + + long[] lv = new long[v.size()]; + for (int i = 0; i < v.size(); i++) + lv[i] = v.get(i).uid; + return lv; + } + + /** + * Get the sequence numbers for UIDs specified in the array. + * Nothing is returned. The caller must map the UIDs to messages after + * the FETCH UID responses have been handled by the reponse handler, + * along with any possible EXPUNGE responses, to ensure that the + * UIDs are matched with the correct sequence numbers. + * + * @param uids the UIDs + * @exception ProtocolException for protocol failures + * @since JavaMail 1.5.3 + */ + public void fetchSequenceNumbers(long[] uids) throws ProtocolException { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < uids.length; i++) { + if (i > 0) + sb.append(","); + sb.append(String.valueOf(uids[i])); + } + + Response[] r = fetch(sb.toString(), "UID", true); + + notifyResponseHandlers(r); + handleResult(r[r.length-1]); + } + + /** + * Get the sequence numbers for messages changed since the given + * modseq and with UIDs ranging from start till end. + * Also, prefetch the flags for the returned messages. + * + * @param start first UID + * @param end last UID + * @param modseq the MODSEQ + * @return array of sequence numbers + * @exception ProtocolException for protocol failures + * @see "RFC 4551" + * @since JavaMail 1.5.1 + */ + public int[] uidfetchChangedSince(long start, long end, long modseq) + throws ProtocolException { + String msgSequence = String.valueOf(start) + ":" + + (end == UIDFolder.LASTUID ? "*" : + String.valueOf(end)); + Response[] r = command("UID FETCH " + msgSequence + + " (FLAGS) (CHANGEDSINCE " + String.valueOf(modseq) + ")", null); + + List<Integer> v = new ArrayList<>(); + for (int i = 0, len = r.length; i < len; i++) { + if (r[i] == null || !(r[i] instanceof FetchResponse)) + continue; + + FetchResponse fr = (FetchResponse)r[i]; + v.add(Integer.valueOf(fr.getNumber())); + } + + notifyResponseHandlers(r); + handleResult(r[r.length-1]); + + // Copy the list into 'matches' + int vsize = v.size(); + int[] matches = new int[vsize]; + for (int i = 0; i < vsize; i++) + matches[i] = v.get(i).intValue(); + return matches; + } + + public Response[] fetch(MessageSet[] msgsets, String what) + throws ProtocolException { + return fetch(MessageSet.toString(msgsets), what, false); + } + + public Response[] fetch(int start, int end, String what) + throws ProtocolException { + return fetch(String.valueOf(start) + ":" + String.valueOf(end), + what, false); + } + + public Response[] fetch(int msg, String what) + throws ProtocolException { + return fetch(String.valueOf(msg), what, false); + } + + private Response[] fetch(String msgSequence, String what, boolean uid) + throws ProtocolException { + if (uid) + return command("UID FETCH " + msgSequence +" (" + what + ")",null); + else + return command("FETCH " + msgSequence + " (" + what + ")", null); + } + + /** + * COPY command. + * + * @param msgsets the messages to copy + * @param mbox the mailbox to copy them to + * @exception ProtocolException for protocol failures + */ + public void copy(MessageSet[] msgsets, String mbox) + throws ProtocolException { + copyuid(MessageSet.toString(msgsets), mbox, false); + } + + /** + * COPY command. + * + * @param start start message number + * @param end end message number + * @param mbox the mailbox to copy them to + * @exception ProtocolException for protocol failures + */ + public void copy(int start, int end, String mbox) + throws ProtocolException { + copyuid(String.valueOf(start) + ":" + String.valueOf(end), + mbox, false); + } + + /** + * COPY command, return uid from COPYUID response code. + * + * @param msgsets the messages to copy + * @param mbox the mailbox to copy them to + * @return COPYUID response data + * @exception ProtocolException for protocol failures + * @see "RFC 4315, section 3" + */ + public CopyUID copyuid(MessageSet[] msgsets, String mbox) + throws ProtocolException { + return copyuid(MessageSet.toString(msgsets), mbox, true); + } + + /** + * COPY command, return uid from COPYUID response code. + * + * @param start start message number + * @param end end message number + * @param mbox the mailbox to copy them to + * @return COPYUID response data + * @exception ProtocolException for protocol failures + * @see "RFC 4315, section 3" + */ + public CopyUID copyuid(int start, int end, String mbox) + throws ProtocolException { + return copyuid(String.valueOf(start) + ":" + String.valueOf(end), + mbox, true); + } + + private CopyUID copyuid(String msgSequence, String mbox, boolean uid) + throws ProtocolException { + if (uid && !hasCapability("UIDPLUS")) + throw new BadCommandException("UIDPLUS not supported"); + + Argument args = new Argument(); + args.writeAtom(msgSequence); + writeMailboxName(args, mbox); + + Response[] r = command("COPY", args); + + // dispatch untagged responses + notifyResponseHandlers(r); + + // Handle result of this command + handleResult(r[r.length-1]); + + if (uid) + return getCopyUID(r); + else + return null; + } + + /** + * MOVE command. + * + * @param msgsets the messages to move + * @param mbox the mailbox to move them to + * @exception ProtocolException for protocol failures + * @see "RFC 6851" + * @since JavaMail 1.5.4 + */ + public void move(MessageSet[] msgsets, String mbox) + throws ProtocolException { + moveuid(MessageSet.toString(msgsets), mbox, false); + } + + /** + * MOVE command. + * + * @param start start message number + * @param end end message number + * @param mbox the mailbox to move them to + * @exception ProtocolException for protocol failures + * @see "RFC 6851" + * @since JavaMail 1.5.4 + */ + public void move(int start, int end, String mbox) + throws ProtocolException { + moveuid(String.valueOf(start) + ":" + String.valueOf(end), + mbox, false); + } + + /** + * MOVE Command, return uid from COPYUID response code. + * + * @param msgsets the messages to move + * @param mbox the mailbox to move them to + * @return COPYUID response data + * @exception ProtocolException for protocol failures + * @see "RFC 6851" + * @see "RFC 4315, section 3" + * @since JavaMail 1.5.4 + */ + public CopyUID moveuid(MessageSet[] msgsets, String mbox) + throws ProtocolException { + return moveuid(MessageSet.toString(msgsets), mbox, true); + } + + /** + * MOVE Command, return uid from COPYUID response code. + * + * @param start start message number + * @param end end message number + * @param mbox the mailbox to move them to + * @return COPYUID response data + * @exception ProtocolException for protocol failures + * @see "RFC 6851" + * @see "RFC 4315, section 3" + * @since JavaMail 1.5.4 + */ + public CopyUID moveuid(int start, int end, String mbox) + throws ProtocolException { + return moveuid(String.valueOf(start) + ":" + String.valueOf(end), + mbox, true); + } + + /** + * MOVE Command, return uid from COPYUID response code. + * + * @see "RFC 6851" + * @see "RFC 4315, section 3" + * @since JavaMail 1.5.4 + */ + private CopyUID moveuid(String msgSequence, String mbox, boolean uid) + throws ProtocolException { + if (!hasCapability("MOVE")) + throw new BadCommandException("MOVE not supported"); + if (uid && !hasCapability("UIDPLUS")) + throw new BadCommandException("UIDPLUS not supported"); + + Argument args = new Argument(); + args.writeAtom(msgSequence); + writeMailboxName(args, mbox); + + Response[] r = command("MOVE", args); + + // dispatch untagged responses + notifyResponseHandlers(r); + + // Handle result of this command + handleResult(r[r.length-1]); + + if (uid) + return getCopyUID(r); + else + return null; + } + + /** + * If the response contains a COPYUID response code, extract + * it and return a CopyUID object with the information. + * + * @param rr the responses to examine + * @return the COPYUID response code data, or null if not found + * @since JavaMail 1.5.4 + */ + protected CopyUID getCopyUID(Response[] rr) { + // most likely in the last response, so start there and work backward + for (int i = rr.length - 1; i >= 0; i--) { + Response r = rr[i]; + if (r == null || !r.isOK()) + continue; + byte b; + while ((b = r.readByte()) > 0 && b != (byte)'[') + ; + if (b == 0) + continue; + String s; + s = r.readAtom(); + if (!s.equalsIgnoreCase("COPYUID")) + continue; + + // XXX - need to merge more than one response for MOVE? + long uidvalidity = r.readLong(); + String src = r.readAtom(); + String dst = r.readAtom(); + return new CopyUID(uidvalidity, + UIDSet.parseUIDSets(src), UIDSet.parseUIDSets(dst)); + } + return null; + } + + public void storeFlags(MessageSet[] msgsets, Flags flags, boolean set) + throws ProtocolException { + storeFlags(MessageSet.toString(msgsets), flags, set); + } + + public void storeFlags(int start, int end, Flags flags, boolean set) + throws ProtocolException { + storeFlags(String.valueOf(start) + ":" + String.valueOf(end), + flags, set); + } + + /** + * Set the specified flags on this message. + * + * @param msg the message number + * @param flags the flags + * @param set true to set, false to clear + * @exception ProtocolException for protocol failures + */ + public void storeFlags(int msg, Flags flags, boolean set) + throws ProtocolException { + storeFlags(String.valueOf(msg), flags, set); + } + + private void storeFlags(String msgset, Flags flags, boolean set) + throws ProtocolException { + Response[] r; + if (set) + r = command("STORE " + msgset + " +FLAGS " + + createFlagList(flags), null); + else + r = command("STORE " + msgset + " -FLAGS " + + createFlagList(flags), null); + + // Dispatch untagged responses + notifyResponseHandlers(r); + handleResult(r[r.length-1]); + } + + /** + * Creates an IMAP flag_list from the given Flags object. + * + * @param flags the flags + * @return the IMAP flag_list + * @since JavaMail 1.5.4 + */ + protected String createFlagList(Flags flags) { + StringBuilder sb = new StringBuilder("("); // start of flag_list + + Flags.Flag[] sf = flags.getSystemFlags(); // get the system flags + boolean first = true; + for (int i = 0; i < sf.length; i++) { + String s; + Flags.Flag f = sf[i]; + if (f == Flags.Flag.ANSWERED) + s = "\\Answered"; + else if (f == Flags.Flag.DELETED) + s = "\\Deleted"; + else if (f == Flags.Flag.DRAFT) + s = "\\Draft"; + else if (f == Flags.Flag.FLAGGED) + s = "\\Flagged"; + else if (f == Flags.Flag.RECENT) + s = "\\Recent"; + else if (f == Flags.Flag.SEEN) + s = "\\Seen"; + else + continue; // skip it + if (first) + first = false; + else + sb.append(' '); + sb.append(s); + } + + String[] uf = flags.getUserFlags(); // get the user flag strings + for (int i = 0; i < uf.length; i++) { + if (first) + first = false; + else + sb.append(' '); + sb.append(uf[i]); + } + + sb.append(")"); // terminate flag_list + return sb.toString(); + } + + /** + * Issue the given search criterion on the specified message sets. + * Returns array of matching sequence numbers. An empty array + * is returned if no matches are found. + * + * @param msgsets array of MessageSets + * @param term SearchTerm + * @return array of matching sequence numbers. + * @exception ProtocolException for protocol failures + * @exception SearchException for search failures + */ + public int[] search(MessageSet[] msgsets, SearchTerm term) + throws ProtocolException, SearchException { + return search(MessageSet.toString(msgsets), term); + } + + /** + * Issue the given search criterion on all messages in this folder. + * Returns array of matching sequence numbers. An empty array + * is returned if no matches are found. + * + * @param term SearchTerm + * @return array of matching sequence numbers. + * @exception ProtocolException for protocol failures + * @exception SearchException for search failures + */ + public int[] search(SearchTerm term) + throws ProtocolException, SearchException { + return search("ALL", term); + } + + /* + * Apply the given SearchTerm on the specified sequence. + * Returns array of matching sequence numbers. Note that an empty + * array is returned for no matches. + */ + private int[] search(String msgSequence, SearchTerm term) + throws ProtocolException, SearchException { + // Check if the search "text" terms contain only ASCII chars, + // or if utf8 support has been enabled (in which case CHARSET + // is not allowed; see RFC 6855, section 3, last paragraph) + if (supportsUtf8() || SearchSequence.isAscii(term)) { + try { + return issueSearch(msgSequence, term, null); + } catch (IOException ioex) { /* will not happen */ } + } + + /* + * The search "text" terms do contain non-ASCII chars and utf8 + * support has not been enabled. We need to use: + * "SEARCH CHARSET <charset> ..." + * The charsets we try to use are UTF-8 and the locale's + * default charset. If the server supports UTF-8, great, + * always use it. Else we try to use the default charset. + */ + + // Cycle thru the list of charsets + for (int i = 0; i < searchCharsets.length; i++) { + if (searchCharsets[i] == null) + continue; + + try { + return issueSearch(msgSequence, term, searchCharsets[i]); + } catch (CommandFailedException cfx) { + /* + * Server returned NO. For now, I'll just assume that + * this indicates that this charset is unsupported. + * We can check the BADCHARSET response code once + * that's spec'd into the IMAP RFC .. + */ + searchCharsets[i] = null; + continue; + } catch (IOException ioex) { + /* Charset conversion failed. Try the next one */ + continue; + } catch (ProtocolException pex) { + throw pex; + } catch (SearchException sex) { + throw sex; + } + } + + // No luck. + throw new SearchException("Search failed"); + } + + /* Apply the given SearchTerm on the specified sequence, using the + * given charset. <p> + * Returns array of matching sequence numbers. Note that an empty + * array is returned for no matches. + */ + private int[] issueSearch(String msgSequence, SearchTerm term, + String charset) + throws ProtocolException, SearchException, IOException { + + // Generate a search-sequence with the given charset + Argument args = getSearchSequence().generateSequence(term, + charset == null ? null : + MimeUtility.javaCharset(charset) + ); + args.writeAtom(msgSequence); + + Response[] r; + + if (charset == null) // text is all US-ASCII + r = command("SEARCH", args); + else + r = command("SEARCH CHARSET " + charset, args); + + Response response = r[r.length-1]; + int[] matches = null; + + // Grab all SEARCH responses + if (response.isOK()) { // command succesful + List<Integer> v = new ArrayList<>(); + int num; + for (int i = 0, len = r.length; i < len; i++) { + if (!(r[i] instanceof IMAPResponse)) + continue; + + IMAPResponse ir = (IMAPResponse)r[i]; + // There *will* be one SEARCH response. + if (ir.keyEquals("SEARCH")) { + while ((num = ir.readNumber()) != -1) + v.add(Integer.valueOf(num)); + r[i] = null; + } + } + + // Copy the list into 'matches' + int vsize = v.size(); + matches = new int[vsize]; + for (int i = 0; i < vsize; i++) + matches[i] = v.get(i).intValue(); + } + + // dispatch remaining untagged responses + notifyResponseHandlers(r); + handleResult(response); + return matches; + } + + /** + * Get the SearchSequence object. + * The SearchSequence object instance is saved in the searchSequence + * field. Subclasses of IMAPProtocol may override this method to + * return a subclass of SearchSequence, in order to add support for + * product-specific search terms. + * + * @return the SearchSequence + * @since JavaMail 1.4.6 + */ + protected SearchSequence getSearchSequence() { + if (searchSequence == null) + searchSequence = new SearchSequence(this); + return searchSequence; + } + + /** + * Sort messages in the folder according to the specified sort criteria. + * If the search term is not null, limit the sort to only the messages + * that match the search term. + * Returns an array of sorted sequence numbers. An empty array + * is returned if no matches are found. + * + * @param term sort criteria + * @param sterm SearchTerm + * @return array of matching sequence numbers. + * @exception ProtocolException for protocol failures + * @exception SearchException for search failures + * + * @see "RFC 5256" + * @since JavaMail 1.4.4 + */ + public int[] sort(SortTerm[] term, SearchTerm sterm) + throws ProtocolException, SearchException { + if (!hasCapability("SORT*")) + throw new BadCommandException("SORT not supported"); + + if (term == null || term.length == 0) + throw new BadCommandException("Must have at least one sort term"); + + Argument args = new Argument(); + Argument sargs = new Argument(); + for (int i = 0; i < term.length; i++) + sargs.writeAtom(term[i].toString()); + args.writeArgument(sargs); // sort criteria + + args.writeAtom("UTF-8"); // charset specification + if (sterm != null) { + try { + args.append( + getSearchSequence().generateSequence(sterm, "UTF-8")); + } catch (IOException ioex) { + // should never happen + throw new SearchException(ioex.toString()); + } + } else + args.writeAtom("ALL"); + + Response[] r = command("SORT", args); + Response response = r[r.length-1]; + int[] matches = null; + + // Grab all SORT responses + if (response.isOK()) { // command succesful + List<Integer> v = new ArrayList<>(); + int num; + for (int i = 0, len = r.length; i < len; i++) { + if (!(r[i] instanceof IMAPResponse)) + continue; + + IMAPResponse ir = (IMAPResponse)r[i]; + if (ir.keyEquals("SORT")) { + while ((num = ir.readNumber()) != -1) + v.add(Integer.valueOf(num)); + r[i] = null; + } + } + + // Copy the list into 'matches' + int vsize = v.size(); + matches = new int[vsize]; + for (int i = 0; i < vsize; i++) + matches[i] = v.get(i).intValue(); + } + + // dispatch remaining untagged responses + notifyResponseHandlers(r); + handleResult(response); + return matches; + } + + /** + * NAMESPACE Command. + * + * @return the namespaces + * @exception ProtocolException for protocol failures + * @see "RFC2342" + */ + public Namespaces namespace() throws ProtocolException { + if (!hasCapability("NAMESPACE")) + throw new BadCommandException("NAMESPACE not supported"); + + Response[] r = command("NAMESPACE", null); + + Namespaces namespace = null; + Response response = r[r.length-1]; + + // Grab NAMESPACE response + if (response.isOK()) { // command succesful + for (int i = 0, len = r.length; i < len; i++) { + if (!(r[i] instanceof IMAPResponse)) + continue; + + IMAPResponse ir = (IMAPResponse)r[i]; + if (ir.keyEquals("NAMESPACE")) { + if (namespace == null) + namespace = new Namespaces(ir); + r[i] = null; + } + } + } + + // dispatch remaining untagged responses + notifyResponseHandlers(r); + handleResult(response); + return namespace; + } + + /** + * GETQUOTAROOT Command. + * + * Returns an array of Quota objects, representing the quotas + * for this mailbox and, indirectly, the quotaroots for this + * mailbox. + * + * @param mbox the mailbox + * @return array of Quota objects + * @exception ProtocolException for protocol failures + * @see "RFC2087" + */ + public Quota[] getQuotaRoot(String mbox) throws ProtocolException { + if (!hasCapability("QUOTA")) + throw new BadCommandException("GETQUOTAROOT not supported"); + + Argument args = new Argument(); + writeMailboxName(args, mbox); + + Response[] r = command("GETQUOTAROOT", args); + + Response response = r[r.length-1]; + + Map<String, Quota> tab = new HashMap<>(); + + // Grab all QUOTAROOT and QUOTA responses + if (response.isOK()) { // command succesful + for (int i = 0, len = r.length; i < len; i++) { + if (!(r[i] instanceof IMAPResponse)) + continue; + + IMAPResponse ir = (IMAPResponse)r[i]; + if (ir.keyEquals("QUOTAROOT")) { + // quotaroot_response + // ::= "QUOTAROOT" SP astring *(SP astring) + + // read name of mailbox and throw away + ir.readAtomString(); + // for each quotaroot add a placeholder quota + String root = null; + while ((root = ir.readAtomString()) != null && + root.length() > 0) + tab.put(root, new Quota(root)); + r[i] = null; + } else if (ir.keyEquals("QUOTA")) { + Quota quota = parseQuota(ir); + Quota q = tab.get(quota.quotaRoot); + if (q != null && q.resources != null) { + // merge resources + int newl = q.resources.length + quota.resources.length; + Quota.Resource[] newr = new Quota.Resource[newl]; + System.arraycopy(q.resources, 0, newr, 0, + q.resources.length); + System.arraycopy(quota.resources, 0, + newr, q.resources.length, quota.resources.length); + quota.resources = newr; + } + tab.put(quota.quotaRoot, quota); + r[i] = null; + } + } + } + + // dispatch remaining untagged responses + notifyResponseHandlers(r); + handleResult(response); + + return tab.values().toArray(new Quota[tab.size()]); + } + + /** + * GETQUOTA Command. + * + * Returns an array of Quota objects, representing the quotas + * for this quotaroot. + * + * @param root the quotaroot + * @return the quotas + * @exception ProtocolException for protocol failures + * @see "RFC2087" + */ + public Quota[] getQuota(String root) throws ProtocolException { + if (!hasCapability("QUOTA")) + throw new BadCommandException("QUOTA not supported"); + + Argument args = new Argument(); + args.writeString(root); // XXX - could be UTF-8? + + Response[] r = command("GETQUOTA", args); + + Quota quota = null; + List<Quota> v = new ArrayList<>(); + Response response = r[r.length-1]; + + // Grab all QUOTA responses + if (response.isOK()) { // command succesful + for (int i = 0, len = r.length; i < len; i++) { + if (!(r[i] instanceof IMAPResponse)) + continue; + + IMAPResponse ir = (IMAPResponse)r[i]; + if (ir.keyEquals("QUOTA")) { + quota = parseQuota(ir); + v.add(quota); + r[i] = null; + } + } + } + + // dispatch remaining untagged responses + notifyResponseHandlers(r); + handleResult(response); + return v.toArray(new Quota[v.size()]); + } + + /** + * SETQUOTA Command. + * + * Set the indicated quota on the corresponding quotaroot. + * + * @param quota the quota to set + * @exception ProtocolException for protocol failures + * @see "RFC2087" + */ + public void setQuota(Quota quota) throws ProtocolException { + if (!hasCapability("QUOTA")) + throw new BadCommandException("QUOTA not supported"); + + Argument args = new Argument(); + args.writeString(quota.quotaRoot); // XXX - could be UTF-8? + Argument qargs = new Argument(); + if (quota.resources != null) { + for (int i = 0; i < quota.resources.length; i++) { + qargs.writeAtom(quota.resources[i].name); + qargs.writeNumber(quota.resources[i].limit); + } + } + args.writeArgument(qargs); + + Response[] r = command("SETQUOTA", args); + Response response = r[r.length-1]; + + // XXX - It's not clear from the RFC whether the SETQUOTA command + // will provoke untagged QUOTA responses. If it does, perhaps + // we should grab them here and return them? + + /* + Quota quota = null; + List<Quota> v = new ArrayList<Quota>(); + + // Grab all QUOTA responses + if (response.isOK()) { // command succesful + for (int i = 0, len = r.length; i < len; i++) { + if (!(r[i] instanceof IMAPResponse)) + continue; + + IMAPResponse ir = (IMAPResponse)r[i]; + if (ir.keyEquals("QUOTA")) { + quota = parseQuota(ir); + v.add(quota); + r[i] = null; + } + } + } + */ + + // dispatch remaining untagged responses + notifyResponseHandlers(r); + handleResult(response); + /* + return v.toArray(new Quota[v.size()]); + */ + } + + /** + * Parse a QUOTA response. + */ + private Quota parseQuota(Response r) throws ParsingException { + // quota_response ::= "QUOTA" SP astring SP quota_list + String quotaRoot = r.readAtomString(); // quotaroot ::= astring + Quota q = new Quota(quotaRoot); + r.skipSpaces(); + // quota_list ::= "(" #quota_resource ")" + if (r.readByte() != '(') + throw new ParsingException("parse error in QUOTA"); + + List<Quota.Resource> v = new ArrayList<>(); + while (!r.isNextNonSpace(')')) { + // quota_resource ::= atom SP number SP number + String name = r.readAtom(); + if (name != null) { + long usage = r.readLong(); + long limit = r.readLong(); + Quota.Resource res = new Quota.Resource(name, usage, limit); + v.add(res); + } + } + q.resources = v.toArray(new Quota.Resource[v.size()]); + return q; + } + + + /** + * SETACL Command. + * + * @param mbox the mailbox + * @param modifier the ACL modifier + * @param acl the ACL + * @exception ProtocolException for protocol failures + * @see "RFC2086" + */ + public void setACL(String mbox, char modifier, ACL acl) + throws ProtocolException { + if (!hasCapability("ACL")) + throw new BadCommandException("ACL not supported"); + + Argument args = new Argument(); + writeMailboxName(args, mbox); + args.writeString(acl.getName()); + String rights = acl.getRights().toString(); + if (modifier == '+' || modifier == '-') + rights = modifier + rights; + args.writeString(rights); + + Response[] r = command("SETACL", args); + Response response = r[r.length-1]; + + // dispatch untagged responses + notifyResponseHandlers(r); + handleResult(response); + } + + /** + * DELETEACL Command. + * + * @param mbox the mailbox + * @param user the user + * @exception ProtocolException for protocol failures + * @see "RFC2086" + */ + public void deleteACL(String mbox, String user) throws ProtocolException { + if (!hasCapability("ACL")) + throw new BadCommandException("ACL not supported"); + + Argument args = new Argument(); + writeMailboxName(args, mbox); + args.writeString(user); // XXX - could be UTF-8? + + Response[] r = command("DELETEACL", args); + Response response = r[r.length-1]; + + // dispatch untagged responses + notifyResponseHandlers(r); + handleResult(response); + } + + /** + * GETACL Command. + * + * @param mbox the mailbox + * @return the ACL array + * @exception ProtocolException for protocol failures + * @see "RFC2086" + */ + public ACL[] getACL(String mbox) throws ProtocolException { + if (!hasCapability("ACL")) + throw new BadCommandException("ACL not supported"); + + Argument args = new Argument(); + writeMailboxName(args, mbox); + + Response[] r = command("GETACL", args); + Response response = r[r.length-1]; + + // Grab all ACL responses + List<ACL> v = new ArrayList<>(); + if (response.isOK()) { // command succesful + for (int i = 0, len = r.length; i < len; i++) { + if (!(r[i] instanceof IMAPResponse)) + continue; + + IMAPResponse ir = (IMAPResponse)r[i]; + if (ir.keyEquals("ACL")) { + // acl_data ::= "ACL" SPACE mailbox + // *(SPACE identifier SPACE rights) + // read name of mailbox and throw away + ir.readAtomString(); + String name = null; + while ((name = ir.readAtomString()) != null) { + String rights = ir.readAtomString(); + if (rights == null) + break; + ACL acl = new ACL(name, new Rights(rights)); + v.add(acl); + } + r[i] = null; + } + } + } + + // dispatch remaining untagged responses + notifyResponseHandlers(r); + handleResult(response); + return v.toArray(new ACL[v.size()]); + } + + /** + * LISTRIGHTS Command. + * + * @param mbox the mailbox + * @param user the user rights to return + * @return the rights array + * @exception ProtocolException for protocol failures + * @see "RFC2086" + */ + public Rights[] listRights(String mbox, String user) + throws ProtocolException { + if (!hasCapability("ACL")) + throw new BadCommandException("ACL not supported"); + + Argument args = new Argument(); + writeMailboxName(args, mbox); + args.writeString(user); // XXX - could be UTF-8? + + Response[] r = command("LISTRIGHTS", args); + Response response = r[r.length-1]; + + // Grab LISTRIGHTS response + List<Rights> v = new ArrayList<>(); + if (response.isOK()) { // command succesful + for (int i = 0, len = r.length; i < len; i++) { + if (!(r[i] instanceof IMAPResponse)) + continue; + + IMAPResponse ir = (IMAPResponse)r[i]; + if (ir.keyEquals("LISTRIGHTS")) { + // listrights_data ::= "LISTRIGHTS" SPACE mailbox + // SPACE identifier SPACE rights *(SPACE rights) + // read name of mailbox and throw away + ir.readAtomString(); + // read identifier and throw away + ir.readAtomString(); + String rights; + while ((rights = ir.readAtomString()) != null) + v.add(new Rights(rights)); + r[i] = null; + } + } + } + + // dispatch remaining untagged responses + notifyResponseHandlers(r); + handleResult(response); + return v.toArray(new Rights[v.size()]); + } + + /** + * MYRIGHTS Command. + * + * @param mbox the mailbox + * @return the rights + * @exception ProtocolException for protocol failures + * @see "RFC2086" + */ + public Rights myRights(String mbox) throws ProtocolException { + if (!hasCapability("ACL")) + throw new BadCommandException("ACL not supported"); + + Argument args = new Argument(); + writeMailboxName(args, mbox); + + Response[] r = command("MYRIGHTS", args); + Response response = r[r.length-1]; + + // Grab MYRIGHTS response + Rights rights = null; + if (response.isOK()) { // command succesful + for (int i = 0, len = r.length; i < len; i++) { + if (!(r[i] instanceof IMAPResponse)) + continue; + + IMAPResponse ir = (IMAPResponse)r[i]; + if (ir.keyEquals("MYRIGHTS")) { + // myrights_data ::= "MYRIGHTS" SPACE mailbox SPACE rights + // read name of mailbox and throw away + ir.readAtomString(); + String rs = ir.readAtomString(); + if (rights == null) + rights = new Rights(rs); + r[i] = null; + } + } + } + + // dispatch remaining untagged responses + notifyResponseHandlers(r); + handleResult(response); + return rights; + } + + /* + * The tag used on the IDLE command. Set by idleStart() and + * used in processIdleResponse() to determine if the response + * is the matching end tag. + */ + private volatile String idleTag; + + /** + * IDLE Command. <p> + * + * If the server supports the IDLE command extension, the IDLE + * command is issued and this method blocks until a response has + * been received. Once the first response has been received, the + * IDLE command is terminated and all responses are collected and + * handled and this method returns. <p> + * + * Note that while this method is blocked waiting for a response, + * no other threads may issue any commands to the server that would + * use this same connection. + * + * @exception ProtocolException for protocol failures + * @see "RFC2177" + * @since JavaMail 1.4.1 + */ + public synchronized void idleStart() throws ProtocolException { + if (!hasCapability("IDLE")) + throw new BadCommandException("IDLE not supported"); + + List<Response> v = new ArrayList<>(); + boolean done = false; + Response r = null; + + // write the command + try { + idleTag = writeCommand("IDLE", null); + } catch (LiteralException lex) { + v.add(lex.getResponse()); + done = true; + } catch (Exception ex) { + // Convert this into a BYE response + v.add(Response.byeResponse(ex)); + done = true; + } + + while (!done) { + try { + r = readResponse(); + } catch (IOException ioex) { + // convert this into a BYE response + r = Response.byeResponse(ioex); + } catch (ProtocolException pex) { + continue; // skip this response + } + + v.add(r); + + if (r.isContinuation() || r.isBYE()) + done = true; + } + + Response[] responses = v.toArray(new Response[v.size()]); + r = responses[responses.length-1]; + + // dispatch remaining untagged responses + notifyResponseHandlers(responses); + if (!r.isContinuation()) + handleResult(r); + } + + /** + * While an IDLE command is in progress, read a response + * sent from the server. The response is read with no locks + * held so that when the read blocks waiting for the response + * from the server it's not holding locks that would prevent + * other threads from interrupting the IDLE command. + * + * @return the response + * @since JavaMail 1.4.1 + */ + public synchronized Response readIdleResponse() { + if (idleTag == null) + return null; // IDLE not in progress + Response r = null; + try { + r = readResponse(); + } catch (IOException ioex) { + // convert this into a BYE response + r = Response.byeResponse(ioex); + } catch (ProtocolException pex) { + // convert this into a BYE response + r = Response.byeResponse(pex); + } + return r; + } + + /** + * Process a response returned by readIdleResponse(). + * This method will be called with appropriate locks + * held so that the processing of the response is safe. + * + * @param r the response + * @return true if IDLE is done + * @exception ProtocolException for protocol failures + * @since JavaMail 1.4.1 + */ + public boolean processIdleResponse(Response r) throws ProtocolException { + Response[] responses = new Response[1]; + responses[0] = r; + boolean done = false; // done reading responses? + notifyResponseHandlers(responses); + + if (r.isBYE()) // shouldn't wait for command completion response + done = true; + + // If this is a matching command completion response, we are done + if (r.isTagged() && r.getTag().equals(idleTag)) + done = true; + + if (done) + idleTag = null; // no longer in IDLE + + handleResult(r); + return !done; + } + + // the DONE command to break out of IDLE + private static final byte[] DONE = { 'D', 'O', 'N', 'E', '\r', '\n' }; + + /** + * Abort an IDLE command. While one thread is blocked in + * readIdleResponse(), another thread will use this method + * to abort the IDLE command, which will cause the server + * to send the closing tag for the IDLE command, which + * readIdleResponse() and processIdleResponse() will see + * and terminate the IDLE state. + * + * @since JavaMail 1.4.1 + */ + public void idleAbort() { + OutputStream os = getOutputStream(); + try { + os.write(DONE); + os.flush(); + } catch (Exception ex) { + // nothing to do, hope to detect it again later + logger.log(Level.FINEST, "Exception aborting IDLE", ex); + } + } + + /** + * ID Command. + * + * @param clientParams map of names and values + * @return map of names and values from server + * @exception ProtocolException for protocol failures + * @see "RFC 2971" + * @since JavaMail 1.5.1 + */ + public Map<String, String> id(Map<String, String> clientParams) + throws ProtocolException { + if (!hasCapability("ID")) + throw new BadCommandException("ID not supported"); + + Response[] r = command("ID", ID.getArgumentList(clientParams)); + + ID id = null; + Response response = r[r.length-1]; + + // Grab ID response + if (response.isOK()) { // command succesful + for (int i = 0, len = r.length; i < len; i++) { + if (!(r[i] instanceof IMAPResponse)) + continue; + + IMAPResponse ir = (IMAPResponse)r[i]; + if (ir.keyEquals("ID")) { + if (id == null) + id = new ID(ir); + r[i] = null; + } + } + } + + // dispatch remaining untagged responses + notifyResponseHandlers(r); + handleResult(response); + return id == null ? null : id.getServerParams(); + } +}
diff --git a/mail/src/main/java/com/sun/mail/imap/protocol/IMAPReferralException.java b/mail/src/main/java/com/sun/mail/imap/protocol/IMAPReferralException.java new file mode 100644 index 0000000..82f1ea3 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/imap/protocol/IMAPReferralException.java
@@ -0,0 +1,53 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import com.sun.mail.iap.ProtocolException; + +/** + * A ProtocolException that includes IMAP login referral information. + * + * @since JavaMail 1.5.5 + */ + +public class IMAPReferralException extends ProtocolException { + + private String url; + + private static final long serialVersionUID = 2578770669364251968L; + + /** + * Constructs an IMAPReferralException with the specified detail message. + * and URL. + * + * @param s the detail message + * @param url the URL + */ + public IMAPReferralException(String s, String url) { + super(s); + this.url = url; + } + + /** + * Return the IMAP URL in the referral. + * + * @return the IMAP URL + */ + public String getUrl() { + return url; + } +}
diff --git a/mail/src/main/java/com/sun/mail/imap/protocol/IMAPResponse.java b/mail/src/main/java/com/sun/mail/imap/protocol/IMAPResponse.java new file mode 100644 index 0000000..4e6be4e --- /dev/null +++ b/mail/src/main/java/com/sun/mail/imap/protocol/IMAPResponse.java
@@ -0,0 +1,140 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import java.io.*; +import java.util.*; +import com.sun.mail.util.ASCIIUtility; +import com.sun.mail.iap.*; + +/** + * This class represents a response obtained from the input stream + * of an IMAP server. + * + * @author John Mani + */ + +public class IMAPResponse extends Response { + private String key; + private int number; + + public IMAPResponse(Protocol c) throws IOException, ProtocolException { + super(c); + init(); + } + + private void init() throws IOException, ProtocolException { + // continue parsing if this is an untagged response + if (isUnTagged() && !isOK() && !isNO() && !isBAD() && !isBYE()) { + key = readAtom(); + + // Is this response of the form "* <number> <command>" + try { + number = Integer.parseInt(key); + key = readAtom(); + } catch (NumberFormatException ne) { } + } + } + + /** + * Copy constructor. + * + * @param r the IMAPResponse to copy + */ + public IMAPResponse(IMAPResponse r) { + super((Response)r); + key = r.key; + number = r.number; + } + + /** + * For testing. + * + * @param r the response string + * @exception IOException for I/O errors + * @exception ProtocolException for protocol failures + */ + public IMAPResponse(String r) throws IOException, ProtocolException { + this(r, true); + } + + /** + * For testing. + * + * @param r the response string + * @param utf8 UTF-8 allowed? + * @exception IOException for I/O errors + * @exception ProtocolException for protocol failures + * @since JavaMail 1.6.0 + */ + public IMAPResponse(String r, boolean utf8) + throws IOException, ProtocolException { + super(r, utf8); + init(); + } + + /** + * Read a list of space-separated "flag-extension" sequences and + * return the list as a array of Strings. An empty list is returned + * as null. Each item is expected to be an atom, possibly preceeded + * by a backslash, but we aren't that strict; we just look for strings + * separated by spaces and terminated by a right paren. We assume items + * are always ASCII. + * + * @return the list items as a String array + */ + public String[] readSimpleList() { + skipSpaces(); + + if (buffer[index] != '(') // not what we expected + return null; + index++; // skip '(' + + List<String> v = new ArrayList<>(); + int start; + for (start = index; buffer[index] != ')'; index++) { + if (buffer[index] == ' ') { // got one item + v.add(ASCIIUtility.toString(buffer, start, index)); + start = index+1; // index gets incremented at the top + } + } + if (index > start) // get the last item + v.add(ASCIIUtility.toString(buffer, start, index)); + index++; // skip ')' + + int size = v.size(); + if (size > 0) + return v.toArray(new String[size]); + else // empty list + return null; + } + + public String getKey() { + return key; + } + + public boolean keyEquals(String k) { + if (key != null && key.equalsIgnoreCase(k)) + return true; + else + return false; + } + + public int getNumber() { + return number; + } +}
diff --git a/mail/src/main/java/com/sun/mail/imap/protocol/IMAPSaslAuthenticator.java b/mail/src/main/java/com/sun/mail/imap/protocol/IMAPSaslAuthenticator.java new file mode 100644 index 0000000..d4187e0 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/imap/protocol/IMAPSaslAuthenticator.java
@@ -0,0 +1,284 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import java.io.*; +import java.util.*; +import java.util.logging.Level; +import javax.security.sasl.*; +import javax.security.auth.callback.*; + +import com.sun.mail.iap.*; +import com.sun.mail.imap.*; +import com.sun.mail.util.PropUtil; +import com.sun.mail.util.MailLogger; +import com.sun.mail.util.ASCIIUtility; +import com.sun.mail.util.BASE64EncoderStream; +import com.sun.mail.util.BASE64DecoderStream; + +/** + * This class contains a single method that does authentication using + * SASL. This is in a separate class so that it can be compiled with + * J2SE 1.5. Eventually it should be merged into IMAPProtocol.java. + */ + +public class IMAPSaslAuthenticator implements SaslAuthenticator { + + private IMAPProtocol pr; + private String name; + private Properties props; + private MailLogger logger; + private String host; + + /* + * This is a hack to initialize the OAUTH SASL provider just before, + * and only if, we might need it. This avoids the need for the user + * to initialize it explicitly, or manually configure the security + * providers file. + */ + static { + try { + com.sun.mail.auth.OAuth2SaslClientFactory.init(); + } catch (Throwable t) { } + } + + public IMAPSaslAuthenticator(IMAPProtocol pr, String name, Properties props, + MailLogger logger, String host) { + this.pr = pr; + this.name = name; + this.props = props; + this.logger = logger; + this.host = host; + } + + @Override + public boolean authenticate(String[] mechs, final String realm, + final String authzid, final String u, + final String p) throws ProtocolException { + + synchronized (pr) { // authenticate method should be synchronized + List<Response> v = new ArrayList<>(); + String tag = null; + Response r = null; + boolean done = false; + if (logger.isLoggable(Level.FINE)) { + logger.fine("SASL Mechanisms:"); + for (int i = 0; i < mechs.length; i++) + logger.fine(" " + mechs[i]); + logger.fine(""); + } + + SaslClient sc; + CallbackHandler cbh = new CallbackHandler() { + @Override + public void handle(Callback[] callbacks) { + if (logger.isLoggable(Level.FINE)) + logger.fine("SASL callback length: " + callbacks.length); + for (int i = 0; i < callbacks.length; i++) { + if (logger.isLoggable(Level.FINE)) + logger.fine("SASL callback " + i + ": " + callbacks[i]); + if (callbacks[i] instanceof NameCallback) { + NameCallback ncb = (NameCallback)callbacks[i]; + ncb.setName(u); + } else if (callbacks[i] instanceof PasswordCallback) { + PasswordCallback pcb = (PasswordCallback)callbacks[i]; + pcb.setPassword(p.toCharArray()); + } else if (callbacks[i] instanceof RealmCallback) { + RealmCallback rcb = (RealmCallback)callbacks[i]; + rcb.setText(realm != null ? + realm : rcb.getDefaultText()); + } else if (callbacks[i] instanceof RealmChoiceCallback) { + RealmChoiceCallback rcb = + (RealmChoiceCallback)callbacks[i]; + if (realm == null) + rcb.setSelectedIndex(rcb.getDefaultChoice()); + else { + // need to find specified realm in list + String[] choices = rcb.getChoices(); + for (int k = 0; k < choices.length; k++) { + if (choices[k].equals(realm)) { + rcb.setSelectedIndex(k); + break; + } + } + } + } + } + } + }; + + try { + @SuppressWarnings("unchecked") + Map<String, ?> propsMap = (Map) props; + sc = Sasl.createSaslClient(mechs, authzid, name, host, + propsMap, cbh); + } catch (SaslException sex) { + logger.log(Level.FINE, "Failed to create SASL client", sex); + throw new UnsupportedOperationException(sex.getMessage(), sex); + } + if (sc == null) { + logger.fine("No SASL support"); + throw new UnsupportedOperationException("No SASL support"); + } + if (logger.isLoggable(Level.FINE)) + logger.fine("SASL client " + sc.getMechanismName()); + + try { + Argument args = new Argument(); + args.writeAtom(sc.getMechanismName()); + if (pr.hasCapability("SASL-IR") && sc.hasInitialResponse()) { + String irs; + byte[] ba = sc.evaluateChallenge(new byte[0]); + if (ba.length > 0) { + ba = BASE64EncoderStream.encode(ba); + irs = ASCIIUtility.toString(ba, 0, ba.length); + } else + irs = "="; + args.writeAtom(irs); + } + tag = pr.writeCommand("AUTHENTICATE", args); + } catch (Exception ex) { + logger.log(Level.FINE, "SASL AUTHENTICATE Exception", ex); + return false; + } + + OutputStream os = pr.getIMAPOutputStream(); // stream to IMAP server + + /* + * Wrap a BASE64Encoder around a ByteArrayOutputstream + * to craft b64 encoded username and password strings + * + * Note that the encoded bytes should be sent "as-is" to the + * server, *not* as literals or quoted-strings. + * + * Also note that unlike the B64 definition in MIME, CRLFs + * should *not* be inserted during the encoding process. So, I + * use Integer.MAX_VALUE (0x7fffffff (> 1G)) as the bytesPerLine, + * which should be sufficiently large ! + */ + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + byte[] CRLF = { (byte)'\r', (byte)'\n'}; + + // Hack for Novell GroupWise XGWTRUSTEDAPP authentication mechanism + // http://www.novell.com/developer/documentation/gwimap/? + // page=/developer/documentation/gwimap/gwimpenu/data/al7te9j.html + boolean isXGWTRUSTEDAPP = + sc.getMechanismName().equals("XGWTRUSTEDAPP") && + PropUtil.getBooleanProperty(props, + "mail." + name + ".sasl.xgwtrustedapphack.enable", true); + while (!done) { // loop till we are done + try { + r = pr.readResponse(); + if (r.isContinuation()) { + byte[] ba = null; + if (!sc.isComplete()) { + ba = r.readByteArray().getNewBytes(); + if (ba.length > 0) + ba = BASE64DecoderStream.decode(ba); + if (logger.isLoggable(Level.FINE)) + logger.fine("SASL challenge: " + + ASCIIUtility.toString(ba, 0, ba.length) + " :"); + ba = sc.evaluateChallenge(ba); + } + if (ba == null) { + logger.fine("SASL no response"); + os.write(CRLF); // write out empty line + os.flush(); // flush the stream + bos.reset(); // reset buffer + } else { + if (logger.isLoggable(Level.FINE)) + logger.fine("SASL response: " + + ASCIIUtility.toString(ba, 0, ba.length) + " :"); + ba = BASE64EncoderStream.encode(ba); + if (isXGWTRUSTEDAPP) + bos.write(ASCIIUtility.getBytes("XGWTRUSTEDAPP ")); + bos.write(ba); + + bos.write(CRLF); // CRLF termination + os.write(bos.toByteArray()); // write out line + os.flush(); // flush the stream + bos.reset(); // reset buffer + } + } else if (r.isTagged() && r.getTag().equals(tag)) + // Ah, our tagged response + done = true; + else if (r.isBYE()) // outta here + done = true; + else // hmm .. unsolicited response here ?! + v.add(r); + } catch (Exception ioex) { + logger.log(Level.FINE, "SASL Exception", ioex); + // convert this into a BYE response + r = Response.byeResponse(ioex); + done = true; + // XXX - ultimately return true??? + } + } + + if (sc.isComplete() /*&& res.status == SUCCESS*/) { + String qop = (String)sc.getNegotiatedProperty(Sasl.QOP); + if (qop != null && (qop.equalsIgnoreCase("auth-int") || + qop.equalsIgnoreCase("auth-conf"))) { + // XXX - NOT SUPPORTED!!! + logger.fine( + "SASL Mechanism requires integrity or confidentiality"); + return false; + } + } + + Response[] responses = v.toArray(new Response[v.size()]); + + // handle an illegal but not uncommon untagged CAPABILTY response + pr.handleCapabilityResponse(responses); + + /* + * Dispatch untagged responses. + * NOTE: in our current upper level IMAP classes, we add the + * responseHandler to the Protocol object only *after* the + * connection has been authenticated. So, for now, the below + * code really ends up being just a no-op. + */ + pr.notifyResponseHandlers(responses); + + // Handle the final OK, NO, BAD or BYE response + pr.handleLoginResult(r); + pr.setCapabilities(r); + + /* + * If we're using the Novell Groupwise XGWTRUSTEDAPP mechanism + * to run as a specified authorization ID, we have to issue a + * LOGIN command to select the user we want to operate as. + */ + if (isXGWTRUSTEDAPP && authzid != null) { + Argument args = new Argument(); + args.writeString(authzid); + + responses = pr.command("LOGIN", args); + + // dispatch untagged responses + pr.notifyResponseHandlers(responses); + + // Handle result of this command + pr.handleResult(responses[responses.length-1]); + // If the response includes a CAPABILITY response code, process it + pr.setCapabilities(responses[responses.length-1]); + } + return true; + } + } +}
diff --git a/mail/src/main/java/com/sun/mail/imap/protocol/INTERNALDATE.java b/mail/src/main/java/com/sun/mail/imap/protocol/INTERNALDATE.java new file mode 100644 index 0000000..7f432ff --- /dev/null +++ b/mail/src/main/java/com/sun/mail/imap/protocol/INTERNALDATE.java
@@ -0,0 +1,128 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import java.util.Date; +import java.util.TimeZone; +import java.util.Locale; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.text.FieldPosition; + +import javax.mail.internet.MailDateFormat; + +import com.sun.mail.iap.*; + + +/** + * An INTERNALDATE FETCH item. + * + * @author John Mani + */ + +public class INTERNALDATE implements Item { + + static final char[] name = + {'I','N','T','E','R','N','A','L','D','A','T','E'}; + public int msgno; + protected Date date; + + /* + * Used to parse dates only. The parse method is thread safe + * so we only need to create a single object for use by all + * instances. We depend on the fact that the MailDateFormat + * class will parse dates in INTERNALDATE format as well as + * dates in RFC 822 format. + */ + private static final MailDateFormat mailDateFormat = new MailDateFormat(); + + /** + * Constructor. + * + * @param r the FetchResponse + * @exception ParsingException for parsing failures + */ + public INTERNALDATE(FetchResponse r) throws ParsingException { + msgno = r.getNumber(); + r.skipSpaces(); + String s = r.readString(); + if (s == null) + throw new ParsingException("INTERNALDATE is NIL"); + try { + synchronized (mailDateFormat) { + date = mailDateFormat.parse(s); + } + } catch (ParseException pex) { + throw new ParsingException("INTERNALDATE parse error"); + } + } + + public Date getDate() { + return date; + } + + // INTERNALDATE formatter + + private static SimpleDateFormat df = + // Need Locale.US, the "MMM" field can produce unexpected values + // in non US locales ! + new SimpleDateFormat("dd-MMM-yyyy HH:mm:ss ", Locale.US); + + /** + * Format given Date object into INTERNALDATE string + * + * @param d the Date + * @return INTERNALDATE string + */ + public static String format(Date d) { + /* + * SimpleDateFormat objects aren't thread safe, so rather + * than create a separate such object for each request, + * we create one object and synchronize its use here + * so that only one thread is using it at a time. This + * trades off some potential concurrency for speed in the + * common case. + * + * This method is only used when formatting the date in a + * message that's being appended to a folder. + */ + StringBuffer sb = new StringBuffer(); + synchronized (df) { + df.format(d, sb, new FieldPosition(0)); + } + + // compute timezone offset string + TimeZone tz = TimeZone.getDefault(); + int offset = tz.getOffset(d.getTime()); // get offset from GMT + int rawOffsetInMins = offset / 60 / 1000; // offset from GMT in mins + if (rawOffsetInMins < 0) { + sb.append('-'); + rawOffsetInMins = (-rawOffsetInMins); + } else + sb.append('+'); + + int offsetInHrs = rawOffsetInMins / 60; + int offsetInMins = rawOffsetInMins % 60; + + sb.append(Character.forDigit((offsetInHrs/10), 10)); + sb.append(Character.forDigit((offsetInHrs%10), 10)); + sb.append(Character.forDigit((offsetInMins/10), 10)); + sb.append(Character.forDigit((offsetInMins%10), 10)); + + return sb.toString(); + } +}
diff --git a/mail/src/main/java/com/sun/mail/imap/protocol/Item.java b/mail/src/main/java/com/sun/mail/imap/protocol/Item.java new file mode 100644 index 0000000..8d604ab --- /dev/null +++ b/mail/src/main/java/com/sun/mail/imap/protocol/Item.java
@@ -0,0 +1,30 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +/** + * A tagging interface for all IMAP data items. + * Note that the "name" field of all IMAP items MUST be in uppercase. <p> + * + * See the BODY, BODYSTRUCTURE, ENVELOPE, FLAGS, INTERNALDATE, RFC822DATA, + * RFC822SIZE, and UID classes. + * + * @author John Mani + */ + +public interface Item { +}
diff --git a/mail/src/main/java/com/sun/mail/imap/protocol/ListInfo.java b/mail/src/main/java/com/sun/mail/imap/protocol/ListInfo.java new file mode 100644 index 0000000..0db1f02 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/imap/protocol/ListInfo.java
@@ -0,0 +1,79 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import java.util.List; +import java.util.ArrayList; + +import com.sun.mail.iap.*; + +/** + * A LIST response. + * + * @author John Mani + * @author Bill Shannon + */ + +public class ListInfo { + public String name = null; + public char separator = '/'; + public boolean hasInferiors = true; + public boolean canOpen = true; + public int changeState = INDETERMINATE; + public String[] attrs; + + public static final int CHANGED = 1; + public static final int UNCHANGED = 2; + public static final int INDETERMINATE = 3; + + public ListInfo(IMAPResponse r) throws ParsingException { + String[] s = r.readSimpleList(); + + List<String> v = new ArrayList<>(); // accumulate attributes + if (s != null) { + // non-empty attribute list + for (int i = 0; i < s.length; i++) { + if (s[i].equalsIgnoreCase("\\Marked")) + changeState = CHANGED; + else if (s[i].equalsIgnoreCase("\\Unmarked")) + changeState = UNCHANGED; + else if (s[i].equalsIgnoreCase("\\Noselect")) + canOpen = false; + else if (s[i].equalsIgnoreCase("\\Noinferiors")) + hasInferiors = false; + v.add(s[i]); + } + } + attrs = v.toArray(new String[v.size()]); + + r.skipSpaces(); + if (r.readByte() == '"') { + if ((separator = (char)r.readByte()) == '\\') + // escaped separator character + separator = (char)r.readByte(); + r.skip(1); // skip <"> + } else // NIL + r.skip(2); + + r.skipSpaces(); + name = r.readAtomString(); + + if (!r.supportsUtf8()) + // decode the name (using RFC2060's modified UTF7) + name = BASE64MailboxDecoder.decode(name); + } +}
diff --git a/mail/src/main/java/com/sun/mail/imap/protocol/MODSEQ.java b/mail/src/main/java/com/sun/mail/imap/protocol/MODSEQ.java new file mode 100644 index 0000000..f800569 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/imap/protocol/MODSEQ.java
@@ -0,0 +1,53 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import com.sun.mail.iap.*; + +/** + * This class represents the MODSEQ data item. + * + * @since JavaMail 1.5.1 + * @author Bill Shannon + */ + +public class MODSEQ implements Item { + + static final char[] name = {'M','O','D','S','E','Q'}; + public int seqnum; + + public long modseq; + + /** + * Constructor. + * + * @param r the FetchResponse + * @exception ParsingException for parsing failures + */ + public MODSEQ(FetchResponse r) throws ParsingException { + seqnum = r.getNumber(); + r.skipSpaces(); + + if (r.readByte() != '(') + throw new ParsingException("MODSEQ parse error"); + + modseq = r.readLong(); + + if (!r.isNextNonSpace(')')) + throw new ParsingException("MODSEQ parse error"); + } +}
diff --git a/mail/src/main/java/com/sun/mail/imap/protocol/MailboxInfo.java b/mail/src/main/java/com/sun/mail/imap/protocol/MailboxInfo.java new file mode 100644 index 0000000..ccff0b6 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/imap/protocol/MailboxInfo.java
@@ -0,0 +1,163 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import java.util.List; +import java.util.ArrayList; + +import javax.mail.Flags; + +import com.sun.mail.iap.*; + +/** + * Information collected when opening a mailbox. + * + * @author John Mani + * @author Bill Shannon + */ + +public class MailboxInfo { + /** The available flags. */ + public Flags availableFlags = null; + /** The permanent flags. */ + public Flags permanentFlags = null; + /** The total number of messages. */ + public int total = -1; + /** The number of recent messages. */ + public int recent = -1; + /** The first unseen message. */ + public int first = -1; + /** The UIDVALIDITY. */ + public long uidvalidity = -1; + /** The next UID value to be assigned. */ + public long uidnext = -1; + /** UIDs are not sticky. */ + public boolean uidNotSticky = false; // RFC 4315 + /** The highest MODSEQ value. */ + public long highestmodseq = -1; // RFC 4551 - CONDSTORE + /** Folder.READ_WRITE or Folder.READ_ONLY, set by IMAPProtocol. */ + public int mode; + /** VANISHED or FETCH responses received while opening the mailbox. */ + public List<IMAPResponse> responses; + + /** + * Collect the information about this mailbox from the + * responses to a SELECT or EXAMINE. + * + * @param r the responses + * @exception ParsingException for errors parsing the responses + */ + public MailboxInfo(Response[] r) throws ParsingException { + for (int i = 0; i < r.length; i++) { + if (r[i] == null || !(r[i] instanceof IMAPResponse)) + continue; + + IMAPResponse ir = (IMAPResponse)r[i]; + + if (ir.keyEquals("EXISTS")) { + total = ir.getNumber(); + r[i] = null; // remove this response + } else if (ir.keyEquals("RECENT")) { + recent = ir.getNumber(); + r[i] = null; // remove this response + } else if (ir.keyEquals("FLAGS")) { + availableFlags = new FLAGS(ir); + r[i] = null; // remove this response + } else if (ir.keyEquals("VANISHED")) { + if (responses == null) + responses = new ArrayList<>(); + responses.add(ir); + r[i] = null; // remove this response + } else if (ir.keyEquals("FETCH")) { + if (responses == null) + responses = new ArrayList<>(); + responses.add(ir); + r[i] = null; // remove this response + } else if (ir.isUnTagged() && ir.isOK()) { + /* + * should be one of: + * * OK [UNSEEN 12] + * * OK [UIDVALIDITY 3857529045] + * * OK [PERMANENTFLAGS (\Deleted)] + * * OK [UIDNEXT 44] + * * OK [HIGHESTMODSEQ 103] + */ + ir.skipSpaces(); + + if (ir.readByte() != '[') { // huh ??? + ir.reset(); + continue; + } + + boolean handled = true; + String s = ir.readAtom(); + if (s.equalsIgnoreCase("UNSEEN")) + first = ir.readNumber(); + else if (s.equalsIgnoreCase("UIDVALIDITY")) + uidvalidity = ir.readLong(); + else if (s.equalsIgnoreCase("PERMANENTFLAGS")) + permanentFlags = new FLAGS(ir); + else if (s.equalsIgnoreCase("UIDNEXT")) + uidnext = ir.readLong(); + else if (s.equalsIgnoreCase("HIGHESTMODSEQ")) + highestmodseq = ir.readLong(); + else + handled = false; // possibly an ALERT + + if (handled) + r[i] = null; // remove this response + else + ir.reset(); // so ALERT can be read + } else if (ir.isUnTagged() && ir.isNO()) { + /* + * should be one of: + * * NO [UIDNOTSTICKY] + */ + ir.skipSpaces(); + + if (ir.readByte() != '[') { // huh ??? + ir.reset(); + continue; + } + + boolean handled = true; + String s = ir.readAtom(); + if (s.equalsIgnoreCase("UIDNOTSTICKY")) + uidNotSticky = true; + else + handled = false; // possibly an ALERT + + if (handled) + r[i] = null; // remove this response + else + ir.reset(); // so ALERT can be read + } + } + + /* + * The PERMANENTFLAGS response code is optional, and if + * not present implies that all flags in the required FLAGS + * response can be changed permanently. + */ + if (permanentFlags == null) { + if (availableFlags != null) + permanentFlags = new Flags(availableFlags); + else + permanentFlags = new Flags(); + } + } +}
diff --git a/mail/src/main/java/com/sun/mail/imap/protocol/MessageSet.java b/mail/src/main/java/com/sun/mail/imap/protocol/MessageSet.java new file mode 100644 index 0000000..a7e6cc6 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/imap/protocol/MessageSet.java
@@ -0,0 +1,120 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import java.util.List; +import java.util.ArrayList; + +/** + * This class holds the 'start' and 'end' for a range of messages. + */ +public class MessageSet { + + public int start; + public int end; + + public MessageSet() { } + + public MessageSet(int start, int end) { + this.start = start; + this.end = end; + } + + /** + * Count the total number of elements in a MessageSet + * + * @return how many messages in this MessageSet + */ + public int size() { + return end - start + 1; + } + + /** + * Convert an array of integers into an array of MessageSets + * + * @param msgs the messages + * @return array of MessageSet objects + */ + public static MessageSet[] createMessageSets(int[] msgs) { + List<MessageSet> v = new ArrayList<>(); + int i,j; + + for (i=0; i < msgs.length; i++) { + MessageSet ms = new MessageSet(); + ms.start = msgs[i]; + + // Look for contiguous elements + for (j=i+1; j < msgs.length; j++) { + if (msgs[j] != msgs[j-1] +1) + break; + } + ms.end = msgs[j-1]; + v.add(ms); + i = j-1; // i gets incremented @ top of the loop + } + return v.toArray(new MessageSet[v.size()]); + } + + /** + * Convert an array of MessageSets into an IMAP sequence range + * + * @param msgsets the MessageSets + * @return IMAP sequence string + */ + public static String toString(MessageSet[] msgsets) { + if (msgsets == null || msgsets.length == 0) // Empty msgset + return null; + + int i = 0; // msgset index + StringBuilder s = new StringBuilder(); + int size = msgsets.length; + int start, end; + + for (;;) { + start = msgsets[i].start; + end = msgsets[i].end; + + if (end > start) + s.append(start).append(':').append(end); + else // end == start means only one element + s.append(start); + + i++; // Next MessageSet + if (i >= size) // No more MessageSets + break; + else + s.append(','); + } + return s.toString(); + } + + + /* + * Count the total number of elements in an array of MessageSets + */ + public static int size(MessageSet[] msgsets) { + int count = 0; + + if (msgsets == null) // Null msgset + return 0; + + for (int i=0; i < msgsets.length; i++) + count += msgsets[i].size(); + + return count; + } +}
diff --git a/mail/src/main/java/com/sun/mail/imap/protocol/Namespaces.java b/mail/src/main/java/com/sun/mail/imap/protocol/Namespaces.java new file mode 100644 index 0000000..df54e67 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/imap/protocol/Namespaces.java
@@ -0,0 +1,147 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import java.util.*; +import com.sun.mail.iap.*; + +/** + * This class and its inner class represent the response to the + * NAMESPACE command. <p> + * + * See <A HREF="http://www.ietf.org/rfc/rfc2342.txt">RFC 2342</A>. + * + * @author Bill Shannon + */ + +public class Namespaces { + + /** + * A single namespace entry. + */ + public static class Namespace { + /** + * Prefix string for the namespace. + */ + public String prefix; + + /** + * Delimiter between names in this namespace. + */ + public char delimiter; + + /** + * Parse a namespace element out of the response. + * + * @param r the Response to parse + * @exception ProtocolException for any protocol errors + */ + public Namespace(Response r) throws ProtocolException { + // Namespace_Element = "(" string SP (<"> QUOTED_CHAR <"> / nil) + // *(Namespace_Response_Extension) ")" + if (!r.isNextNonSpace('(')) + throw new ProtocolException( + "Missing '(' at start of Namespace"); + // first, the prefix + prefix = r.readString(); + if (!r.supportsUtf8()) + prefix = BASE64MailboxDecoder.decode(prefix); + r.skipSpaces(); + // delimiter is a quoted character or NIL + if (r.peekByte() == '"') { + r.readByte(); + delimiter = (char)r.readByte(); + if (delimiter == '\\') + delimiter = (char)r.readByte(); + if (r.readByte() != '"') + throw new ProtocolException( + "Missing '\"' at end of QUOTED_CHAR"); + } else { + String s = r.readAtom(); + if (s == null) + throw new ProtocolException("Expected NIL, got null"); + if (!s.equalsIgnoreCase("NIL")) + throw new ProtocolException("Expected NIL, got " + s); + delimiter = 0; + } + // at end of Namespace data? + if (r.isNextNonSpace(')')) + return; + + // otherwise, must be a Namespace_Response_Extension + // Namespace_Response_Extension = SP string SP + // "(" string *(SP string) ")" + r.readString(); + r.skipSpaces(); + r.readStringList(); + if (!r.isNextNonSpace(')')) + throw new ProtocolException("Missing ')' at end of Namespace"); + } + }; + + /** + * The personal namespaces. + * May be null. + */ + public Namespace[] personal; + + /** + * The namespaces for other users. + * May be null. + */ + public Namespace[] otherUsers; + + /** + * The shared namespace. + * May be null. + */ + public Namespace[] shared; + + /** + * Parse out all the namespaces. + * + * @param r the Response to parse + * @throws ProtocolException for any protocol errors + */ + public Namespaces(Response r) throws ProtocolException { + personal = getNamespaces(r); + otherUsers = getNamespaces(r); + shared = getNamespaces(r); + } + + /** + * Parse out one of the three sets of namespaces. + */ + private Namespace[] getNamespaces(Response r) throws ProtocolException { + // Namespace = nil / "(" 1*( Namespace_Element) ")" + if (r.isNextNonSpace('(')) { + List<Namespace> v = new ArrayList<>(); + do { + Namespace ns = new Namespace(r); + v.add(ns); + } while (!r.isNextNonSpace(')')); + return v.toArray(new Namespace[v.size()]); + } else { + String s = r.readAtom(); + if (s == null) + throw new ProtocolException("Expected NIL, got null"); + if (!s.equalsIgnoreCase("NIL")) + throw new ProtocolException("Expected NIL, got " + s); + return null; + } + } +}
diff --git a/mail/src/main/java/com/sun/mail/imap/protocol/RFC822DATA.java b/mail/src/main/java/com/sun/mail/imap/protocol/RFC822DATA.java new file mode 100644 index 0000000..bda754f --- /dev/null +++ b/mail/src/main/java/com/sun/mail/imap/protocol/RFC822DATA.java
@@ -0,0 +1,76 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import java.io.ByteArrayInputStream; +import com.sun.mail.iap.*; +import com.sun.mail.util.ASCIIUtility; + +/** + * The RFC822 response data item. + * + * @author John Mani + * @author Bill Shannon + */ + +public class RFC822DATA implements Item { + + static final char[] name = {'R','F','C','8','2','2'}; + private final int msgno; + private final ByteArray data; + private final boolean isHeader; + + /** + * Constructor, header flag is false. + * + * @param r the FetchResponse + * @exception ParsingException for parsing failures + */ + public RFC822DATA(FetchResponse r) throws ParsingException { + this(r, false); + } + + /** + * Constructor, specifying header flag. + * + * @param r the FetchResponse + * @param isHeader just header information? + * @exception ParsingException for parsing failures + */ + public RFC822DATA(FetchResponse r, boolean isHeader) + throws ParsingException { + this.isHeader = isHeader; + msgno = r.getNumber(); + r.skipSpaces(); + data = r.readByteArray(); + } + + public ByteArray getByteArray() { + return data; + } + + public ByteArrayInputStream getByteArrayInputStream() { + if (data != null) + return data.toByteArrayInputStream(); + else + return null; + } + + public boolean isHeader() { + return isHeader; + } +}
diff --git a/mail/src/main/java/com/sun/mail/imap/protocol/RFC822SIZE.java b/mail/src/main/java/com/sun/mail/imap/protocol/RFC822SIZE.java new file mode 100644 index 0000000..59118f1 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/imap/protocol/RFC822SIZE.java
@@ -0,0 +1,45 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import com.sun.mail.iap.*; + +/** + * An RFC822SIZE FETCH item. + * + * @author John Mani + */ + +public class RFC822SIZE implements Item { + + static final char[] name = {'R','F','C','8','2','2','.','S','I','Z','E'}; + public int msgno; + + public long size; + + /** + * Constructor. + * + * @param r the FetchResponse + * @exception ParsingException for parsing failures + */ + public RFC822SIZE(FetchResponse r) throws ParsingException { + msgno = r.getNumber(); + r.skipSpaces(); + size = r.readLong(); + } +}
diff --git a/mail/src/main/java/com/sun/mail/imap/protocol/SaslAuthenticator.java b/mail/src/main/java/com/sun/mail/imap/protocol/SaslAuthenticator.java new file mode 100644 index 0000000..4c31c76 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/imap/protocol/SaslAuthenticator.java
@@ -0,0 +1,29 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import com.sun.mail.iap.ProtocolException; + +/** + * Interface to make it easier to call IMAPSaslAuthenticator. + */ + +public interface SaslAuthenticator { + public boolean authenticate(String[] mechs, String realm, String authzid, + String u, String p) throws ProtocolException; + +}
diff --git a/mail/src/main/java/com/sun/mail/imap/protocol/SearchSequence.java b/mail/src/main/java/com/sun/mail/imap/protocol/SearchSequence.java new file mode 100644 index 0000000..c138837 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/imap/protocol/SearchSequence.java
@@ -0,0 +1,527 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import java.util.*; +import java.io.IOException; + +import javax.mail.*; +import javax.mail.search.*; +import com.sun.mail.iap.*; +import com.sun.mail.imap.OlderTerm; +import com.sun.mail.imap.YoungerTerm; +import com.sun.mail.imap.ModifiedSinceTerm; + +/** + * This class traverses a search-tree and generates the + * corresponding IMAP search sequence. + * + * Each IMAPProtocol instance contains an instance of this class, + * which might be subclassed by subclasses of IMAPProtocol to add + * support for additional product-specific search terms. + * + * @author John Mani + * @author Bill Shannon + */ +public class SearchSequence { + + private IMAPProtocol protocol; // for hasCapability checks; may be null + + /** + * Create a SearchSequence for this IMAPProtocol. + * + * @param p the IMAPProtocol object for the server + * @since JavaMail 1.6.0 + */ + public SearchSequence(IMAPProtocol p) { + protocol = p; + } + + /** + * Create a SearchSequence. + */ + @Deprecated + public SearchSequence() { + } + + /** + * Generate the IMAP search sequence for the given search expression. + * + * @param term the search term + * @param charset charset for the search + * @return the SEARCH Argument + * @exception SearchException for failures + * @exception IOException for I/O errors + */ + public Argument generateSequence(SearchTerm term, String charset) + throws SearchException, IOException { + /* + * Call the appropriate handler depending on the type of + * the search-term ... + */ + if (term instanceof AndTerm) // AND + return and((AndTerm)term, charset); + else if (term instanceof OrTerm) // OR + return or((OrTerm)term, charset); + else if (term instanceof NotTerm) // NOT + return not((NotTerm)term, charset); + else if (term instanceof HeaderTerm) // HEADER + return header((HeaderTerm)term, charset); + else if (term instanceof FlagTerm) // FLAG + return flag((FlagTerm)term); + else if (term instanceof FromTerm) { // FROM + FromTerm fterm = (FromTerm)term; + return from(fterm.getAddress().toString(), charset); + } + else if (term instanceof FromStringTerm) { // FROM + FromStringTerm fterm = (FromStringTerm)term; + return from(fterm.getPattern(), charset); + } + else if (term instanceof RecipientTerm) { // RECIPIENT + RecipientTerm rterm = (RecipientTerm)term; + return recipient(rterm.getRecipientType(), + rterm.getAddress().toString(), + charset); + } + else if (term instanceof RecipientStringTerm) { // RECIPIENT + RecipientStringTerm rterm = (RecipientStringTerm)term; + return recipient(rterm.getRecipientType(), + rterm.getPattern(), + charset); + } + else if (term instanceof SubjectTerm) // SUBJECT + return subject((SubjectTerm)term, charset); + else if (term instanceof BodyTerm) // BODY + return body((BodyTerm)term, charset); + else if (term instanceof SizeTerm) // SIZE + return size((SizeTerm)term); + else if (term instanceof SentDateTerm) // SENTDATE + return sentdate((SentDateTerm)term); + else if (term instanceof ReceivedDateTerm) // INTERNALDATE + return receiveddate((ReceivedDateTerm)term); + else if (term instanceof OlderTerm) // RFC 5032 OLDER + return older((OlderTerm)term); + else if (term instanceof YoungerTerm) // RFC 5032 YOUNGER + return younger((YoungerTerm)term); + else if (term instanceof MessageIDTerm) // MessageID + return messageid((MessageIDTerm)term, charset); + else if (term instanceof ModifiedSinceTerm) // RFC 4551 MODSEQ + return modifiedSince((ModifiedSinceTerm)term); + else + throw new SearchException("Search too complex"); + } + + /** + * Check if the "text" terms in the given SearchTerm contain + * non US-ASCII characters. + * + * @param term the search term + * @return true if only ASCII + */ + public static boolean isAscii(SearchTerm term) { + if (term instanceof AndTerm) + return isAscii(((AndTerm)term).getTerms()); + else if (term instanceof OrTerm) + return isAscii(((OrTerm)term).getTerms()); + else if (term instanceof NotTerm) + return isAscii(((NotTerm)term).getTerm()); + else if (term instanceof StringTerm) + return isAscii(((StringTerm)term).getPattern()); + else if (term instanceof AddressTerm) + return isAscii(((AddressTerm)term).getAddress().toString()); + + // Any other term returns true. + return true; + } + + /** + * Check if any of the "text" terms in the given SearchTerms contain + * non US-ASCII characters. + * + * @param terms the search terms + * @return true if only ASCII + */ + public static boolean isAscii(SearchTerm[] terms) { + for (int i = 0; i < terms.length; i++) + if (!isAscii(terms[i])) // outta here ! + return false; + return true; + } + + /** + * Does this string contain only ASCII characters? + * + * @param s the string + * @return true if only ASCII + */ + public static boolean isAscii(String s) { + int l = s.length(); + + for (int i=0; i < l; i++) { + if ((int)s.charAt(i) > 0177) // non-ascii + return false; + } + return true; + } + + protected Argument and(AndTerm term, String charset) + throws SearchException, IOException { + // Combine the sequences for both terms + SearchTerm[] terms = term.getTerms(); + // Generate the search sequence for the first term + Argument result = generateSequence(terms[0], charset); + // Append other terms + for (int i = 1; i < terms.length; i++) + result.append(generateSequence(terms[i], charset)); + return result; + } + + protected Argument or(OrTerm term, String charset) + throws SearchException, IOException { + SearchTerm[] terms = term.getTerms(); + + /* The IMAP OR operator takes only two operands. So if + * we have more than 2 operands, group them into 2-operand + * OR Terms. + */ + if (terms.length > 2) { + SearchTerm t = terms[0]; + + // Include rest of the terms + for (int i = 1; i < terms.length; i++) + t = new OrTerm(t, terms[i]); + + term = (OrTerm)t; // set 'term' to the new jumbo OrTerm we + // just created + terms = term.getTerms(); + } + + // 'term' now has only two operands + Argument result = new Argument(); + + // Add the OR search-key, if more than one term + if (terms.length > 1) + result.writeAtom("OR"); + + /* If this term is an AND expression, we need to enclose it + * within paranthesis. + * + * AND expressions are either AndTerms or FlagTerms + */ + if (terms[0] instanceof AndTerm || terms[0] instanceof FlagTerm) + result.writeArgument(generateSequence(terms[0], charset)); + else + result.append(generateSequence(terms[0], charset)); + + // Repeat the above for the second term, if there is one + if (terms.length > 1) { + if (terms[1] instanceof AndTerm || terms[1] instanceof FlagTerm) + result.writeArgument(generateSequence(terms[1], charset)); + else + result.append(generateSequence(terms[1], charset)); + } + + return result; + } + + protected Argument not(NotTerm term, String charset) + throws SearchException, IOException { + Argument result = new Argument(); + + // Add the NOT search-key + result.writeAtom("NOT"); + + /* If this term is an AND expression, we need to enclose it + * within paranthesis. + * + * AND expressions are either AndTerms or FlagTerms + */ + SearchTerm nterm = term.getTerm(); + if (nterm instanceof AndTerm || nterm instanceof FlagTerm) + result.writeArgument(generateSequence(nterm, charset)); + else + result.append(generateSequence(nterm, charset)); + + return result; + } + + protected Argument header(HeaderTerm term, String charset) + throws SearchException, IOException { + Argument result = new Argument(); + result.writeAtom("HEADER"); + result.writeString(term.getHeaderName()); + result.writeString(term.getPattern(), charset); + return result; + } + + protected Argument messageid(MessageIDTerm term, String charset) + throws SearchException, IOException { + Argument result = new Argument(); + result.writeAtom("HEADER"); + result.writeString("Message-ID"); + // XXX confirm that charset conversion ought to be done + result.writeString(term.getPattern(), charset); + return result; + } + + protected Argument flag(FlagTerm term) throws SearchException { + boolean set = term.getTestSet(); + + Argument result = new Argument(); + + Flags flags = term.getFlags(); + Flags.Flag[] sf = flags.getSystemFlags(); + String[] uf = flags.getUserFlags(); + if (sf.length == 0 && uf.length == 0) + throw new SearchException("Invalid FlagTerm"); + + for (int i = 0; i < sf.length; i++) { + if (sf[i] == Flags.Flag.DELETED) + result.writeAtom(set ? "DELETED": "UNDELETED"); + else if (sf[i] == Flags.Flag.ANSWERED) + result.writeAtom(set ? "ANSWERED": "UNANSWERED"); + else if (sf[i] == Flags.Flag.DRAFT) + result.writeAtom(set ? "DRAFT": "UNDRAFT"); + else if (sf[i] == Flags.Flag.FLAGGED) + result.writeAtom(set ? "FLAGGED": "UNFLAGGED"); + else if (sf[i] == Flags.Flag.RECENT) + result.writeAtom(set ? "RECENT": "OLD"); + else if (sf[i] == Flags.Flag.SEEN) + result.writeAtom(set ? "SEEN": "UNSEEN"); + } + + for (int i = 0; i < uf.length; i++) { + result.writeAtom(set ? "KEYWORD" : "UNKEYWORD"); + result.writeAtom(uf[i]); + } + + return result; + } + + protected Argument from(String address, String charset) + throws SearchException, IOException { + Argument result = new Argument(); + result.writeAtom("FROM"); + result.writeString(address, charset); + return result; + } + + protected Argument recipient(Message.RecipientType type, + String address, String charset) + throws SearchException, IOException { + Argument result = new Argument(); + + if (type == Message.RecipientType.TO) + result.writeAtom("TO"); + else if (type == Message.RecipientType.CC) + result.writeAtom("CC"); + else if (type == Message.RecipientType.BCC) + result.writeAtom("BCC"); + else + throw new SearchException("Illegal Recipient type"); + + result.writeString(address, charset); + return result; + } + + protected Argument subject(SubjectTerm term, String charset) + throws SearchException, IOException { + Argument result = new Argument(); + + result.writeAtom("SUBJECT"); + result.writeString(term.getPattern(), charset); + return result; + } + + protected Argument body(BodyTerm term, String charset) + throws SearchException, IOException { + Argument result = new Argument(); + + result.writeAtom("BODY"); + result.writeString(term.getPattern(), charset); + return result; + } + + protected Argument size(SizeTerm term) + throws SearchException { + Argument result = new Argument(); + + switch (term.getComparison()) { + case ComparisonTerm.GT: + result.writeAtom("LARGER"); + break; + case ComparisonTerm.LT: + result.writeAtom("SMALLER"); + break; + default: + // GT and LT is all we get from IMAP for size + throw new SearchException("Cannot handle Comparison"); + } + + result.writeNumber(term.getNumber()); + return result; + } + + // Date SEARCH stuff ... + + // NOTE: The built-in IMAP date comparisons are equivalent to + // "<" (BEFORE), "=" (ON), and ">=" (SINCE)!!! + // There is no built-in greater-than comparison! + + /** + * Print an IMAP Date string, that is suitable for the Date + * SEARCH commands. + * + * The IMAP Date string is : + * date ::= date_day "-" date_month "-" date_year + * + * Note that this format does not contain the TimeZone + */ + private static String monthTable[] = { + "Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" + }; + + // A GregorianCalendar object in the current timezone + protected Calendar cal = new GregorianCalendar(); + + protected String toIMAPDate(Date date) { + StringBuilder s = new StringBuilder(); + + cal.setTime(date); + + s.append(cal.get(Calendar.DATE)).append("-"); + s.append(monthTable[cal.get(Calendar.MONTH)]).append('-'); + s.append(cal.get(Calendar.YEAR)); + + return s.toString(); + } + + protected Argument sentdate(DateTerm term) + throws SearchException { + Argument result = new Argument(); + String date = toIMAPDate(term.getDate()); + + switch (term.getComparison()) { + case ComparisonTerm.GT: + result.writeAtom("NOT SENTON " + date + " SENTSINCE " + date); + break; + case ComparisonTerm.EQ: + result.writeAtom("SENTON " + date); + break; + case ComparisonTerm.LT: + result.writeAtom("SENTBEFORE " + date); + break; + case ComparisonTerm.GE: + result.writeAtom("SENTSINCE " + date); + break; + case ComparisonTerm.LE: + result.writeAtom("OR SENTBEFORE " + date + " SENTON " + date); + break; + case ComparisonTerm.NE: + result.writeAtom("NOT SENTON " + date); + break; + default: + throw new SearchException("Cannot handle Date Comparison"); + } + + return result; + } + + protected Argument receiveddate(DateTerm term) + throws SearchException { + Argument result = new Argument(); + String date = toIMAPDate(term.getDate()); + + switch (term.getComparison()) { + case ComparisonTerm.GT: + result.writeAtom("NOT ON " + date + " SINCE " + date); + break; + case ComparisonTerm.EQ: + result.writeAtom("ON " + date); + break; + case ComparisonTerm.LT: + result.writeAtom("BEFORE " + date); + break; + case ComparisonTerm.GE: + result.writeAtom("SINCE " + date); + break; + case ComparisonTerm.LE: + result.writeAtom("OR BEFORE " + date + " ON " + date); + break; + case ComparisonTerm.NE: + result.writeAtom("NOT ON " + date); + break; + default: + throw new SearchException("Cannot handle Date Comparison"); + } + + return result; + } + + /** + * Generate argument for OlderTerm. + * + * @param term the search term + * @return the SEARCH Argument + * @exception SearchException for failures + * @since JavaMail 1.5.1 + */ + protected Argument older(OlderTerm term) throws SearchException { + if (protocol != null && !protocol.hasCapability("WITHIN")) + throw new SearchException("Server doesn't support OLDER searches"); + Argument result = new Argument(); + result.writeAtom("OLDER"); + result.writeNumber(term.getInterval()); + return result; + } + + /** + * Generate argument for YoungerTerm. + * + * @param term the search term + * @return the SEARCH Argument + * @exception SearchException for failures + * @since JavaMail 1.5.1 + */ + protected Argument younger(YoungerTerm term) throws SearchException { + if (protocol != null && !protocol.hasCapability("WITHIN")) + throw new SearchException("Server doesn't support YOUNGER searches"); + Argument result = new Argument(); + result.writeAtom("YOUNGER"); + result.writeNumber(term.getInterval()); + return result; + } + + /** + * Generate argument for ModifiedSinceTerm. + * + * @param term the search term + * @return the SEARCH Argument + * @exception SearchException for failures + * @since JavaMail 1.5.1 + */ + protected Argument modifiedSince(ModifiedSinceTerm term) + throws SearchException { + if (protocol != null && !protocol.hasCapability("CONDSTORE")) + throw new SearchException("Server doesn't support MODSEQ searches"); + Argument result = new Argument(); + result.writeAtom("MODSEQ"); + result.writeNumber(term.getModSeq()); + return result; + } +}
diff --git a/mail/src/main/java/com/sun/mail/imap/protocol/Status.java b/mail/src/main/java/com/sun/mail/imap/protocol/Status.java new file mode 100644 index 0000000..76d29c3 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/imap/protocol/Status.java
@@ -0,0 +1,144 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import java.util.Map; +import java.util.HashMap; +import java.util.Locale; + +import com.sun.mail.iap.*; + +/** + * STATUS response. + * + * @author John Mani + * @author Bill Shannon + */ + +public class Status { + public String mbox = null; + public int total = -1; + public int recent = -1; + public long uidnext = -1; + public long uidvalidity = -1; + public int unseen = -1; + public long highestmodseq = -1; + public Map<String,Long> items; // any unknown items + + static final String[] standardItems = + { "MESSAGES", "RECENT", "UNSEEN", "UIDNEXT", "UIDVALIDITY" }; + + public Status(Response r) throws ParsingException { + // mailbox := astring + mbox = r.readAtomString(); + if (!r.supportsUtf8()) + mbox = BASE64MailboxDecoder.decode(mbox); + + // Workaround buggy IMAP servers that don't quote folder names + // with spaces. + final StringBuilder buffer = new StringBuilder(); + boolean onlySpaces = true; + + while (r.peekByte() != '(' && r.peekByte() != 0) { + final char next = (char)r.readByte(); + + buffer.append(next); + + if (next != ' ') { + onlySpaces = false; + } + } + + if (!onlySpaces) { + mbox = (mbox + buffer).trim(); + } + + if (r.readByte() != '(') + throw new ParsingException("parse error in STATUS"); + + do { + String attr = r.readAtom(); + if (attr == null) + throw new ParsingException("parse error in STATUS"); + if (attr.equalsIgnoreCase("MESSAGES")) + total = r.readNumber(); + else if (attr.equalsIgnoreCase("RECENT")) + recent = r.readNumber(); + else if (attr.equalsIgnoreCase("UIDNEXT")) + uidnext = r.readLong(); + else if (attr.equalsIgnoreCase("UIDVALIDITY")) + uidvalidity = r.readLong(); + else if (attr.equalsIgnoreCase("UNSEEN")) + unseen = r.readNumber(); + else if (attr.equalsIgnoreCase("HIGHESTMODSEQ")) + highestmodseq = r.readLong(); + else { + if (items == null) + items = new HashMap<>(); + items.put(attr.toUpperCase(Locale.ENGLISH), + Long.valueOf(r.readLong())); + } + } while (!r.isNextNonSpace(')')); + } + + /** + * Get the value for the STATUS item. + * + * @param item the STATUS item + * @return the value + * @since JavaMail 1.5.2 + */ + public long getItem(String item) { + item = item.toUpperCase(Locale.ENGLISH); + Long v; + long ret = -1; + if (items != null && (v = items.get(item)) != null) + ret = v.longValue(); + else if (item.equals("MESSAGES")) + ret = total; + else if (item.equals("RECENT")) + ret = recent; + else if (item.equals("UIDNEXT")) + ret = uidnext; + else if (item.equals("UIDVALIDITY")) + ret = uidvalidity; + else if (item.equals("UNSEEN")) + ret = unseen; + else if (item.equals("HIGHESTMODSEQ")) + ret = highestmodseq; + return ret; + } + + public static void add(Status s1, Status s2) { + if (s2.total != -1) + s1.total = s2.total; + if (s2.recent != -1) + s1.recent = s2.recent; + if (s2.uidnext != -1) + s1.uidnext = s2.uidnext; + if (s2.uidvalidity != -1) + s1.uidvalidity = s2.uidvalidity; + if (s2.unseen != -1) + s1.unseen = s2.unseen; + if (s2.highestmodseq != -1) + s1.highestmodseq = s2.highestmodseq; + if (s1.items == null) + s1.items = s2.items; + else if (s2.items != null) + s1.items.putAll(s2.items); + } +}
diff --git a/mail/src/main/java/com/sun/mail/imap/protocol/UID.java b/mail/src/main/java/com/sun/mail/imap/protocol/UID.java new file mode 100644 index 0000000..58082c2 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/imap/protocol/UID.java
@@ -0,0 +1,45 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import com.sun.mail.iap.*; + +/** + * This class represents the UID data item. + * + * @author John Mani + */ + +public class UID implements Item { + + static final char[] name = {'U','I','D'}; + public int seqnum; + + public long uid; + + /** + * Constructor. + * + * @param r the FetchResponse + * @exception ParsingException for parsing failures + */ + public UID(FetchResponse r) throws ParsingException { + seqnum = r.getNumber(); + r.skipSpaces(); + uid = r.readLong(); + } +}
diff --git a/mail/src/main/java/com/sun/mail/imap/protocol/UIDSet.java b/mail/src/main/java/com/sun/mail/imap/protocol/UIDSet.java new file mode 100644 index 0000000..2228581 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/imap/protocol/UIDSet.java
@@ -0,0 +1,235 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import java.util.List; +import java.util.ArrayList; +import java.util.StringTokenizer; + +/** + * This class holds the 'start' and 'end' for a range of UIDs. + * Just like MessageSet except using long instead of int. + */ +public class UIDSet { + + public long start; + public long end; + + public UIDSet() { } + + public UIDSet(long start, long end) { + this.start = start; + this.end = end; + } + + /** + * Count the total number of elements in a UIDSet + * + * @return the number of elements + */ + public long size() { + return end - start + 1; + } + + /** + * Convert an array of longs into an array of UIDSets + * + * @param uids the UIDs + * @return array of UIDSet objects + */ + public static UIDSet[] createUIDSets(long[] uids) { + if (uids == null) + return null; + List<UIDSet> v = new ArrayList<>(); + int i,j; + + for (i=0; i < uids.length; i++) { + UIDSet ms = new UIDSet(); + ms.start = uids[i]; + + // Look for contiguous elements + for (j=i+1; j < uids.length; j++) { + if (uids[j] != uids[j-1] +1) + break; + } + ms.end = uids[j-1]; + v.add(ms); + i = j-1; // i gets incremented @ top of the loop + } + UIDSet[] uidset = new UIDSet[v.size()]; + return v.toArray(uidset); + } + + /** + * Parse a string in IMAP UID range format. + * + * @param uids UID string + * @return array of UIDSet objects + * @since JavaMail 1.5.1 + */ + public static UIDSet[] parseUIDSets(String uids) { + if (uids == null) + return null; + List<UIDSet> v = new ArrayList<>(); + StringTokenizer st = new StringTokenizer(uids, ",:", true); + long start = -1; + UIDSet cur = null; + try { + while(st.hasMoreTokens()) { + String s = st.nextToken(); + if (s.equals(",")) { + if (cur != null) + v.add(cur); + cur = null; + } else if (s.equals(":")) { + // nothing to do, wait for next number + } else { // better be a number + long n = Long.parseLong(s); + if (cur != null) + cur.end = n; + else + cur = new UIDSet(n, n); + } + } + } catch (NumberFormatException nex) { + // give up and return what we have so far + } + if (cur != null) + v.add(cur); + UIDSet[] uidset = new UIDSet[v.size()]; + return v.toArray(uidset); + } + + /** + * Convert an array of UIDSets into an IMAP sequence range. + * + * @param uidset the UIDSets + * @return the IMAP sequence string + */ + public static String toString(UIDSet[] uidset) { + if (uidset == null) + return null; + if (uidset.length == 0) // Empty uidset + return ""; + + int i = 0; // uidset index + StringBuilder s = new StringBuilder(); + int size = uidset.length; + long start, end; + + for (;;) { + start = uidset[i].start; + end = uidset[i].end; + + if (end > start) + s.append(start).append(':').append(end); + else // end == start means only one element + s.append(start); + + i++; // Next UIDSet + if (i >= size) // No more UIDSets + break; + else + s.append(','); + } + return s.toString(); + } + + /** + * Convert an array of UIDSets into a array of long UIDs. + * + * @param uidset the UIDSets + * @return arrray of UIDs + * @since JavaMail 1.5.1 + */ + public static long[] toArray(UIDSet[] uidset) { + //return toArray(uidset, -1); + if (uidset == null) + return null; + long[] uids = new long[(int)UIDSet.size(uidset)]; + int i = 0; + for (UIDSet u : uidset) { + for (long n = u.start; n <= u.end; n++) + uids[i++] = n; + } + return uids; + } + + /** + * Convert an array of UIDSets into a array of long UIDs. + * Don't include any UIDs larger than uidmax. + * + * @param uidset the UIDSets + * @param uidmax maximum UID + * @return arrray of UIDs + * @since JavaMail 1.5.1 + */ + public static long[] toArray(UIDSet[] uidset, long uidmax) { + if (uidset == null) + return null; + long[] uids = new long[(int)UIDSet.size(uidset, uidmax)]; + int i = 0; + for (UIDSet u : uidset) { + for (long n = u.start; n <= u.end; n++) { + if (uidmax >= 0 && n > uidmax) + break; + uids[i++] = n; + } + } + return uids; + } + + /** + * Count the total number of elements in an array of UIDSets. + * + * @param uidset the UIDSets + * @return the number of elements + */ + public static long size(UIDSet[] uidset) { + long count = 0; + + if (uidset != null) + for (UIDSet u : uidset) + count += u.size(); + + return count; + } + + /** + * Count the total number of elements in an array of UIDSets. + * Don't count UIDs greater then uidmax. + * + * @since JavaMail 1.5.1 + */ + private static long size(UIDSet[] uidset, long uidmax) { + long count = 0; + + if (uidset != null) + for (UIDSet u : uidset) { + if (uidmax < 0) + count += u.size(); + else if (u.start <= uidmax) { + if (u.end < uidmax) + count += u.end - u.start + 1; + else + count += uidmax - u.start + 1; + } + } + + return count; + } +}
diff --git a/mail/src/main/java/com/sun/mail/imap/protocol/package.html b/mail/src/main/java/com/sun/mail/imap/protocol/package.html new file mode 100644 index 0000000..370726e --- /dev/null +++ b/mail/src/main/java/com/sun/mail/imap/protocol/package.html
@@ -0,0 +1,33 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> +<HTML> +<HEAD> +<meta http-equiv="Content-Type" content="text/html; charset=utf-8"> +<!-- + + Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<TITLE>com.sun.mail.imap.protocol package</TITLE> +</HEAD> +<BODY BGCOLOR="white"> + +<P> +This package includes internal IMAP support classes and +<strong>SHOULD NOT BE USED DIRECTLY BY APPLICATIONS</strong>. +</P> + +</BODY> +</HTML>
diff --git a/mail/src/main/java/com/sun/mail/pop3/AppendStream.java b/mail/src/main/java/com/sun/mail/pop3/AppendStream.java new file mode 100644 index 0000000..cc56ce3 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/pop3/AppendStream.java
@@ -0,0 +1,67 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.pop3; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.RandomAccessFile; + +/** + * A stream for writing to the temp file, and when done can return a stream for + * reading the data just written. NOTE: We assume that only one thread is + * writing to the file at a time. + */ +class AppendStream extends OutputStream { + + private final WritableSharedFile tf; + private RandomAccessFile raf; + private final long start; + private long end; + + public AppendStream(WritableSharedFile tf) throws IOException { + this.tf = tf; + raf = tf.getWritableFile(); + start = raf.length(); + raf.seek(start); + } + + @Override + public void write(int b) throws IOException { + raf.write(b); + } + + @Override + public void write(byte[] b) throws IOException { + raf.write(b); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + raf.write(b, off, len); + } + + @Override + public synchronized void close() throws IOException { + end = tf.updateLength(); + raf = null; // no more writing allowed + } + + public synchronized InputStream getInputStream() throws IOException { + return tf.newStream(start, end); + } +}
diff --git a/mail/src/main/java/com/sun/mail/pop3/DefaultFolder.java b/mail/src/main/java/com/sun/mail/pop3/DefaultFolder.java new file mode 100644 index 0000000..eaf9d63 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/pop3/DefaultFolder.java
@@ -0,0 +1,141 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.pop3; + +import javax.mail.*; + +/** + * The POP3 DefaultFolder. Only contains the "INBOX" folder. + * + * @author Christopher Cotton + */ +public class DefaultFolder extends Folder { + + DefaultFolder(POP3Store store) { + super(store); + } + + @Override + public String getName() { + return ""; + } + + @Override + public String getFullName() { + return ""; + } + + @Override + public Folder getParent() { + return null; + } + + @Override + public boolean exists() { + return true; + } + + @Override + public Folder[] list(String pattern) throws MessagingException { + Folder[] f = { getInbox() }; + return f; + } + + @Override + public char getSeparator() { + return '/'; + } + + @Override + public int getType() { + return HOLDS_FOLDERS; + } + + @Override + public boolean create(int type) throws MessagingException { + return false; + } + + @Override + public boolean hasNewMessages() throws MessagingException { + return false; + } + + @Override + public Folder getFolder(String name) throws MessagingException { + if (!name.equalsIgnoreCase("INBOX")) { + throw new MessagingException("only INBOX supported"); + } else { + return getInbox(); + } + } + + protected Folder getInbox() throws MessagingException { + return getStore().getFolder("INBOX"); + } + + + @Override + public boolean delete(boolean recurse) throws MessagingException { + throw new MethodNotSupportedException("delete"); + } + + @Override + public boolean renameTo(Folder f) throws MessagingException { + throw new MethodNotSupportedException("renameTo"); + } + + @Override + public void open(int mode) throws MessagingException { + throw new MethodNotSupportedException("open"); + } + + @Override + public void close(boolean expunge) throws MessagingException { + throw new MethodNotSupportedException("close"); + } + + @Override + public boolean isOpen() { + return false; + } + + @Override + public Flags getPermanentFlags() { + return new Flags(); // empty flags object + } + + @Override + public int getMessageCount() throws MessagingException { + return 0; + } + + @Override + public Message getMessage(int msgno) throws MessagingException { + throw new MethodNotSupportedException("getMessage"); + } + + @Override + public void appendMessages(Message[] msgs) throws MessagingException { + throw new MethodNotSupportedException("Append not supported"); + } + + @Override + public Message[] expunge() throws MessagingException { + throw new MethodNotSupportedException("expunge"); + } +}
diff --git a/mail/src/main/java/com/sun/mail/pop3/POP3Folder.java b/mail/src/main/java/com/sun/mail/pop3/POP3Folder.java new file mode 100644 index 0000000..a045a24 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/pop3/POP3Folder.java
@@ -0,0 +1,611 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.pop3; + +import javax.mail.*; +import javax.mail.event.*; +import java.io.InputStream; +import java.io.IOException; +import java.io.EOFException; +import java.util.StringTokenizer; +import java.util.logging.Level; +import java.lang.reflect.Constructor; + +import com.sun.mail.util.LineInputStream; +import com.sun.mail.util.MailLogger; +import java.util.ArrayList; +import java.util.List; + +/** + * A POP3 Folder (can only be "INBOX"). + * + * See the <a href="package-summary.html">com.sun.mail.pop3</a> package + * documentation for further information on the POP3 protocol provider. <p> + * + * @author Bill Shannon + * @author John Mani (ported to the javax.mail APIs) + */ +public class POP3Folder extends Folder { + + private String name; + private POP3Store store; + private volatile Protocol port; + private int total; + private int size; + private boolean exists = false; + private volatile boolean opened = false; + private POP3Message[] message_cache; + private boolean doneUidl = false; + private volatile TempFile fileCache = null; + private boolean forceClose; + + MailLogger logger; // package private, for POP3Message + + protected POP3Folder(POP3Store store, String name) { + super(store); + this.name = name; + this.store = store; + if (name.equalsIgnoreCase("INBOX")) + exists = true; + logger = new MailLogger(this.getClass(), "DEBUG POP3", + store.getSession().getDebug(), store.getSession().getDebugOut()); + } + + @Override + public String getName() { + return name; + } + + @Override + public String getFullName() { + return name; + } + + @Override + public Folder getParent() { + return new DefaultFolder(store); + } + + /** + * Always true for the folder "INBOX", always false for + * any other name. + * + * @return true for INBOX, false otherwise + */ + @Override + public boolean exists() { + return exists; + } + + /** + * Always throws <code>MessagingException</code> because no POP3 folders + * can contain subfolders. + * + * @exception MessagingException always + */ + @Override + public Folder[] list(String pattern) throws MessagingException { + throw new MessagingException("not a directory"); + } + + /** + * Always returns a NUL character because POP3 doesn't support a hierarchy. + * + * @return NUL + */ + @Override + public char getSeparator() { + return '\0'; + } + + /** + * Always returns Folder.HOLDS_MESSAGES. + * + * @return Folder.HOLDS_MESSAGES + */ + @Override + public int getType() { + return HOLDS_MESSAGES; + } + + /** + * Always returns <code>false</code>; the POP3 protocol doesn't + * support creating folders. + * + * @return false + */ + @Override + public boolean create(int type) throws MessagingException { + return false; + } + + /** + * Always returns <code>false</code>; the POP3 protocol provides + * no way to determine when a new message arrives. + * + * @return false + */ + @Override + public boolean hasNewMessages() throws MessagingException { + return false; // no way to know + } + + /** + * Always throws <code>MessagingException</code> because no POP3 folders + * can contain subfolders. + * + * @exception MessagingException always + */ + @Override + public Folder getFolder(String name) throws MessagingException { + throw new MessagingException("not a directory"); + } + + /** + * Always throws <code>MethodNotSupportedException</code> + * because the POP3 protocol doesn't allow the INBOX to + * be deleted. + * + * @exception MethodNotSupportedException always + */ + @Override + public boolean delete(boolean recurse) throws MessagingException { + throw new MethodNotSupportedException("delete"); + } + + /** + * Always throws <code>MethodNotSupportedException</code> + * because the POP3 protocol doesn't support multiple folders. + * + * @exception MethodNotSupportedException always + */ + @Override + public boolean renameTo(Folder f) throws MessagingException { + throw new MethodNotSupportedException("renameTo"); + } + + /** + * Throws <code>FolderNotFoundException</code> unless this + * folder is named "INBOX". + * + * @exception FolderNotFoundException if not INBOX + * @exception AuthenticationFailedException authentication failures + * @exception MessagingException other open failures + */ + @Override + public synchronized void open(int mode) throws MessagingException { + checkClosed(); + if (!exists) + throw new FolderNotFoundException(this, "folder is not INBOX"); + + try { + port = store.getPort(this); + Status s = port.stat(); + total = s.total; + size = s.size; + this.mode = mode; + if (store.useFileCache) { + try { + fileCache = new TempFile(store.fileCacheDir); + } catch (IOException ex) { + logger.log(Level.FINE, "failed to create file cache", ex); + throw ex; // caught below + } + } + opened = true; + } catch (IOException ioex) { + try { + if (port != null) + port.quit(); + } catch (IOException ioex2) { + // ignore + } finally { + port = null; + store.closePort(this); + } + throw new MessagingException("Open failed", ioex); + } + + // Create the message cache array of appropriate size + message_cache = new POP3Message[total]; + doneUidl = false; + + notifyConnectionListeners(ConnectionEvent.OPENED); + } + + @Override + public synchronized void close(boolean expunge) throws MessagingException { + checkOpen(); + + try { + /* + * Some POP3 servers will mark messages for deletion when + * they're read. To prevent such messages from being + * deleted before the client deletes them, you can set + * the mail.pop3.rsetbeforequit property to true. This + * causes us to issue a POP3 RSET command to clear all + * the "marked for deletion" flags. We can then explicitly + * delete messages as desired. + */ + if (store.rsetBeforeQuit && !forceClose) + port.rset(); + POP3Message m; + if (expunge && mode == READ_WRITE && !forceClose) { + // find all messages marked deleted and issue DELE commands + for (int i = 0; i < message_cache.length; i++) { + if ((m = message_cache[i]) != null) { + if (m.isSet(Flags.Flag.DELETED)) + try { + port.dele(i + 1); + } catch (IOException ioex) { + throw new MessagingException( + "Exception deleting messages during close", + ioex); + } + } + } + } + + /* + * Flush and free all cached data for the messages. + */ + for (int i = 0; i < message_cache.length; i++) { + if ((m = message_cache[i]) != null) + m.invalidate(true); + } + + if (forceClose) + port.close(); + else + port.quit(); + } catch (IOException ex) { + // do nothing + } finally { + port = null; + store.closePort(this); + message_cache = null; + opened = false; + notifyConnectionListeners(ConnectionEvent.CLOSED); + if (fileCache != null) { + fileCache.close(); + fileCache = null; + } + } + } + + @Override + public synchronized boolean isOpen() { + if (!opened) + return false; + try { + if (!port.noop()) + throw new IOException("NOOP failed"); + } catch (IOException ioex) { + try { + close(false); + } catch (MessagingException mex) { + // ignore it + } + return false; + } + return true; + } + + /** + * Always returns an empty <code>Flags</code> object because + * the POP3 protocol doesn't support any permanent flags. + * + * @return empty Flags object + */ + @Override + public Flags getPermanentFlags() { + return new Flags(); // empty flags object + } + + /** + * Will not change while the folder is open because the POP3 + * protocol doesn't support notification of new messages + * arriving in open folders. + */ + @Override + public synchronized int getMessageCount() throws MessagingException { + if (!opened) + return -1; + checkReadable(); + return total; + } + + @Override + public synchronized Message getMessage(int msgno) + throws MessagingException { + checkOpen(); + + POP3Message m; + + // Assuming that msgno is <= total + if ((m = message_cache[msgno-1]) == null) { + m = createMessage(this, msgno); + message_cache[msgno-1] = m; + } + return m; + } + + protected POP3Message createMessage(Folder f, int msgno) + throws MessagingException { + POP3Message m = null; + Constructor<?> cons = store.messageConstructor; + if (cons != null) { + try { + Object[] o = { this, Integer.valueOf(msgno) }; + m = (POP3Message)cons.newInstance(o); + } catch (Exception ex) { + // ignore + } + } + if (m == null) + m = new POP3Message(this, msgno); + return m; + } + + /** + * Always throws <code>MethodNotSupportedException</code> + * because the POP3 protocol doesn't support appending messages. + * + * @exception MethodNotSupportedException always + */ + @Override + public void appendMessages(Message[] msgs) throws MessagingException { + throw new MethodNotSupportedException("Append not supported"); + } + + /** + * Always throws <code>MethodNotSupportedException</code> + * because the POP3 protocol doesn't support expunging messages + * without closing the folder; call the {@link #close close} method + * with the <code>expunge</code> argument set to <code>true</code> + * instead. + * + * @exception MethodNotSupportedException always + */ + @Override + public Message[] expunge() throws MessagingException { + throw new MethodNotSupportedException("Expunge not supported"); + } + + /** + * Prefetch information about POP3 messages. + * If the FetchProfile contains <code>UIDFolder.FetchProfileItem.UID</code>, + * POP3 UIDs for all messages in the folder are fetched using the POP3 + * UIDL command. + * If the FetchProfile contains <code>FetchProfile.Item.ENVELOPE</code>, + * the headers and size of all messages are fetched using the POP3 TOP + * and LIST commands. + */ + @Override + public synchronized void fetch(Message[] msgs, FetchProfile fp) + throws MessagingException { + checkReadable(); + if (!doneUidl && store.supportsUidl && + fp.contains(UIDFolder.FetchProfileItem.UID)) { + /* + * Since the POP3 protocol only lets us fetch the UID + * for a single message or for all messages, we go ahead + * and fetch UIDs for all messages here, ignoring the msgs + * parameter. We could be more intelligent and base this + * decision on the number of messages fetched, or the + * percentage of the total number of messages fetched. + */ + String[] uids = new String[message_cache.length]; + try { + if (!port.uidl(uids)) + return; + } catch (EOFException eex) { + close(false); + throw new FolderClosedException(this, eex.toString()); + } catch (IOException ex) { + throw new MessagingException("error getting UIDL", ex); + } + for (int i = 0; i < uids.length; i++) { + if (uids[i] == null) + continue; + POP3Message m = (POP3Message)getMessage(i + 1); + m.uid = uids[i]; + } + doneUidl = true; // only do this once + } + if (fp.contains(FetchProfile.Item.ENVELOPE)) { + for (int i = 0; i < msgs.length; i++) { + try { + POP3Message msg = (POP3Message)msgs[i]; + // fetch headers + msg.getHeader(""); + // fetch message size + msg.getSize(); + } catch (MessageRemovedException mex) { + // should never happen, but ignore it if it does + } + } + } + } + + /** + * Return the unique ID string for this message, or null if + * not available. Uses the POP3 UIDL command. + * + * @param msg the message + * @return unique ID string + * @exception MessagingException for failures + */ + public synchronized String getUID(Message msg) throws MessagingException { + checkOpen(); + if (!(msg instanceof POP3Message)) + throw new MessagingException("message is not a POP3Message"); + POP3Message m = (POP3Message)msg; + try { + if (!store.supportsUidl) + return null; + if (m.uid == POP3Message.UNKNOWN) + m.uid = port.uidl(m.getMessageNumber()); + return m.uid; + } catch (EOFException eex) { + close(false); + throw new FolderClosedException(this, eex.toString()); + } catch (IOException ex) { + throw new MessagingException("error getting UIDL", ex); + } + } + + /** + * Return the size of this folder, as was returned by the POP3 STAT + * command when this folder was opened. + * + * @return folder size + * @exception IllegalStateException if the folder isn't open + * @exception MessagingException for other failures + */ + public synchronized int getSize() throws MessagingException { + checkOpen(); + return size; + } + + /** + * Return the sizes of all messages in this folder, as returned + * by the POP3 LIST command. Each entry in the array corresponds + * to a message; entry <i>i</i> corresponds to message number <i>i+1</i>. + * + * @return array of message sizes + * @exception IllegalStateException if the folder isn't open + * @exception MessagingException for other failures + * @since JavaMail 1.3.3 + */ + public synchronized int[] getSizes() throws MessagingException { + checkOpen(); + int sizes[] = new int[total]; + InputStream is = null; + LineInputStream lis = null; + try { + is = port.list(); + lis = new LineInputStream(is); + String line; + while ((line = lis.readLine()) != null) { + try { + StringTokenizer st = new StringTokenizer(line); + int msgnum = Integer.parseInt(st.nextToken()); + int size = Integer.parseInt(st.nextToken()); + if (msgnum > 0 && msgnum <= total) + sizes[msgnum - 1] = size; + } catch (RuntimeException e) { + } + } + } catch (IOException ex) { + // ignore it? + } finally { + try { + if (lis != null) + lis.close(); + } catch (IOException cex) { } + try { + if (is != null) + is.close(); + } catch (IOException cex) { } + } + return sizes; + } + + /** + * Return the raw results of the POP3 LIST command with no arguments. + * + * @return InputStream containing results + * @exception IllegalStateException if the folder isn't open + * @exception IOException for I/O errors talking to the server + * @exception MessagingException for other errors + * @since JavaMail 1.3.3 + */ + public synchronized InputStream listCommand() + throws MessagingException, IOException { + checkOpen(); + return port.list(); + } + + /** + * Close the folder when we're finalized. + */ + @Override + protected void finalize() throws Throwable { + forceClose = !store.finalizeCleanClose; + try { + if (opened) + close(false); + } finally { + super.finalize(); + forceClose = false; + } + } + + /* Ensure the folder is open */ + private void checkOpen() throws IllegalStateException { + if (!opened) + throw new IllegalStateException("Folder is not Open"); + } + + /* Ensure the folder is not open */ + private void checkClosed() throws IllegalStateException { + if (opened) + throw new IllegalStateException("Folder is Open"); + } + + /* Ensure the folder is open & readable */ + private void checkReadable() throws IllegalStateException { + if (!opened || (mode != READ_ONLY && mode != READ_WRITE)) + throw new IllegalStateException("Folder is not Readable"); + } + + /* Ensure the folder is open & writable */ + /* + private void checkWritable() throws IllegalStateException { + if (!opened || mode != READ_WRITE) + throw new IllegalStateException("Folder is not Writable"); + } + */ + + /** + * Centralize access to the Protocol object by POP3Message + * objects so that they will fail appropriately when the folder + * is closed. + */ + Protocol getProtocol() throws MessagingException { + Protocol p = port; // read it before close() can set it to null + checkOpen(); + // close() might happen here + return p; + } + + /* + * Only here to make accessible to POP3Message. + */ + @Override + protected void notifyMessageChangedListeners(int type, Message m) { + super.notifyMessageChangedListeners(type, m); + } + + /** + * Used by POP3Message. + */ + TempFile getFileCache() { + return fileCache; + } +}
diff --git a/mail/src/main/java/com/sun/mail/pop3/POP3Message.java b/mail/src/main/java/com/sun/mail/pop3/POP3Message.java new file mode 100644 index 0000000..7bc9c9f --- /dev/null +++ b/mail/src/main/java/com/sun/mail/pop3/POP3Message.java
@@ -0,0 +1,644 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.pop3; + +import java.io.*; +import java.util.Enumeration; +import java.util.logging.Level; +import java.lang.ref.SoftReference; +import javax.mail.*; +import javax.mail.internet.*; +import javax.mail.event.*; +import com.sun.mail.util.ReadableMime; + +/** + * A POP3 Message. Just like a MimeMessage except that + * some things are not supported. + * + * @author Bill Shannon + */ +public class POP3Message extends MimeMessage implements ReadableMime { + + /* + * Our locking strategy is to always lock the POP3Folder before the + * POP3Message so we have to be careful to drop our lock before calling + * back to the folder to close it and notify of connection lost events. + */ + + // flag to indicate we haven't tried to fetch the UID yet + static final String UNKNOWN = "UNKNOWN"; + + private POP3Folder folder; // overrides folder in MimeMessage + private int hdrSize = -1; + private int msgSize = -1; + String uid = UNKNOWN; // controlled by folder lock + + // rawData itself is never null + private SoftReference<InputStream> rawData + = new SoftReference<>(null); + + public POP3Message(Folder folder, int msgno) + throws MessagingException { + super(folder, msgno); + assert folder instanceof POP3Folder; + this.folder = (POP3Folder)folder; + } + + /** + * Set the specified flags on this message to the specified value. + * + * @param newFlags the flags to be set + * @param set the value to be set + */ + @Override + public synchronized void setFlags(Flags newFlags, boolean set) + throws MessagingException { + Flags oldFlags = (Flags)flags.clone(); + super.setFlags(newFlags, set); + if (!flags.equals(oldFlags)) + folder.notifyMessageChangedListeners( + MessageChangedEvent.FLAGS_CHANGED, this); + } + + /** + * Return the size of the content of this message in bytes. + * Returns -1 if the size cannot be determined. <p> + * + * Note that this number may not be an exact measure of the + * content size and may or may not account for any transfer + * encoding of the content. <p> + * + * @return size of content in bytes + * @exception MessagingException for failures + */ + @Override + public int getSize() throws MessagingException { + try { + synchronized (this) { + // if we already have the size, return it + if (msgSize > 0) + return msgSize; + } + + /* + * Use LIST to determine the entire message + * size and subtract out the header size + * (which may involve loading the headers, + * which may load the content as a side effect). + * If the content is loaded as a side effect of + * loading the headers, it will set the size. + * + * Make sure to call loadHeaders() outside of the + * synchronization block. There's a potential race + * condition here but synchronization will occur in + * loadHeaders() to make sure the headers are only + * loaded once, and again in the following block to + * only compute msgSize once. + */ + if (headers == null) + loadHeaders(); + + synchronized (this) { + if (msgSize < 0) + msgSize = folder.getProtocol().list(msgnum) - hdrSize; + return msgSize; + } + } catch (EOFException eex) { + folder.close(false); + throw new FolderClosedException(folder, eex.toString()); + } catch (IOException ex) { + throw new MessagingException("error getting size", ex); + } + } + + /** + * Produce the raw bytes of the message. The data is fetched using + * the POP3 RETR command. If skipHeader is true, just the content + * is returned. + */ + private InputStream getRawStream(boolean skipHeader) + throws MessagingException { + InputStream rawcontent = null; + try { + synchronized(this) { + rawcontent = rawData.get(); + if (rawcontent == null) { + TempFile cache = folder.getFileCache(); + if (cache != null) { + if (folder.logger.isLoggable(Level.FINE)) + folder.logger.fine("caching message #" + msgnum + + " in temp file"); + AppendStream os = cache.getAppendStream(); + BufferedOutputStream bos = new BufferedOutputStream(os); + try { + folder.getProtocol().retr(msgnum, bos); + } finally { + bos.close(); + } + rawcontent = os.getInputStream(); + } else { + rawcontent = folder.getProtocol().retr(msgnum, + msgSize > 0 ? msgSize + hdrSize : 0); + } + if (rawcontent == null) { + expunged = true; + throw new MessageRemovedException( + "can't retrieve message #" + msgnum + + " in POP3Message.getContentStream"); // XXX - what else? + } + + if (headers == null || + ((POP3Store)(folder.getStore())).forgetTopHeaders) { + headers = new InternetHeaders(rawcontent); + hdrSize = + (int)((SharedInputStream)rawcontent).getPosition(); + } else { + /* + * Already have the headers, have to skip the headers + * in the content array and return the body. + * + * XXX - It seems that some mail servers return slightly + * different headers in the RETR results than were returned + * in the TOP results, so we can't depend on remembering + * the size of the headers from the TOP command and just + * skipping that many bytes. Instead, we have to process + * the content, skipping over the header until we come to + * the empty line that separates the header from the body. + */ + int offset = 0; + for (;;) { + int len = 0; // number of bytes in this line + int c1; + while ((c1 = rawcontent.read()) >= 0) { + if (c1 == '\n') // end of line + break; + else if (c1 == '\r') { + // got CR, is the next char LF? + if (rawcontent.available() > 0) { + rawcontent.mark(1); + if (rawcontent.read() != '\n') + rawcontent.reset(); + } + break; // in any case, end of line + } + + // not CR, NL, or CRLF, count the byte + len++; + } + // here when end of line or out of data + + // if out of data, we're done + if (rawcontent.available() == 0) + break; + + // if it was an empty line, we're done + if (len == 0) + break; + } + hdrSize = + (int)((SharedInputStream)rawcontent).getPosition(); + } + + // skipped the header, the message is what's left + msgSize = rawcontent.available(); + + rawData = new SoftReference<>(rawcontent); + } + } + } catch (EOFException eex) { + folder.close(false); + throw new FolderClosedException(folder, eex.toString()); + } catch (IOException ex) { + throw new MessagingException("error fetching POP3 content", ex); + } + + /* + * We have a cached stream, but we need to return + * a fresh stream to read from the beginning and + * that can be safely closed. + */ + rawcontent = ((SharedInputStream)rawcontent).newStream( + skipHeader ? hdrSize : 0, -1); + return rawcontent; + } + + /** + * Produce the raw bytes of the content. The data is fetched using + * the POP3 RETR command. + * + * @see #contentStream + */ + @Override + protected synchronized InputStream getContentStream() + throws MessagingException { + if (contentStream != null) + return ((SharedInputStream)contentStream).newStream(0, -1); + + InputStream cstream = getRawStream(true); + + /* + * Keep a hard reference to the data if we're using a file + * cache or if the "mail.pop3.keepmessagecontent" prop is set. + */ + TempFile cache = folder.getFileCache(); + if (cache != null || + ((POP3Store)(folder.getStore())).keepMessageContent) + contentStream = ((SharedInputStream)cstream).newStream(0, -1); + return cstream; + } + + /** + * Return the MIME format stream corresponding to this message part. + * + * @return the MIME format stream + * @since JavaMail 1.4.5 + */ + @Override + public InputStream getMimeStream() throws MessagingException { + return getRawStream(false); + } + + /** + * Invalidate the cache of content for this message object, causing + * it to be fetched again from the server the next time it is needed. + * If <code>invalidateHeaders</code> is true, invalidate the headers + * as well. + * + * @param invalidateHeaders invalidate the headers as well? + */ + public synchronized void invalidate(boolean invalidateHeaders) { + content = null; + InputStream rstream = rawData.get(); + if (rstream != null) { + // note that if the content is in the file cache, it will be lost + // and fetched from the server if it's needed again + try { + rstream.close(); + } catch (IOException ex) { + // ignore it + } + rawData = new SoftReference<>(null); + } + if (contentStream != null) { + try { + contentStream.close(); + } catch (IOException ex) { + // ignore it + } + contentStream = null; + } + msgSize = -1; + if (invalidateHeaders) { + headers = null; + hdrSize = -1; + } + } + + /** + * Fetch the header of the message and the first <code>n</code> lines + * of the raw content of the message. The headers and data are + * available in the returned InputStream. + * + * @param n number of lines of content to fetch + * @return InputStream containing the message headers and n content lines + * @exception MessagingException for failures + */ + public InputStream top(int n) throws MessagingException { + try { + synchronized (this) { + return folder.getProtocol().top(msgnum, n); + } + } catch (EOFException eex) { + folder.close(false); + throw new FolderClosedException(folder, eex.toString()); + } catch (IOException ex) { + throw new MessagingException("error getting size", ex); + } + } + + /** + * Get all the headers for this header_name. Note that certain + * headers may be encoded as per RFC 2047 if they contain + * non US-ASCII characters and these should be decoded. <p> + * + * @param name name of header + * @return array of headers + * @exception MessagingException for failures + * @see javax.mail.internet.MimeUtility + */ + @Override + public String[] getHeader(String name) + throws MessagingException { + if (headers == null) + loadHeaders(); + return headers.getHeader(name); + } + + /** + * Get all the headers for this header name, returned as a single + * String, with headers separated by the delimiter. If the + * delimiter is <code>null</code>, only the first header is + * returned. + * + * @param name the name of this header + * @param delimiter delimiter between returned headers + * @return the value fields for all headers with + * this name + * @exception MessagingException for failures + */ + @Override + public String getHeader(String name, String delimiter) + throws MessagingException { + if (headers == null) + loadHeaders(); + return headers.getHeader(name, delimiter); + } + + /** + * Set the value for this header_name. Throws IllegalWriteException + * because POP3 messages are read-only. + * + * @param name header name + * @param value header value + * @see javax.mail.internet.MimeUtility + * @exception IllegalWriteException because the underlying + * implementation does not support modification + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + @Override + public void setHeader(String name, String value) + throws MessagingException { + // XXX - should check for read-only folder? + throw new IllegalWriteException("POP3 messages are read-only"); + } + + /** + * Add this value to the existing values for this header_name. + * Throws IllegalWriteException because POP3 messages are read-only. + * + * @param name header name + * @param value header value + * @see javax.mail.internet.MimeUtility + * @exception IllegalWriteException because the underlying + * implementation does not support modification + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + */ + @Override + public void addHeader(String name, String value) + throws MessagingException { + // XXX - should check for read-only folder? + throw new IllegalWriteException("POP3 messages are read-only"); + } + + /** + * Remove all headers with this name. + * Throws IllegalWriteException because POP3 messages are read-only. + * + * @exception IllegalWriteException because the underlying + * implementation does not support modification + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + */ + @Override + public void removeHeader(String name) + throws MessagingException { + // XXX - should check for read-only folder? + throw new IllegalWriteException("POP3 messages are read-only"); + } + + /** + * Return all the headers from this Message as an enumeration + * of Header objects. <p> + * + * Note that certain headers may be encoded as per RFC 2047 + * if they contain non US-ASCII characters and these should + * be decoded. <p> + * + * @return array of header objects + * @exception MessagingException for failures + * @see javax.mail.internet.MimeUtility + */ + @Override + public Enumeration<Header> getAllHeaders() throws MessagingException { + if (headers == null) + loadHeaders(); + return headers.getAllHeaders(); + } + + /** + * Return matching headers from this Message as an Enumeration of + * Header objects. + * + * @exception MessagingException for failures + */ + @Override + public Enumeration<Header> getMatchingHeaders(String[] names) + throws MessagingException { + if (headers == null) + loadHeaders(); + return headers.getMatchingHeaders(names); + } + + /** + * Return non-matching headers from this Message as an + * Enumeration of Header objects. + * + * @exception MessagingException for failures + */ + @Override + public Enumeration<Header> getNonMatchingHeaders(String[] names) + throws MessagingException { + if (headers == null) + loadHeaders(); + return headers.getNonMatchingHeaders(names); + } + + /** + * Add a raw RFC822 header-line. + * Throws IllegalWriteException because POP3 messages are read-only. + * + * @exception IllegalWriteException because the underlying + * implementation does not support modification + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + */ + @Override + public void addHeaderLine(String line) throws MessagingException { + // XXX - should check for read-only folder? + throw new IllegalWriteException("POP3 messages are read-only"); + } + + /** + * Get all header lines as an Enumeration of Strings. A Header + * line is a raw RFC822 header-line, containing both the "name" + * and "value" field. + * + * @exception MessagingException for failures + */ + @Override + public Enumeration<String> getAllHeaderLines() throws MessagingException { + if (headers == null) + loadHeaders(); + return headers.getAllHeaderLines(); + } + + /** + * Get matching header lines as an Enumeration of Strings. + * A Header line is a raw RFC822 header-line, containing both + * the "name" and "value" field. + * + * @exception MessagingException for failures + */ + @Override + public Enumeration<String> getMatchingHeaderLines(String[] names) + throws MessagingException { + if (headers == null) + loadHeaders(); + return headers.getMatchingHeaderLines(names); + } + + /** + * Get non-matching header lines as an Enumeration of Strings. + * A Header line is a raw RFC822 header-line, containing both + * the "name" and "value" field. + * + * @exception MessagingException for failures + */ + @Override + public Enumeration<String> getNonMatchingHeaderLines(String[] names) + throws MessagingException { + if (headers == null) + loadHeaders(); + return headers.getNonMatchingHeaderLines(names); + } + + /** + * POP3 message can't be changed. This method throws + * IllegalWriteException. + * + * @exception IllegalWriteException because the underlying + * implementation does not support modification + */ + @Override + public void saveChanges() throws MessagingException { + // POP3 Messages are read-only + throw new IllegalWriteException("POP3 messages are read-only"); + } + + /** + * Output the message as an RFC 822 format stream, without + * specified headers. If the property "mail.pop3.cachewriteto" + * is set to "true", and ignoreList is null, and the message hasn't + * already been cached as a side effect of other operations, the message + * content is cached before being written. Otherwise, the message is + * streamed directly to the output stream without being cached. + * + * @exception IOException if an error occurs writing to the stream + * or if an error is generated by the + * javax.activation layer. + * @exception MessagingException for other failures + * @see javax.activation.DataHandler#writeTo + */ + @Override + public synchronized void writeTo(OutputStream os, String[] ignoreList) + throws IOException, MessagingException { + InputStream rawcontent = rawData.get(); + if (rawcontent == null && ignoreList == null && + !((POP3Store)(folder.getStore())).cacheWriteTo) { + if (folder.logger.isLoggable(Level.FINE)) + folder.logger.fine("streaming msg " + msgnum); + if (!folder.getProtocol().retr(msgnum, os)) { + expunged = true; + throw new MessageRemovedException("can't retrieve message #" + + msgnum + " in POP3Message.writeTo"); // XXX - what else? + } + } else if (rawcontent != null && ignoreList == null) { + // can just copy the cached data + InputStream in = ((SharedInputStream)rawcontent).newStream(0, -1); + try { + byte[] buf = new byte[16*1024]; + int len; + while ((len = in.read(buf)) > 0) + os.write(buf, 0, len); + } finally { + try { + if (in != null) + in.close(); + } catch (IOException ex) { } + } + } else + super.writeTo(os, ignoreList); + } + + /** + * Load the headers for this message into the InternetHeaders object. + * The headers are fetched using the POP3 TOP command. + */ + private void loadHeaders() throws MessagingException { + assert !Thread.holdsLock(this); + try { + boolean fetchContent = false; + synchronized (this) { + if (headers != null) // check again under lock + return; + InputStream hdrs = null; + if (((POP3Store)(folder.getStore())).disableTop || + (hdrs = folder.getProtocol().top(msgnum, 0)) == null) { + // possibly because the TOP command isn't supported, + // load headers as a side effect of loading the entire + // content. + fetchContent = true; + } else { + try { + hdrSize = hdrs.available(); + headers = new InternetHeaders(hdrs); + } finally { + hdrs.close(); + } + } + } + + /* + * Outside the synchronization block... + * + * Do we need to fetch the entire mesage content in order to + * load the headers as a side effect? Yes, there's a race + * condition here - multiple threads could decide that the + * content needs to be fetched. Fortunately, they'll all + * synchronize in the getContentStream method and the content + * will only be loaded once. + */ + if (fetchContent) { + InputStream cs = null; + try { + cs = getContentStream(); + } finally { + if (cs != null) + cs.close(); + } + } + } catch (EOFException eex) { + folder.close(false); + throw new FolderClosedException(folder, eex.toString()); + } catch (IOException ex) { + throw new MessagingException("error loading POP3 headers", ex); + } + } +}
diff --git a/mail/src/main/java/com/sun/mail/pop3/POP3Provider.java b/mail/src/main/java/com/sun/mail/pop3/POP3Provider.java new file mode 100644 index 0000000..73488e8 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/pop3/POP3Provider.java
@@ -0,0 +1,29 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.pop3; + +import javax.mail.Provider; + +/** + * The POP3 protocol provider. + */ +public class POP3Provider extends Provider { + public POP3Provider() { + super(Provider.Type.STORE, "pop3", POP3Store.class.getName(), + "Oracle", null); + } +}
diff --git a/mail/src/main/java/com/sun/mail/pop3/POP3SSLProvider.java b/mail/src/main/java/com/sun/mail/pop3/POP3SSLProvider.java new file mode 100644 index 0000000..5ab7c2c --- /dev/null +++ b/mail/src/main/java/com/sun/mail/pop3/POP3SSLProvider.java
@@ -0,0 +1,29 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.pop3; + +import javax.mail.Provider; + +/** + * The POP3 SSL protocol provider. + */ +public class POP3SSLProvider extends Provider { + public POP3SSLProvider() { + super(Provider.Type.STORE, "pop3s", POP3SSLStore.class.getName(), + "Oracle", null); + } +}
diff --git a/mail/src/main/java/com/sun/mail/pop3/POP3SSLStore.java b/mail/src/main/java/com/sun/mail/pop3/POP3SSLStore.java new file mode 100644 index 0000000..fa0054c --- /dev/null +++ b/mail/src/main/java/com/sun/mail/pop3/POP3SSLStore.java
@@ -0,0 +1,31 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.pop3; + +import javax.mail.*; + +/** + * A POP3 Message Store using SSL. Contains only one folder, "INBOX". + * + * @author Bill Shannon + */ +public class POP3SSLStore extends POP3Store { + + public POP3SSLStore(Session session, URLName url) { + super(session, url, "pop3s", true); + } +}
diff --git a/mail/src/main/java/com/sun/mail/pop3/POP3Store.java b/mail/src/main/java/com/sun/mail/pop3/POP3Store.java new file mode 100644 index 0000000..c9818a1 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/pop3/POP3Store.java
@@ -0,0 +1,429 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.pop3; + +import java.util.Properties; +import java.util.logging.Level; +import java.lang.reflect.*; + +import javax.mail.*; +import javax.mail.internet.*; +import java.io.File; +import java.io.PrintStream; +import java.io.IOException; +import java.io.EOFException; +import java.util.Collections; +import java.util.Map; + +import com.sun.mail.util.PropUtil; +import com.sun.mail.util.MailLogger; +import com.sun.mail.util.SocketConnectException; +import com.sun.mail.util.MailConnectException; + +/** + * A POP3 Message Store. Contains only one folder, "INBOX". + * + * See the <a href="package-summary.html">com.sun.mail.pop3</a> package + * documentation for further information on the POP3 protocol provider. <p> + * + * @author Bill Shannon + * @author John Mani + */ +public class POP3Store extends Store { + + private String name = "pop3"; // my protocol name + private int defaultPort = 110; // default POP3 port + private boolean isSSL = false; // use SSL? + + private Protocol port = null; // POP3 port for self + private POP3Folder portOwner = null; // folder owning port + private String host = null; // host + private int portNum = -1; + private String user = null; + private String passwd = null; + private boolean useStartTLS = false; + private boolean requireStartTLS = false; + private boolean usingSSL = false; + private Map<String, String> capabilities; + private MailLogger logger; + + // following set here and accessed by other classes in this package + volatile Constructor<?> messageConstructor = null; + volatile boolean rsetBeforeQuit = false; + volatile boolean disableTop = false; + volatile boolean forgetTopHeaders = false; + volatile boolean supportsUidl = true; + volatile boolean cacheWriteTo = false; + volatile boolean useFileCache = false; + volatile File fileCacheDir = null; + volatile boolean keepMessageContent = false; + volatile boolean finalizeCleanClose = false; + + public POP3Store(Session session, URLName url) { + this(session, url, "pop3", false); + } + + public POP3Store(Session session, URLName url, + String name, boolean isSSL) { + super(session, url); + if (url != null) + name = url.getProtocol(); + this.name = name; + logger = new MailLogger(this.getClass(), "DEBUG POP3", + session.getDebug(), session.getDebugOut()); + + if (!isSSL) + isSSL = PropUtil.getBooleanProperty(session.getProperties(), + "mail." + name + ".ssl.enable", false); + if (isSSL) + this.defaultPort = 995; + else + this.defaultPort = 110; + this.isSSL = isSSL; + + rsetBeforeQuit = getBoolProp("rsetbeforequit"); + disableTop = getBoolProp("disabletop"); + forgetTopHeaders = getBoolProp("forgettopheaders"); + cacheWriteTo = getBoolProp("cachewriteto"); + useFileCache = getBoolProp("filecache.enable"); + String dir = session.getProperty("mail." + name + ".filecache.dir"); + if (dir != null && logger.isLoggable(Level.CONFIG)) + logger.config("mail." + name + ".filecache.dir: " + dir); + if (dir != null) + fileCacheDir = new File(dir); + keepMessageContent = getBoolProp("keepmessagecontent"); + + // mail.pop3.starttls.enable enables use of STLS command + useStartTLS = getBoolProp("starttls.enable"); + + // mail.pop3.starttls.required requires use of STLS command + requireStartTLS = getBoolProp("starttls.required"); + + // mail.pop3.finalizecleanclose requires clean close when finalizing + finalizeCleanClose = getBoolProp("finalizecleanclose"); + + String s = session.getProperty("mail." + name + ".message.class"); + if (s != null) { + logger.log(Level.CONFIG, "message class: {0}", s); + try { + ClassLoader cl = this.getClass().getClassLoader(); + + // now load the class + Class<?> messageClass = null; + try { + // First try the "application's" class loader. + // This should eventually be replaced by + // Thread.currentThread().getContextClassLoader(). + messageClass = Class.forName(s, false, cl); + } catch (ClassNotFoundException ex1) { + // That didn't work, now try the "system" class loader. + // (Need both of these because JDK 1.1 class loaders + // may not delegate to their parent class loader.) + messageClass = Class.forName(s); + } + + Class<?>[] c = {javax.mail.Folder.class, int.class}; + messageConstructor = messageClass.getConstructor(c); + } catch (Exception ex) { + logger.log(Level.CONFIG, "failed to load message class", ex); + } + } + } + + /** + * Get the value of a boolean property. + * Print out the value if logging is enabled. + */ + private final synchronized boolean getBoolProp(String prop) { + prop = "mail." + name + "." + prop; + boolean val = PropUtil.getBooleanProperty(session.getProperties(), + prop, false); + if (logger.isLoggable(Level.CONFIG)) + logger.config(prop + ": " + val); + return val; + } + + /** + * Get a reference to the session. + */ + synchronized Session getSession() { + return session; + } + + @Override + protected synchronized boolean protocolConnect(String host, int portNum, + String user, String passwd) throws MessagingException { + + // check for non-null values of host, password, user + if (host == null || passwd == null || user == null) + return false; + + // if port is not specified, set it to value of mail.pop3.port + // property if it exists, otherwise default to 110 + if (portNum == -1) + portNum = PropUtil.getIntProperty(session.getProperties(), + "mail." + name + ".port", -1); + + if (portNum == -1) + portNum = defaultPort; + + this.host = host; + this.portNum = portNum; + this.user = user; + this.passwd = passwd; + try { + port = getPort(null); + } catch (EOFException eex) { + throw new AuthenticationFailedException(eex.getMessage()); + } catch (SocketConnectException scex) { + throw new MailConnectException(scex); + } catch (IOException ioex) { + throw new MessagingException("Connect failed", ioex); + } + + return true; + } + + /** + * Check whether this store is connected. Override superclass + * method, to actually ping our server connection. + */ + /* + * Note that we maintain somewhat of an illusion of being connected + * even if we're not really connected. This is because a Folder + * can use the connection and close it when it's done. If we then + * ask whether the Store's connected we want the answer to be true, + * as long as we can reconnect at that point. This means that we + * need to be able to reconnect the Store on demand. + */ + @Override + public synchronized boolean isConnected() { + if (!super.isConnected()) + // if we haven't been connected at all, don't bother with + // the NOOP. + return false; + try { + if (port == null) + port = getPort(null); + else if (!port.noop()) + throw new IOException("NOOP failed"); + return true; + } catch (IOException ioex) { + // no longer connected, close it down + try { + super.close(); // notifies listeners + } catch (MessagingException mex) { + // ignore it + } + return false; + } + } + + synchronized Protocol getPort(POP3Folder owner) throws IOException { + Protocol p; + + // if we already have a port, remember who's using it + if (port != null && portOwner == null) { + portOwner = owner; + return port; + } + + // need a new port, create it and try to login + p = new Protocol(host, portNum, logger, + session.getProperties(), "mail." + name, isSSL); + + if (useStartTLS || requireStartTLS) { + if (p.hasCapability("STLS")) { + if (p.stls()) { + // success, refresh capabilities + p.setCapabilities(p.capa()); + } else if (requireStartTLS) { + logger.fine("STLS required but failed"); + throw cleanupAndThrow(p, + new EOFException("STLS required but failed")); + } + } else if (requireStartTLS) { + logger.fine("STLS required but not supported"); + throw cleanupAndThrow(p, + new EOFException("STLS required but not supported")); + } + } + + capabilities = p.getCapabilities(); // save for later, may be null + usingSSL = p.isSSL(); // in case anyone asks + + /* + * If we haven't explicitly disabled use of the TOP command, + * and the server has provided its capabilities, + * and the server doesn't support the TOP command, + * disable the TOP command. + */ + if (!disableTop && + capabilities != null && !capabilities.containsKey("TOP")) { + disableTop = true; + logger.fine("server doesn't support TOP, disabling it"); + } + + supportsUidl = capabilities == null || capabilities.containsKey("UIDL"); + + String msg = null; + if ((msg = p.login(user, passwd)) != null) { + throw cleanupAndThrow(p, new EOFException(msg)); + } + + /* + * If a Folder closes the port, and then a Folder + * is opened, the Store won't have a port. In that + * case, the getPort call will come from Folder.open, + * but we need to keep track of the port in the Store + * so that a later call to Folder.isOpen, which calls + * Store.isConnected, will use the same port. + */ + if (port == null && owner != null) { + port = p; + portOwner = owner; + } + if (portOwner == null) + portOwner = owner; + return p; + } + + private static IOException cleanupAndThrow(Protocol p, IOException ife) { + try { + p.quit(); + } catch (Throwable thr) { + if (isRecoverable(thr)) { + ife.addSuppressed(thr); + } else { + thr.addSuppressed(ife); + if (thr instanceof Error) { + throw (Error) thr; + } + if (thr instanceof RuntimeException) { + throw (RuntimeException) thr; + } + throw new RuntimeException("unexpected exception", thr); + } + } + return ife; + } + + private static boolean isRecoverable(Throwable t) { + return (t instanceof Exception) || (t instanceof LinkageError); + } + + synchronized void closePort(POP3Folder owner) { + if (portOwner == owner) { + port = null; + portOwner = null; + } + } + + @Override + public synchronized void close() throws MessagingException { + close(false); + } + + synchronized void close(boolean force) throws MessagingException { + try { + if (port != null) { + if (force) + port.close(); + else + port.quit(); + } + } catch (IOException ioex) { + } finally { + port = null; + + // to set the state and send the closed connection event + super.close(); + } + } + + @Override + public Folder getDefaultFolder() throws MessagingException { + checkConnected(); + return new DefaultFolder(this); + } + + /** + * Only the name "INBOX" is supported. + */ + @Override + public Folder getFolder(String name) throws MessagingException { + checkConnected(); + return new POP3Folder(this, name); + } + + @Override + public Folder getFolder(URLName url) throws MessagingException { + checkConnected(); + return new POP3Folder(this, url.getFile()); + } + + /** + * Return a Map of the capabilities the server provided, + * as per RFC 2449. If the server doesn't support RFC 2449, + * an emtpy Map is returned. The returned Map can not be modified. + * The key to the Map is the upper case capability name as + * a String. The value of the entry is the entire String + * capability line returned by the server. <p> + * + * For example, to check if the server supports the STLS capability, use: + * <code>if (store.capabilities().containsKey("STLS")) ...</code> + * + * @return Map of capabilities + * @exception MessagingException for failures + * @since JavaMail 1.4.3 + */ + public Map<String, String> capabilities() throws MessagingException { + Map<String, String> c; + synchronized (this) { + c = capabilities; + } + if (c != null) + return Collections.unmodifiableMap(c); + else + return Collections.<String, String>emptyMap(); + } + + /** + * Is this POP3Store using SSL to connect to the server? + * + * @return true if using SSL + * @since JavaMail 1.4.6 + */ + public synchronized boolean isSSL() { + return usingSSL; + } + + @Override + protected void finalize() throws Throwable { + try { + if (port != null) // don't force a connection attempt + close(!finalizeCleanClose); + } finally { + super.finalize(); + } + } + + private void checkConnected() throws MessagingException { + if (!super.isConnected()) + throw new MessagingException("Not connected"); + } +}
diff --git a/mail/src/main/java/com/sun/mail/pop3/Protocol.java b/mail/src/main/java/com/sun/mail/pop3/Protocol.java new file mode 100644 index 0000000..bfba0d9 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/pop3/Protocol.java
@@ -0,0 +1,861 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.pop3; + +import java.util.*; +import java.net.*; +import java.io.*; +import java.security.*; +import java.util.logging.Level; +import javax.net.ssl.SSLSocket; + +import com.sun.mail.util.PropUtil; +import com.sun.mail.util.MailLogger; +import com.sun.mail.util.SocketFetcher; +import com.sun.mail.util.LineInputStream; +import com.sun.mail.util.TraceInputStream; +import com.sun.mail.util.TraceOutputStream; +import com.sun.mail.util.SharedByteArrayOutputStream; + +class Response { + boolean ok = false; // true if "+OK" + String data = null; // rest of line after "+OK" or "-ERR" + InputStream bytes = null; // all the bytes from a multi-line response +} + +/** + * This class provides a POP3 connection and implements + * the POP3 protocol requests. + * + * APOP support courtesy of "chamness". + * + * @author Bill Shannon + */ +class Protocol { + private Socket socket; // POP3 socket + private String host; // host we're connected to + private Properties props; // session properties + private String prefix; // protocol name prefix, for props + private BufferedReader input; // input buf + private PrintWriter output; // output buf + private TraceInputStream traceInput; + private TraceOutputStream traceOutput; + private MailLogger logger; + private MailLogger traceLogger; + private String apopChallenge = null; + private Map<String, String> capabilities = null; + private boolean pipelining; + private boolean noauthdebug = true; // hide auth info in debug output + private boolean traceSuspended; // temporarily suspend tracing + + private static final int POP3_PORT = 110; // standard POP3 port + private static final String CRLF = "\r\n"; + // sometimes the returned size isn't quite big enough + private static final int SLOP = 128; + + /** + * Open a connection to the POP3 server. + */ + Protocol(String host, int port, MailLogger logger, + Properties props, String prefix, boolean isSSL) + throws IOException { + this.host = host; + this.props = props; + this.prefix = prefix; + this.logger = logger; + traceLogger = logger.getSubLogger("protocol", null); + noauthdebug = !PropUtil.getBooleanProperty(props, + "mail.debug.auth", false); + + Response r; + boolean enableAPOP = getBoolProp(props, prefix + ".apop.enable"); + boolean disableCapa = getBoolProp(props, prefix + ".disablecapa"); + try { + if (port == -1) + port = POP3_PORT; + if (logger.isLoggable(Level.FINE)) + logger.fine("connecting to host \"" + host + + "\", port " + port + ", isSSL " + isSSL); + + socket = SocketFetcher.getSocket(host, port, props, prefix, isSSL); + initStreams(); + r = simpleCommand(null); + } catch (IOException ioe) { + throw cleanupAndThrow(socket, ioe); + } + + if (!r.ok) { + throw cleanupAndThrow(socket, new IOException("Connect failed")); + } + if (enableAPOP && r.data != null) { + int challStart = r.data.indexOf('<'); // start of challenge + int challEnd = r.data.indexOf('>', challStart); // end of challenge + if (challStart != -1 && challEnd != -1) + apopChallenge = r.data.substring(challStart, challEnd + 1); + logger.log(Level.FINE, "APOP challenge: {0}", apopChallenge); + } + + // if server supports RFC 2449, set capabilities + if (!disableCapa) + setCapabilities(capa()); + + pipelining = hasCapability("PIPELINING") || + PropUtil.getBooleanProperty(props, prefix + ".pipelining", false); + if (pipelining) + logger.config("PIPELINING enabled"); + } + + private static IOException cleanupAndThrow(Socket socket, IOException ife) { + try { + socket.close(); + } catch (Throwable thr) { + if (isRecoverable(thr)) { + ife.addSuppressed(thr); + } else { + thr.addSuppressed(ife); + if (thr instanceof Error) { + throw (Error) thr; + } + if (thr instanceof RuntimeException) { + throw (RuntimeException) thr; + } + throw new RuntimeException("unexpected exception", thr); + } + } + return ife; + } + + private static boolean isRecoverable(Throwable t) { + return (t instanceof Exception) || (t instanceof LinkageError); + } + + /** + * Get the value of a boolean property. + * Print out the value if logging is enabled. + */ + private final synchronized boolean getBoolProp(Properties props, + String prop) { + boolean val = PropUtil.getBooleanProperty(props, prop, false); + if (logger.isLoggable(Level.CONFIG)) + logger.config(prop + ": " + val); + return val; + } + + private void initStreams() throws IOException { + boolean quote = PropUtil.getBooleanProperty(props, + "mail.debug.quote", false); + traceInput = + new TraceInputStream(socket.getInputStream(), traceLogger); + traceInput.setQuote(quote); + + traceOutput = + new TraceOutputStream(socket.getOutputStream(), traceLogger); + traceOutput.setQuote(quote); + + // should be US-ASCII, but not all JDK's support it so use iso-8859-1 + input = new BufferedReader(new InputStreamReader(traceInput, + "iso-8859-1")); + output = new PrintWriter( + new BufferedWriter( + new OutputStreamWriter(traceOutput, "iso-8859-1"))); + } + + @Override + protected void finalize() throws Throwable { + try { + if (socket != null) // Forgot to logout ?! + quit(); + } finally { + super.finalize(); + } + } + + /** + * Parse the capabilities from a CAPA response. + */ + synchronized void setCapabilities(InputStream in) { + if (in == null) { + capabilities = null; + return; + } + + capabilities = new HashMap<>(10); + BufferedReader r = null; + try { + r = new BufferedReader(new InputStreamReader(in, "us-ascii")); + } catch (UnsupportedEncodingException ex) { + // should never happen + assert false; + } + String s; + try { + while ((s = r.readLine()) != null) { + String cap = s; + int i = cap.indexOf(' '); + if (i > 0) + cap = cap.substring(0, i); + capabilities.put(cap.toUpperCase(Locale.ENGLISH), s); + } + } catch (IOException ex) { + // should never happen + } finally { + try { + in.close(); + } catch (IOException ex) { } + } + } + + /** + * Check whether the given capability is supported by + * this server. Returns <code>true</code> if so, otherwise + * returns false. + */ + synchronized boolean hasCapability(String c) { + return capabilities != null && + capabilities.containsKey(c.toUpperCase(Locale.ENGLISH)); + } + + /** + * Return the map of capabilities returned by the server. + */ + synchronized Map<String, String> getCapabilities() { + return capabilities; + } + + /** + * Login to the server, using the USER and PASS commands. + */ + synchronized String login(String user, String password) + throws IOException { + Response r; + // only pipeline password if connection is secure + boolean batch = pipelining && socket instanceof SSLSocket; + + try { + + if (noauthdebug && isTracing()) { + logger.fine("authentication command trace suppressed"); + suspendTracing(); + } + String dpw = null; + if (apopChallenge != null) + dpw = getDigest(password); + if (apopChallenge != null && dpw != null) { + r = simpleCommand("APOP " + user + " " + dpw); + } else if (batch) { + String cmd = "USER " + user; + batchCommandStart(cmd); + issueCommand(cmd); + cmd = "PASS " + password; + batchCommandContinue(cmd); + issueCommand(cmd); + r = readResponse(); + if (!r.ok) { + String err = r.data != null ? r.data : "USER command failed"; + readResponse(); // read and ignore PASS response + batchCommandEnd(); + return err; + } + r = readResponse(); + batchCommandEnd(); + } else { + r = simpleCommand("USER " + user); + if (!r.ok) + return r.data != null ? r.data : "USER command failed"; + r = simpleCommand("PASS " + password); + } + if (noauthdebug && isTracing()) + logger.log(Level.FINE, "authentication command {0}", + (r.ok ? "succeeded" : "failed")); + if (!r.ok) + return r.data != null ? r.data : "login failed"; + return null; + + } finally { + resumeTracing(); + } + } + + /** + * Gets the APOP message digest. + * From RFC 1939: + * + * The 'digest' parameter is calculated by applying the MD5 + * algorithm [RFC1321] to a string consisting of the timestamp + * (including angle-brackets) followed by a shared secret. + * The 'digest' parameter itself is a 16-octet value which is + * sent in hexadecimal format, using lower-case ASCII characters. + * + * @param password The APOP password + * @return The APOP digest or an empty string if an error occurs. + */ + private String getDigest(String password) { + String key = apopChallenge + password; + byte[] digest; + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + digest = md.digest(key.getBytes("iso-8859-1")); // XXX + } catch (NoSuchAlgorithmException nsae) { + return null; + } catch (UnsupportedEncodingException uee) { + return null; + } + return toHex(digest); + } + + private static char[] digits = { + '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' + }; + + /** + * Convert a byte array to a string of hex digits representing the bytes. + */ + private static String toHex(byte[] bytes) { + char[] result = new char[bytes.length * 2]; + + for (int index = 0, i = 0; index < bytes.length; index++) { + int temp = bytes[index] & 0xFF; + result[i++] = digits[temp >> 4]; + result[i++] = digits[temp & 0xF]; + } + return new String(result); + } + + /** + * Close down the connection, sending the QUIT command. + */ + synchronized boolean quit() throws IOException { + boolean ok = false; + try { + Response r = simpleCommand("QUIT"); + ok = r.ok; + } finally { + close(); + } + return ok; + } + + /** + * Close the connection without sending any commands. + */ + void close() { + try { + socket.close(); + } catch (IOException ex) { + // ignore it + } finally { + socket = null; + input = null; + output = null; + } + } + + /** + * Return the total number of messages and mailbox size, + * using the STAT command. + */ + synchronized Status stat() throws IOException { + Response r = simpleCommand("STAT"); + Status s = new Status(); + + /* + * Normally the STAT command shouldn't fail but apparently it + * does when accessing Hotmail too often, returning: + * -ERR login allowed only every 15 minutes + * (Why it doesn't just fail the login, I don't know.) + * This is a serious failure that we don't want to hide + * from the user. + */ + if (!r.ok) + throw new IOException("STAT command failed: " + r.data); + + if (r.data != null) { + try { + StringTokenizer st = new StringTokenizer(r.data); + s.total = Integer.parseInt(st.nextToken()); + s.size = Integer.parseInt(st.nextToken()); + } catch (RuntimeException e) { + } + } + return s; + } + + /** + * Return the size of the message using the LIST command. + */ + synchronized int list(int msg) throws IOException { + Response r = simpleCommand("LIST " + msg); + int size = -1; + if (r.ok && r.data != null) { + try { + StringTokenizer st = new StringTokenizer(r.data); + st.nextToken(); // skip message number + size = Integer.parseInt(st.nextToken()); + } catch (RuntimeException e) { + // ignore it + } + } + return size; + } + + /** + * Return the size of all messages using the LIST command. + */ + synchronized InputStream list() throws IOException { + Response r = multilineCommand("LIST", 128); // 128 == output size est + return r.bytes; + } + + /** + * Retrieve the specified message. + * Given an estimate of the message's size we can be more efficient, + * preallocating the array and returning a SharedInputStream to allow + * us to share the array. + */ + synchronized InputStream retr(int msg, int size) throws IOException { + Response r; + String cmd; + boolean batch = size == 0 && pipelining; + if (batch) { + cmd = "LIST " + msg; + batchCommandStart(cmd); + issueCommand(cmd); + cmd = "RETR " + msg; + batchCommandContinue(cmd); + issueCommand(cmd); + r = readResponse(); + if (r.ok && r.data != null) { + // parse the LIST response to get the message size + try { + StringTokenizer st = new StringTokenizer(r.data); + st.nextToken(); // skip message number + size = Integer.parseInt(st.nextToken()); + // don't allow ridiculous sizes + if (size > 1024*1024*1024 || size < 0) + size = 0; + else { + if (logger.isLoggable(Level.FINE)) + logger.fine("pipeline message size " + size); + size += SLOP; + } + } catch (RuntimeException e) { + } + } + r = readResponse(); + if (r.ok) + r.bytes = readMultilineResponse(size + SLOP); + batchCommandEnd(); + } else { + cmd = "RETR " + msg; + multilineCommandStart(cmd); + issueCommand(cmd); + r = readResponse(); + if (!r.ok) { + multilineCommandEnd(); + return null; + } + + /* + * Many servers return a response to the RETR command of the form: + * +OK 832 octets + * If we don't have a size guess already, try to parse the response + * for data in that format and use it if found. It's only a guess, + * but it might be a good guess. + */ + if (size <= 0 && r.data != null) { + try { + StringTokenizer st = new StringTokenizer(r.data); + String s = st.nextToken(); + String octets = st.nextToken(); + if (octets.equals("octets")) { + size = Integer.parseInt(s); + // don't allow ridiculous sizes + if (size > 1024*1024*1024 || size < 0) + size = 0; + else { + if (logger.isLoggable(Level.FINE)) + logger.fine("guessing message size: " + size); + size += SLOP; + } + } + } catch (RuntimeException e) { + } + } + r.bytes = readMultilineResponse(size); + multilineCommandEnd(); + } + if (r.ok) { + if (size > 0 && logger.isLoggable(Level.FINE)) + logger.fine("got message size " + r.bytes.available()); + } + return r.bytes; + } + + /** + * Retrieve the specified message and stream the content to the + * specified OutputStream. Return true on success. + */ + synchronized boolean retr(int msg, OutputStream os) throws IOException { + String cmd = "RETR " + msg; + multilineCommandStart(cmd); + issueCommand(cmd); + Response r = readResponse(); + if (!r.ok) { + multilineCommandEnd(); + return false; + } + + Throwable terr = null; + int b, lastb = '\n'; + try { + while ((b = input.read()) >= 0) { + if (lastb == '\n' && b == '.') { + b = input.read(); + if (b == '\r') { + // end of response, consume LF as well + b = input.read(); + break; + } + } + + /* + * Keep writing unless we get an error while writing, + * which we defer until all of the data has been read. + */ + if (terr == null) { + try { + os.write(b); + } catch (IOException ex) { + logger.log(Level.FINE, "exception while streaming", ex); + terr = ex; + } catch (RuntimeException ex) { + logger.log(Level.FINE, "exception while streaming", ex); + terr = ex; + } + } + lastb = b; + } + } catch (InterruptedIOException iioex) { + /* + * As above in simpleCommand, close the socket to recover. + */ + try { + socket.close(); + } catch (IOException cex) { } + throw iioex; + } + if (b < 0) + throw new EOFException("EOF on socket"); + + // was there a deferred error? + if (terr != null) { + if (terr instanceof IOException) + throw (IOException)terr; + if (terr instanceof RuntimeException) + throw (RuntimeException)terr; + assert false; // can't get here + } + multilineCommandEnd(); + return true; + } + + /** + * Return the message header and the first n lines of the message. + */ + synchronized InputStream top(int msg, int n) throws IOException { + Response r = multilineCommand("TOP " + msg + " " + n, 0); + return r.bytes; + } + + /** + * Delete (permanently) the specified message. + */ + synchronized boolean dele(int msg) throws IOException { + Response r = simpleCommand("DELE " + msg); + return r.ok; + } + + /** + * Return the UIDL string for the message. + */ + synchronized String uidl(int msg) throws IOException { + Response r = simpleCommand("UIDL " + msg); + if (!r.ok) + return null; + int i = r.data.indexOf(' '); + if (i > 0) + return r.data.substring(i + 1); + else + return null; + } + + /** + * Return the UIDL strings for all messages. + * The UID for msg #N is returned in uids[N-1]. + */ + synchronized boolean uidl(String[] uids) throws IOException { + Response r = multilineCommand("UIDL", 15 * uids.length); + if (!r.ok) + return false; + LineInputStream lis = new LineInputStream(r.bytes); + String line = null; + while ((line = lis.readLine()) != null) { + int i = line.indexOf(' '); + if (i < 1 || i >= line.length()) + continue; + int n = Integer.parseInt(line.substring(0, i)); + if (n > 0 && n <= uids.length) + uids[n - 1] = line.substring(i + 1); + } + try { + r.bytes.close(); + } catch (IOException ex) { + // ignore it + } + return true; + } + + /** + * Do a NOOP. + */ + synchronized boolean noop() throws IOException { + Response r = simpleCommand("NOOP"); + return r.ok; + } + + /** + * Do an RSET. + */ + synchronized boolean rset() throws IOException { + Response r = simpleCommand("RSET"); + return r.ok; + } + + /** + * Start TLS using STLS command specified by RFC 2595. + * If already using SSL, this is a nop and the STLS command is not issued. + */ + synchronized boolean stls() throws IOException { + if (socket instanceof SSLSocket) + return true; // nothing to do + Response r = simpleCommand("STLS"); + if (r.ok) { + // it worked, now switch the socket into TLS mode + try { + socket = SocketFetcher.startTLS(socket, host, props, prefix); + initStreams(); + } catch (IOException ioex) { + try { + socket.close(); + } finally { + socket = null; + input = null; + output = null; + } + IOException sioex = + new IOException("Could not convert socket to TLS"); + sioex.initCause(ioex); + throw sioex; + } + } + return r.ok; + } + + /** + * Is this connection using SSL? + */ + synchronized boolean isSSL() { + return socket instanceof SSLSocket; + } + + /** + * Get server capabilities using CAPA command specified by RFC 2449. + * Returns null if not supported. + */ + synchronized InputStream capa() throws IOException { + Response r = multilineCommand("CAPA", 128); // 128 == output size est + if (!r.ok) + return null; + return r.bytes; + } + + /** + * Issue a simple POP3 command and return the response. + */ + private Response simpleCommand(String cmd) throws IOException { + simpleCommandStart(cmd); + issueCommand(cmd); + Response r = readResponse(); + simpleCommandEnd(); + return r; + } + + /** + * Send the specified command. + */ + private void issueCommand(String cmd) throws IOException { + if (socket == null) + throw new IOException("Folder is closed"); // XXX + + if (cmd != null) { + cmd += CRLF; + output.print(cmd); // do it in one write + output.flush(); + } + } + + /** + * Read the response to a command. + */ + private Response readResponse() throws IOException { + String line = null; + try { + line = input.readLine(); + } catch (InterruptedIOException iioex) { + /* + * If we get a timeout while using the socket, we have no idea + * what state the connection is in. The server could still be + * alive, but slow, and could still be sending data. The only + * safe way to recover is to drop the connection. + */ + try { + socket.close(); + } catch (IOException cex) { } + throw new EOFException(iioex.getMessage()); + } catch (SocketException ex) { + /* + * If we get an error while using the socket, we have no idea + * what state the connection is in. The server could still be + * alive, but slow, and could still be sending data. The only + * safe way to recover is to drop the connection. + */ + try { + socket.close(); + } catch (IOException cex) { } + throw new EOFException(ex.getMessage()); + } + + if (line == null) { + traceLogger.finest("<EOF>"); + throw new EOFException("EOF on socket"); + } + Response r = new Response(); + if (line.startsWith("+OK")) + r.ok = true; + else if (line.startsWith("-ERR")) + r.ok = false; + else + throw new IOException("Unexpected response: " + line); + int i; + if ((i = line.indexOf(' ')) >= 0) + r.data = line.substring(i + 1); + return r; + } + + /** + * Issue a POP3 command that expects a multi-line response. + * <code>size</code> is an estimate of the response size. + */ + private Response multilineCommand(String cmd, int size) throws IOException { + multilineCommandStart(cmd); + issueCommand(cmd); + Response r = readResponse(); + if (!r.ok) { + multilineCommandEnd(); + return r; + } + r.bytes = readMultilineResponse(size); + multilineCommandEnd(); + return r; + } + + /** + * Read the response to a multiline command after the command response. + * The size parameter indicates the expected size of the response; + * the actual size can be different. Returns an InputStream to the + * response bytes. + */ + private InputStream readMultilineResponse(int size) throws IOException { + SharedByteArrayOutputStream buf = new SharedByteArrayOutputStream(size); + int b, lastb = '\n'; + try { + while ((b = input.read()) >= 0) { + if (lastb == '\n' && b == '.') { + b = input.read(); + if (b == '\r') { + // end of response, consume LF as well + b = input.read(); + break; + } + } + buf.write(b); + lastb = b; + } + } catch (InterruptedIOException iioex) { + /* + * As above in readResponse, close the socket to recover. + */ + try { + socket.close(); + } catch (IOException cex) { } + throw iioex; + } + if (b < 0) + throw new EOFException("EOF on socket"); + return buf.toStream(); + } + + /** + * Is protocol tracing enabled? + */ + protected boolean isTracing() { + return traceLogger.isLoggable(Level.FINEST); + } + + /** + * Temporarily turn off protocol tracing, e.g., to prevent + * tracing the authentication sequence, including the password. + */ + private void suspendTracing() { + if (traceLogger.isLoggable(Level.FINEST)) { + traceInput.setTrace(false); + traceOutput.setTrace(false); + } + } + + /** + * Resume protocol tracing, if it was enabled to begin with. + */ + private void resumeTracing() { + if (traceLogger.isLoggable(Level.FINEST)) { + traceInput.setTrace(true); + traceOutput.setTrace(true); + } + } + + /* + * Probe points for GlassFish monitoring. + */ + private void simpleCommandStart(String command) { } + private void simpleCommandEnd() { } + private void multilineCommandStart(String command) { } + private void multilineCommandEnd() { } + private void batchCommandStart(String command) { } + private void batchCommandContinue(String command) { } + private void batchCommandEnd() { } +}
diff --git a/mail/src/main/java/com/sun/mail/pop3/Status.java b/mail/src/main/java/com/sun/mail/pop3/Status.java new file mode 100644 index 0000000..bbfbd9c --- /dev/null +++ b/mail/src/main/java/com/sun/mail/pop3/Status.java
@@ -0,0 +1,25 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.pop3; + +/** + * Result of POP3 STAT command. + */ +class Status { + int total = 0; // number of messages in the mailbox + int size = 0; // size of the mailbox +};
diff --git a/mail/src/main/java/com/sun/mail/pop3/TempFile.java b/mail/src/main/java/com/sun/mail/pop3/TempFile.java new file mode 100644 index 0000000..0d769e4 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/pop3/TempFile.java
@@ -0,0 +1,67 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.pop3; + +import java.io.*; + +/** + * A temporary file used to cache POP3 messages. + */ +class TempFile { + + private File file; // the temp file name + private WritableSharedFile sf; + + /** + * Create a temp file in the specified directory (if not null). + * The file will be deleted when the JVM exits. + */ + public TempFile(File dir) throws IOException { + file = File.createTempFile("pop3.", ".mbox", dir); + // XXX - need JDK 6 to set permissions on the file to owner-only + file.deleteOnExit(); + sf = new WritableSharedFile(file); + } + + /** + * Return a stream for appending to the temp file. + */ + public AppendStream getAppendStream() throws IOException { + return sf.getAppendStream(); + } + + /** + * Close and remove this temp file. + */ + public void close() { + try { + sf.close(); + } catch (IOException ex) { + // ignore it + } + file.delete(); + } + + @Override + protected void finalize() throws Throwable { + try { + close(); + } finally { + super.finalize(); + } + } +}
diff --git a/mail/src/main/java/com/sun/mail/pop3/WritableSharedFile.java b/mail/src/main/java/com/sun/mail/pop3/WritableSharedFile.java new file mode 100644 index 0000000..8daa0c5 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/pop3/WritableSharedFile.java
@@ -0,0 +1,83 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.pop3; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import javax.mail.util.SharedFileInputStream; + +/** + * A subclass of SharedFileInputStream that also allows writing. + */ +class WritableSharedFile extends SharedFileInputStream { + + private RandomAccessFile raf; + private AppendStream af; + + public WritableSharedFile(File file) throws IOException { + super(file); + try { + raf = new RandomAccessFile(file, "rw"); + } catch (IOException ex) { + // if anything goes wrong opening the writable file, + // close the readable file too + super.close(); + } + } + + /** + * Return the writable version of this file. + */ + public RandomAccessFile getWritableFile() { + return raf; + } + + /** + * Close the readable and writable files. + */ + @Override + public void close() throws IOException { + try { + super.close(); + } finally { + raf.close(); + } + } + + /** + * Update the size of the readable file after writing to the file. Updates + * the length to be the current size of the file. + */ + synchronized long updateLength() throws IOException { + datalen = in.length(); + af = null; + return datalen; + } + + /** + * Return a new AppendStream, but only if one isn't in active use. + */ + public synchronized AppendStream getAppendStream() throws IOException { + if (af != null) { + throw new IOException( + "POP3 file cache only supports single threaded access"); + } + af = new AppendStream(this); + return af; + } +}
diff --git a/mail/src/main/java/com/sun/mail/pop3/package.html b/mail/src/main/java/com/sun/mail/pop3/package.html new file mode 100644 index 0000000..ed76cb7 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/pop3/package.html
@@ -0,0 +1,672 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> +<HTML> +<HEAD> +<meta http-equiv="Content-Type" content="text/html; charset=utf-8"> +<!-- + + Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<TITLE>com.sun.mail.pop3 package</TITLE> +</HEAD> +<BODY BGCOLOR="white"> + +A POP3 protocol provider for the JavaMail API +that provides access to a POP3 message store. +Refer to <A HREF="http://www.ietf.org/rfc/rfc1939.txt" TARGET="_top"> +RFC 1939</A> +for more information. +<P> +The POP3 provider provides a Store object that contains a single Folder +named "INBOX". Due to the limitations of the POP3 protocol, many of +the JavaMail API capabilities like event notification, folder management, +flag management, etc. are not allowed. The corresponding methods throw +the MethodNotSupportedException exception; see below for details. +</P> +<P> +Note that JavaMail does <strong>not</strong> include a local store into +which messages can be downloaded and stored. See our +<A HREF="http://java.sun.com/products/javamail/Third_Party.html" TARGET="_top"> +Third Party Products</A> +web page for availability of "mbox" and "MH" local store providers. +</P> +<P> +The POP3 provider is accessed through the JavaMail APIs by using the protocol +name "pop3" or a URL of the form "pop3://user:password@host:port/INBOX". +</P> +<P> +POP3 supports only a single folder named "INBOX". +</P> +<P> +POP3 supports <strong>no</strong> permanent flags (see +{@link javax.mail.Folder#getPermanentFlags Folder.getPermanentFlags()}). +In particular, the <code>Flags.Flag.RECENT</code> flag will never be set +for POP3 +messages. It's up to the application to determine which messages in a +POP3 mailbox are "new". There are several strategies to accomplish +this, depending on the needs of the application and the environment: +</P> +<UL> +<LI> +A simple approach would be to keep track of the newest +message seen by the application. +</LI> +<LI> +An alternative would be to keep track of the UIDs (see below) +of all messages that have been seen. +</LI> +<LI> +Another approach is to download <strong>all</strong> messages into a local +mailbox, so that all messages in the POP3 mailbox are, by +definition, new. +</LI> +</UL> +<P> +All approaches will require some permanent storage associated with the client. +</P> +<P> +POP3 does not support the <code>Folder.expunge()</code> method. To delete and +expunge messages, set the <code>Flags.Flag.DELETED</code> flag on the messages +and close the folder using the <code>Folder.close(true)</code> method. You +cannot expunge without closing the folder. +</P> +<P> +POP3 does not provide a "received date", so the <code>getReceivedDate</code> +method will return null. +It may be possible to examine other message headers (e.g., the +"Received" headers) to estimate the received date, but these techniques +are error-prone at best. +</P> +<P> +The POP3 provider supports the POP3 UIDL command, see +{@link com.sun.mail.pop3.POP3Folder#getUID POP3Folder.getUID()}. +You can use it as follows: +</P> +<BLOCKQUOTE><PRE> +if (folder instanceof com.sun.mail.pop3.POP3Folder) { + com.sun.mail.pop3.POP3Folder pf = + (com.sun.mail.pop3.POP3Folder)folder; + String uid = pf.getUID(msg); + if (uid != null) + ... // use it +} +</PRE></BLOCKQUOTE> +<P> +You can also pre-fetch all the UIDs for all messages like this: +</P> +<BLOCKQUOTE><PRE> +FetchProfile fp = new FetchProfile(); +fp.add(UIDFolder.FetchProfileItem.UID); +folder.fetch(folder.getMessages(), fp); +</PRE></BLOCKQUOTE> +<P> +Then use the technique above to get the UID for each message. This is +similar to the technique used with the UIDFolder interface supported by +IMAP, but note that POP3 UIDs are strings, not integers like IMAP +UIDs. See the POP3 spec for details. +</P> +<P> +When the headers of a POP3 message are accessed, the POP3 provider uses +the TOP command to fetch all headers, which are then cached. Use of the +TOP command can be disabled with the <CODE>mail.pop3.disabletop</CODE> +property, in which case the entire message content is fetched with the +RETR command. +</P> +<P> +When the content of a POP3 message is accessed, the POP3 provider uses +the RETR command to fetch the entire message. Normally the message +content is cached in memory. By setting the +<CODE>mail.pop3.filecache.enable</CODE> property, the message content +will instead be cached in a temporary file. The file will be removed +when the folder is closed. Caching message content in a file is generally +slower, but uses substantially less memory and may be helpful when dealing +with very large messages. +</P> +<P> +The {@link com.sun.mail.pop3.POP3Message#invalidate POP3Message.invalidate} +method can be used to invalidate cached data without closing the folder. +Note that if the file cache is being used the data in the file will be +forgotten and fetched from the server if it's needed again, and stored again +in the file cache. +</P> +<P> +The POP3 CAPA command (defined by +<A HREF="http://www.ietf.org/rfc/rfc2449.txt" TARGET="_top">RFC 2449</A>) +will be used to determine the capabilities supported by the server. +Some servers don't implement the CAPA command, and some servers don't +return correct information, so various properties are available to +disable use of certain POP3 commands, including CAPA. +</P> +<P> +If the server advertises the PIPELINING capability (defined by +<A HREF="http://www.ietf.org/rfc/rfc2449.txt" TARGET="_top">RFC 2449</A>), +or the <CODE>mail.pop3.pipelining</CODE> property is set, the POP3 +provider will send some commands in batches, which can significantly +improve performance and memory use. +Some servers that don't support the CAPA command or don't advertise +PIPELINING may still support pipelining; experimentation may be required. +</P> +<P> +If pipelining is supported and the connection is using +SSL, the USER and PASS commands will be sent as a batch. +(If SSL is not being used, the PASS command isn't sent +until the user is verified to avoid exposing the password +if the user name is bad.) +</P> +<P> +If pipelining is supported, when fetching a message with the RETR command, +the LIST command will be sent as well, and the result will be used to size +the I/O buffer, greatly reducing memory usage when fetching messages. +</P> +<A NAME="properties"><STRONG>Properties</STRONG></A> +<P> +The POP3 protocol provider supports the following properties, +which may be set in the JavaMail <code>Session</code> object. +The properties are always set as strings; the Type column describes +how the string is interpreted. For example, use +</P> +<PRE> + props.put("mail.pop3.port", "888"); +</PRE> +<P> +to set the <CODE>mail.pop3.port</CODE> property, which is of type int. +</P> +<P> +Note that if you're using the "pop3s" protocol to access POP3 over SSL, +all the properties would be named "mail.pop3s.*". +</P> +<TABLE BORDER SUMMARY="POP3 properties"> +<TR> +<TH>Name</TH> +<TH>Type</TH> +<TH>Description</TH> +</TR> + +<TR> +<TD><A NAME="mail.pop3.user">mail.pop3.user</A></TD> +<TD>String</TD> +<TD>Default user name for POP3.</TD> +</TR> + +<TR> +<TD><A NAME="mail.pop3.host">mail.pop3.host</A></TD> +<TD>String</TD> +<TD>The POP3 server to connect to.</TD> +</TR> + +<TR> +<TD><A NAME="mail.pop3.port">mail.pop3.port</A></TD> +<TD>int</TD> +<TD>The POP3 server port to connect to, if the connect() method doesn't +explicitly specify one. Defaults to 110.</TD> +</TR> + +<TR> +<TD><A NAME="mail.pop3.connectiontimeout">mail.pop3.connectiontimeout</A></TD> +<TD>int</TD> +<TD>Socket connection timeout value in milliseconds. +This timeout is implemented by java.net.Socket. +Default is infinite timeout.</TD> +</TR> + +<TR> +<TD><A NAME="mail.pop3.timeout">mail.pop3.timeout</A></TD> +<TD>int</TD> +<TD>Socket read timeout value in milliseconds. +This timeout is implemented by java.net.Socket. +Default is infinite timeout.</TD> +</TR> + +<TR> +<TD><A NAME="mail.pop3.writetimeout">mail.pop3.writetimeout</A></TD> +<TD>int</TD> +<TD>Socket write timeout value in milliseconds. +This timeout is implemented by using a +java.util.concurrent.ScheduledExecutorService per connection +that schedules a thread to close the socket if the timeout expires. +Thus, the overhead of using this timeout is one thread per connection. +Default is infinite timeout.</TD> +</TR> + +<TR> +<TD><A NAME="mail.pop3.rsetbeforequit">mail.pop3.rsetbeforequit</A></TD> +<TD>boolean</TD> +<TD> +Send a POP3 RSET command when closing the folder, before sending the +QUIT command. Useful with POP3 servers that implicitly mark all +messages that are read as "deleted"; this will prevent such messages +from being deleted and expunged unless the client requests so. Default +is false. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.pop3.message.class">mail.pop3.message.class</A></TD> +<TD>String</TD> +<TD> +Class name of a subclass of <code>com.sun.mail.pop3.POP3Message</code>. +The subclass can be used to handle (for example) non-standard +Content-Type headers. The subclass must have a public constructor +of the form <code>MyPOP3Message(Folder f, int msgno) +throws MessagingException</code>. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.pop3.localaddress">mail.pop3.localaddress</A></TD> +<TD>String</TD> +<TD> +Local address (host name) to bind to when creating the POP3 socket. +Defaults to the address picked by the Socket class. +Should not normally need to be set, but useful with multi-homed hosts +where it's important to pick a particular local address to bind to. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.pop3.localport">mail.pop3.localport</A></TD> +<TD>int</TD> +<TD> +Local port number to bind to when creating the POP3 socket. +Defaults to the port number picked by the Socket class. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.pop3.apop.enable">mail.pop3.apop.enable</A></TD> +<TD>boolean</TD> +<TD> +If set to true, use APOP instead of USER/PASS to login to the +POP3 server, if the POP3 server supports APOP. APOP sends a +digest of the password rather than the clear text password. +Defaults to false. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.pop3.socketFactory">mail.pop3.socketFactory</A></TD> +<TD>SocketFactory</TD> +<TD> +If set to a class that implements the +<code>javax.net.SocketFactory</code> interface, this class +will be used to create POP3 sockets. Note that this is an +instance of a class, not a name, and must be set using the +<code>put</code> method, not the <code>setProperty</code> method. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.pop3.socketFactory.class">mail.pop3.socketFactory.class</A></TD> +<TD>String</TD> +<TD> +If set, specifies the name of a class that implements the +<code>javax.net.SocketFactory</code> interface. This class +will be used to create POP3 sockets. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.pop3.socketFactory.fallback">mail.pop3.socketFactory.fallback</A></TD> +<TD>boolean</TD> +<TD> +If set to true, failure to create a socket using the specified +socket factory class will cause the socket to be created using +the <code>java.net.Socket</code> class. +Defaults to true. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.pop3.socketFactory.port">mail.pop3.socketFactory.port</A></TD> +<TD>int</TD> +<TD> +Specifies the port to connect to when using the specified socket +factory. +If not set, the default port will be used. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.pop3.ssl.enable">mail.pop3.ssl.enable</A></TD> +<TD>boolean</TD> +<TD> +If set to true, use SSL to connect and use the SSL port by default. +Defaults to false for the "pop3" protocol and true for the "pop3s" protocol. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.pop3.ssl.checkserveridentity">mail.pop3.ssl.checkserveridentity</A></TD> +<TD>boolean</TD> +<TD> +If set to true, check the server identity as specified by +<A HREF="http://www.ietf.org/rfc/rfc2595.txt" TARGET="_top">RFC 2595</A>. +These additional checks based on the content of the server's certificate +are intended to prevent man-in-the-middle attacks. +Defaults to false. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.pop3.ssl.trust">mail.pop3.ssl.trust</A></TD> +<TD>String</TD> +<TD> +If set, and a socket factory hasn't been specified, enables use of a +{@link com.sun.mail.util.MailSSLSocketFactory MailSSLSocketFactory}. +If set to "*", all hosts are trusted. +If set to a whitespace separated list of hosts, those hosts are trusted. +Otherwise, trust depends on the certificate the server presents. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.pop3.ssl.socketFactory">mail.pop3.ssl.socketFactory</A></TD> +<TD>SSLSocketFactory</TD> +<TD> +If set to a class that extends the +<code>javax.net.ssl.SSLSocketFactory</code> class, this class +will be used to create POP3 SSL sockets. Note that this is an +instance of a class, not a name, and must be set using the +<code>put</code> method, not the <code>setProperty</code> method. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.pop3.ssl.socketFactory.class">mail.pop3.ssl.socketFactory.class</A></TD> +<TD>String</TD> +<TD> +If set, specifies the name of a class that extends the +<code>javax.net.ssl.SSLSocketFactory</code> class. This class +will be used to create POP3 SSL sockets. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.pop3.ssl.socketFactory.port">mail.pop3.ssl.socketFactory.port</A></TD> +<TD>int</TD> +<TD> +Specifies the port to connect to when using the specified socket +factory. +If not set, the default port will be used. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.pop3.ssl.protocols">mail.pop3.ssl.protocols</A></TD> +<TD>string</TD> +<TD> +Specifies the SSL protocols that will be enabled for SSL connections. +The property value is a whitespace separated list of tokens acceptable +to the <code>javax.net.ssl.SSLSocket.setEnabledProtocols</code> method. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.pop3.ssl.ciphersuites">mail.pop3.ssl.ciphersuites</A></TD> +<TD>string</TD> +<TD> +Specifies the SSL cipher suites that will be enabled for SSL connections. +The property value is a whitespace separated list of tokens acceptable +to the <code>javax.net.ssl.SSLSocket.setEnabledCipherSuites</code> method. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.pop3.starttls.enable">mail.pop3.starttls.enable</A></TD> +<TD>boolean</TD> +<TD> +If true, enables the use of the <code>STLS</code> command (if +supported by the server) to switch the connection to a TLS-protected +connection before issuing any login commands. +If the server does not support STARTTLS, the connection continues without +the use of TLS; see the +<A HREF="#mail.pop3.starttls.required"><code>mail.pop3.starttls.required</code></A> +property to fail if STARTTLS isn't supported. +Note that an appropriate trust store must configured so that the client +will trust the server's certificate. +Defaults to false. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.pop3.starttls.required">mail.pop3.starttls.required</A></TD> +<TD>boolean</TD> +<TD> +If true, requires the use of the <code>STLS</code> command. +If the server doesn't support the STLS command, or the command +fails, the connect method will fail. +Defaults to false. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.pop3.proxy.host">mail.pop3.proxy.host</A></TD> +<TD>string</TD> +<TD> +Specifies the host name of an HTTP web proxy server that will be used for +connections to the mail server. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.pop3.proxy.port">mail.pop3.proxy.port</A></TD> +<TD>string</TD> +<TD> +Specifies the port number for the HTTP web proxy server. +Defaults to port 80. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.pop3.proxy.user">mail.pop3.proxy.user</A></TD> +<TD>string</TD> +<TD> +Specifies the user name to use to authenticate with the HTTP web proxy server. +By default, no authentication is done. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.pop3.proxy.password">mail.pop3.proxy.password</A></TD> +<TD>string</TD> +<TD> +Specifies the password to use to authenticate with the HTTP web proxy server. +By default, no authentication is done. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.pop3.socks.host">mail.pop3.socks.host</A></TD> +<TD>string</TD> +<TD> +Specifies the host name of a SOCKS5 proxy server that will be used for +connections to the mail server. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.pop3.socks.port">mail.pop3.socks.port</A></TD> +<TD>string</TD> +<TD> +Specifies the port number for the SOCKS5 proxy server. +This should only need to be used if the proxy server is not using +the standard port number of 1080. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.pop3.disabletop">mail.pop3.disabletop</A></TD> +<TD>boolean</TD> +<TD> +If set to true, the POP3 TOP command will not be used to fetch +message headers. This is useful for POP3 servers that don't +properly implement the TOP command, or that provide incorrect +information in the TOP command results. +Defaults to false. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.pop3.disablecapa">mail.pop3.disablecapa</A></TD> +<TD>boolean</TD> +<TD> +If set to true, the POP3 CAPA command will not be used to fetch +server capabilities. This is useful for POP3 servers that don't +properly implement the CAPA command, or that provide incorrect +information in the CAPA command results. +Defaults to false. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.pop3.forgettopheaders">mail.pop3.forgettopheaders</A></TD> +<TD>boolean</TD> +<TD> +If set to true, the headers that might have been retrieved using +the POP3 TOP command will be forgotten and replaced by headers +retrieved as part of the POP3 RETR command. Some servers, such +as some versions of Microsft Exchange and IBM Lotus Notes, +will return slightly different +headers each time the TOP or RETR command is used. To allow the +POP3 provider to properly parse the message content returned from +the RETR command, the headers also returned by the RETR command +must be used. Setting this property to true will cause these +headers to be used, even if they differ from the headers returned +previously as a result of using the TOP command. +Defaults to false. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.pop3.filecache.enable">mail.pop3.filecache.enable</A></TD> +<TD>boolean</TD> +<TD> +If set to true, the POP3 provider will cache message data in a temporary +file rather than in memory. Messages are only added to the cache when +accessing the message content. Message headers are always cached in +memory (on demand). The file cache is removed when the folder is closed +or the JVM terminates. +Defaults to false. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.pop3.filecache.dir">mail.pop3.filecache.dir</A></TD> +<TD>String</TD> +<TD> +If the file cache is enabled, this property can be used to override the +default directory used by the JDK for temporary files. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.pop3.cachewriteto">mail.pop3.cachewriteto</A></TD> +<TD>boolean</TD> +<TD> +Controls the behavior of the +{@link com.sun.mail.pop3.POP3Message#writeTo writeTo} method +on a POP3 message object. +If set to true, and the message content hasn't yet been cached, +and ignoreList is null, the message is cached before being written. +Otherwise, the message is streamed directly +to the output stream without being cached. +Defaults to false. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.pop3.keepmessagecontent">mail.pop3.keepmessagecontent</A></TD> +<TD>boolean</TD> +<TD> +The content of a message is cached when it is first fetched. +Normally this cache uses a {@link java.lang.ref.SoftReference SoftReference} +to refer to the cached content. This allows the cached content to be purged +if memory is low, in which case the content will be fetched again if it's +needed. +If this property is set to true, a hard reference to the cached content +will be kept, preventing the memory from being reused until the folder +is closed or the cached content is explicitly invalidated (using the +{@link com.sun.mail.pop3.POP3Message#invalidate invalidate} method). +(This was the behavior in previous versions of JavaMail.) +Defaults to false. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.pop3.finalizecleanclose">mail.pop3.finalizecleanclose</A></TD> +<TD>boolean</TD> +<TD> +When the finalizer for POP3Store or POP3Folder is called, +should the connection to the server be closed cleanly, as if the +application called the close method? +Or should the connection to the server be closed without sending +any commands to the server? +Defaults to false, the connection is not closed cleanly. +</TD> +</TR> + +</TABLE> +<P> +In general, applications should not need to use the classes in this +package directly. Instead, they should use the APIs defined by +<code>javax.mail</code> package (and subpackages). Applications should +never construct instances of <code>POP3Store</code> or +<code>POP3Folder</code> directly. Instead, they should use the +<code>Session</code> method <code>getStore</code> to acquire an +appropriate <code>Store</code> object, and from that acquire +<code>Folder</code> objects. +</P> +<P> +In addition to printing debugging output as controlled by the +{@link javax.mail.Session Session} configuration, +the com.sun.mail.pop3 provider logs the same information using +{@link java.util.logging.Logger} as described in the following table: +</P> +<TABLE BORDER SUMMARY="POP3 Loggers"> +<TR> +<TH>Logger Name</TH> +<TH>Logging Level</TH> +<TH>Purpose</TH> +</TR> + +<TR> +<TD>com.sun.mail.pop3</TD> +<TD>CONFIG</TD> +<TD>Configuration of the POP3Store</TD> +</TR> + +<TR> +<TD>com.sun.mail.pop3</TD> +<TD>FINE</TD> +<TD>General debugging output</TD> +</TR> + +<TR> +<TD>com.sun.mail.pop3.protocol</TD> +<TD>FINEST</TD> +<TD>Complete protocol trace</TD> +</TR> +</TABLE> + +<P> +<strong>WARNING:</strong> The APIs unique to this package should be +considered <strong>EXPERIMENTAL</strong>. They may be changed in the +future in ways that are incompatible with applications using the +current APIs. +</P> + +</BODY> +</HTML>
diff --git a/mail/src/main/java/com/sun/mail/smtp/DigestMD5.java b/mail/src/main/java/com/sun/mail/smtp/DigestMD5.java new file mode 100644 index 0000000..401834a --- /dev/null +++ b/mail/src/main/java/com/sun/mail/smtp/DigestMD5.java
@@ -0,0 +1,231 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.smtp; + +import java.io.*; +import java.util.*; +import java.util.logging.Level; +import java.security.*; +import java.nio.charset.StandardCharsets; + +import com.sun.mail.util.MailLogger; +import com.sun.mail.util.ASCIIUtility; +import com.sun.mail.util.BASE64EncoderStream; +import com.sun.mail.util.BASE64DecoderStream; + +/** + * DIGEST-MD5 authentication support. + * + * @author Dean Gibson + * @author Bill Shannon + */ + +public class DigestMD5 { + + private MailLogger logger; + private MessageDigest md5; + private String uri; + private String clientResponse; + + public DigestMD5(MailLogger logger) { + this.logger = logger.getLogger(this.getClass(), "DEBUG DIGEST-MD5"); + logger.config("DIGEST-MD5 Loaded"); + } + + /** + * Return client's authentication response to server's challenge. + * + * @param host the host name + * @param user the user name + * @param passwd the user's password + * @param realm the security realm + * @param serverChallenge the challenge from the server + * @return byte array with client's response + * @exception IOException for I/O errors + */ + public byte[] authClient(String host, String user, String passwd, + String realm, String serverChallenge) + throws IOException { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + OutputStream b64os = new BASE64EncoderStream(bos, Integer.MAX_VALUE); + SecureRandom random; + try { + //random = SecureRandom.getInstance("SHA1PRNG"); + random = new SecureRandom(); + md5 = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException ex) { + logger.log(Level.FINE, "NoSuchAlgorithmException", ex); + throw new IOException(ex.toString()); + } + StringBuilder result = new StringBuilder(); + + uri = "smtp/" + host; + String nc = "00000001"; + String qop = "auth"; + byte[] bytes = new byte[32]; // arbitrary size ... + int resp; + + logger.fine("Begin authentication ..."); + + // Code based on http://www.ietf.org/rfc/rfc2831.txt + Map<String, String> map = tokenize(serverChallenge); + + if (realm == null) { + String text = map.get("realm"); + realm = text != null ? new StringTokenizer(text, ",").nextToken() + : host; + } + + // server challenge random value + String nonce = map.get("nonce"); + + // Does server support UTF-8 usernames and passwords? + String charset = map.get("charset"); + boolean utf8 = charset != null && charset.equalsIgnoreCase("utf-8"); + + random.nextBytes(bytes); + b64os.write(bytes); + b64os.flush(); + + // client challenge random value + String cnonce = bos.toString("iso-8859-1"); // really ASCII? + bos.reset(); + + // DIGEST-MD5 computation, common portion (order critical) + if (utf8) { + String up = user + ":" + realm + ":" + passwd; + md5.update(md5.digest(up.getBytes(StandardCharsets.UTF_8))); + } else + md5.update(md5.digest( + ASCIIUtility.getBytes(user + ":" + realm + ":" + passwd))); + md5.update(ASCIIUtility.getBytes(":" + nonce + ":" + cnonce)); + clientResponse = toHex(md5.digest()) + + ":" + nonce + ":" + nc + ":" + cnonce + ":" + qop + ":"; + + // DIGEST-MD5 computation, client response (order critical) + md5.update(ASCIIUtility.getBytes("AUTHENTICATE:" + uri)); + md5.update(ASCIIUtility.getBytes(clientResponse + toHex(md5.digest()))); + + // build response text (order not critical) + result.append("username=\"" + user + "\""); + result.append(",realm=\"" + realm + "\""); + result.append(",qop=" + qop); + result.append(",nc=" + nc); + result.append(",nonce=\"" + nonce + "\""); + result.append(",cnonce=\"" + cnonce + "\""); + result.append(",digest-uri=\"" + uri + "\""); + if (utf8) + result.append(",charset=\"utf-8\""); + result.append(",response=" + toHex(md5.digest())); + + if (logger.isLoggable(Level.FINE)) + logger.fine("Response => " + result.toString()); + b64os.write(ASCIIUtility.getBytes(result.toString())); + b64os.flush(); + return bos.toByteArray(); + } + + /** + * Allow the client to authenticate the server based on its + * response. + * + * @param serverResponse the response that was received from the server + * @return true if server is authenticated + * @exception IOException for character conversion failures + */ + public boolean authServer(String serverResponse) throws IOException { + Map<String, String> map = tokenize(serverResponse); + // DIGEST-MD5 computation, server response (order critical) + md5.update(ASCIIUtility.getBytes(":" + uri)); + md5.update(ASCIIUtility.getBytes(clientResponse + toHex(md5.digest()))); + String text = toHex(md5.digest()); + if (!text.equals(map.get("rspauth"))) { + if (logger.isLoggable(Level.FINE)) + logger.fine("Expected => rspauth=" + text); + return false; // server NOT authenticated by client !!! + } + return true; + } + + /** + * Tokenize a response from the server. + * + * @return Map containing key/value pairs from server + */ + @SuppressWarnings("fallthrough") + private Map<String, String> tokenize(String serverResponse) + throws IOException { + Map<String, String> map = new HashMap<>(); + byte[] bytes = serverResponse.getBytes("iso-8859-1"); // really ASCII? + String key = null; + int ttype; + StreamTokenizer tokens + = new StreamTokenizer( + new InputStreamReader( + new BASE64DecoderStream( + new ByteArrayInputStream(bytes, 4, bytes.length - 4) + ), "iso-8859-1" // really ASCII? + ) + ); + + tokens.ordinaryChars('0', '9'); // reset digits + tokens.wordChars('0', '9'); // digits may start words + while ((ttype = tokens.nextToken()) != StreamTokenizer.TT_EOF) { + switch (ttype) { + case StreamTokenizer.TT_WORD: + if (key == null) { + key = tokens.sval; + break; + } + // fall-thru + case '"': + if (logger.isLoggable(Level.FINE)) + logger.fine("Received => " + + key + "='" + tokens.sval + "'"); + if (map.containsKey(key)) { // concatenate multiple values + map.put(key, map.get(key) + "," + tokens.sval); + } else { + map.put(key, tokens.sval); + } + key = null; + break; + default: // XXX - should never happen? + break; + } + } + return map; + } + + private static char[] digits = { + '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' + }; + + /** + * Convert a byte array to a string of hex digits representing the bytes. + */ + private static String toHex(byte[] bytes) { + char[] result = new char[bytes.length * 2]; + + for (int index = 0, i = 0; index < bytes.length; index++) { + int temp = bytes[index] & 0xFF; + result[i++] = digits[temp >> 4]; + result[i++] = digits[temp & 0xF]; + } + return new String(result); + } +}
diff --git a/mail/src/main/java/com/sun/mail/smtp/SMTPAddressFailedException.java b/mail/src/main/java/com/sun/mail/smtp/SMTPAddressFailedException.java new file mode 100644 index 0000000..234820e --- /dev/null +++ b/mail/src/main/java/com/sun/mail/smtp/SMTPAddressFailedException.java
@@ -0,0 +1,87 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.smtp; + +import javax.mail.SendFailedException; +import javax.mail.internet.InternetAddress; + +/** + * This exception is thrown when the message cannot be sent. <p> + * + * The exception includes the address to which the message could not be + * sent. This will usually appear in a chained list of exceptions, + * one per address, attached to a top level SendFailedException that + * aggregates all the addresses. + * + * @since JavaMail 1.3.2 + */ + +public class SMTPAddressFailedException extends SendFailedException { + protected InternetAddress addr; // address that failed + protected String cmd; // command issued to server + protected int rc; // return code from SMTP server + + private static final long serialVersionUID = 804831199768630097L; + + /** + * Constructs an SMTPAddressFailedException with the specified + * address, return code, and error string. + * + * @param addr the address that failed + * @param cmd the command that was sent to the SMTP server + * @param rc the SMTP return code indicating the failure + * @param err the error string from the SMTP server + */ + public SMTPAddressFailedException(InternetAddress addr, String cmd, int rc, + String err) { + super(err); + this.addr = addr; + this.cmd = cmd; + this.rc = rc; + } + + /** + * Return the address that failed. + * + * @return the address + */ + public InternetAddress getAddress() { + return addr; + } + + /** + * Return the command that failed. + * + * @return the command + */ + public String getCommand() { + return cmd; + } + + + /** + * Return the return code from the SMTP server that indicates the + * reason for the failure. See + * <A HREF="http://www.ietf.org/rfc/rfc821.txt">RFC 821</A> + * for interpretation of the return code. + * + * @return the return code + */ + public int getReturnCode() { + return rc; + } +}
diff --git a/mail/src/main/java/com/sun/mail/smtp/SMTPAddressSucceededException.java b/mail/src/main/java/com/sun/mail/smtp/SMTPAddressSucceededException.java new file mode 100644 index 0000000..2ddb29a --- /dev/null +++ b/mail/src/main/java/com/sun/mail/smtp/SMTPAddressSucceededException.java
@@ -0,0 +1,86 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.smtp; + +import javax.mail.MessagingException; +import javax.mail.internet.InternetAddress; + +/** + * This exception is chained off a SendFailedException when the + * <code>mail.smtp.reportsuccess</code> property is true. It + * indicates an address to which the message was sent. The command + * will be an SMTP RCPT command and the return code will be the + * return code from that command. + * + * @since JavaMail 1.3.2 + */ + +public class SMTPAddressSucceededException extends MessagingException { + protected InternetAddress addr; // address that succeeded + protected String cmd; // command issued to server + protected int rc; // return code from SMTP server + + private static final long serialVersionUID = -1168335848623096749L; + + /** + * Constructs an SMTPAddressSucceededException with the specified + * address, return code, and error string. + * + * @param addr the address that succeeded + * @param cmd the command that was sent to the SMTP server + * @param rc the SMTP return code indicating the success + * @param err the error string from the SMTP server + */ + public SMTPAddressSucceededException(InternetAddress addr, + String cmd, int rc, String err) { + super(err); + this.addr = addr; + this.cmd = cmd; + this.rc = rc; + } + + /** + * Return the address that succeeded. + * + * @return the address + */ + public InternetAddress getAddress() { + return addr; + } + + /** + * Return the command that succeeded. + * + * @return the command + */ + public String getCommand() { + return cmd; + } + + + /** + * Return the return code from the SMTP server that indicates the + * reason for the success. See + * <A HREF="http://www.ietf.org/rfc/rfc821.txt">RFC 821</A> + * for interpretation of the return code. + * + * @return the return code + */ + public int getReturnCode() { + return rc; + } +}
diff --git a/mail/src/main/java/com/sun/mail/smtp/SMTPMessage.java b/mail/src/main/java/com/sun/mail/smtp/SMTPMessage.java new file mode 100644 index 0000000..826ea44 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/smtp/SMTPMessage.java
@@ -0,0 +1,321 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.smtp; + +import java.io.*; +import javax.mail.*; +import javax.mail.internet.*; + +/** + * This class is a specialization of the MimeMessage class that allows + * you to specify various SMTP options and parameters that will be + * used when this message is sent over SMTP. Simply use this class + * instead of MimeMessage and set SMTP options using the methods on + * this class. <p> + * + * See the <a href="package-summary.html">com.sun.mail.smtp</a> package + * documentation for further information on the SMTP protocol provider. <p> + * + * @author Bill Shannon + * @see javax.mail.internet.MimeMessage + */ + +public class SMTPMessage extends MimeMessage { + + /** Never notify of delivery status */ + public static final int NOTIFY_NEVER = -1; + /** Notify of delivery success */ + public static final int NOTIFY_SUCCESS = 1; + /** Notify of delivery failure */ + public static final int NOTIFY_FAILURE = 2; + /** Notify of delivery delay */ + public static final int NOTIFY_DELAY = 4; + + /** Return full message with delivery status notification */ + public static final int RETURN_FULL = 1; + /** Return only message headers with delivery status notification */ + public static final int RETURN_HDRS = 2; + + private static final String[] returnOptionString = { null, "FULL", "HDRS" }; + + private String envelopeFrom; // the string to use in the MAIL FROM: command + private int notifyOptions = 0; + private int returnOption = 0; + private boolean sendPartial = false; + private boolean allow8bitMIME = false; + private String submitter = null; // RFC 2554 AUTH=submitter + private String extension = null; // extensions to use with MAIL command + + /** + * Default constructor. An empty message object is created. + * The <code>headers</code> field is set to an empty InternetHeaders + * object. The <code>flags</code> field is set to an empty Flags + * object. The <code>modified</code> flag is set to true. + * + * @param session the Session + */ + public SMTPMessage(Session session) { + super(session); + } + + /** + * Constructs an SMTPMessage by reading and parsing the data from the + * specified MIME InputStream. The InputStream will be left positioned + * at the end of the data for the message. Note that the input stream + * parse is done within this constructor itself. + * + * @param session Session object for this message + * @param is the message input stream + * @exception MessagingException for failures + */ + public SMTPMessage(Session session, InputStream is) + throws MessagingException { + super(session, is); + } + + /** + * Constructs a new SMTPMessage with content initialized from the + * <code>source</code> MimeMessage. The new message is independent + * of the original. <p> + * + * Note: The current implementation is rather inefficient, copying + * the data more times than strictly necessary. + * + * @param source the message to copy content from + * @exception MessagingException for failures + */ + public SMTPMessage(MimeMessage source) throws MessagingException { + super(source); + } + + /** + * Set the From address to appear in the SMTP envelope. Note that this + * is different than the From address that appears in the message itself. + * The envelope From address is typically used when reporting errors. + * See <A HREF="http://www.ietf.org/rfc/rfc821.txt">RFC 821</A> for + * details. <p> + * + * If set, overrides the <code>mail.smtp.from</code> property. + * + * @param from the envelope From address + */ + public void setEnvelopeFrom(String from) { + envelopeFrom = from; + } + + /** + * Return the envelope From address. + * + * @return the envelope From address, or null if not set + */ + public String getEnvelopeFrom() { + return envelopeFrom; + } + + /** + * Set notification options to be used if the server supports + * Delivery Status Notification + * (<A HREF="http://www.ietf.org/rfc/rfc1891.txt">RFC 1891</A>). + * Either <code>NOTIFY_NEVER</code> or some combination of + * <code>NOTIFY_SUCCESS</code>, <code>NOTIFY_FAILURE</code>, and + * <code>NOTIFY_DELAY</code>. <p> + * + * If set, overrides the <code>mail.smtp.dsn.notify</code> property. + * + * @param options notification options + */ + public void setNotifyOptions(int options) { + if (options < -1 || options >= 8) + throw new IllegalArgumentException("Bad return option"); + notifyOptions = options; + } + + /** + * Get notification options. Returns zero if no options set. + * + * @return notification options + */ + public int getNotifyOptions() { + return notifyOptions; + } + + /** + * Return notification options as an RFC 1891 string. + * Returns null if no options set. + */ + String getDSNNotify() { + if (notifyOptions == 0) + return null; + if (notifyOptions == NOTIFY_NEVER) + return "NEVER"; + StringBuilder sb = new StringBuilder(); + if ((notifyOptions & NOTIFY_SUCCESS) != 0) + sb.append("SUCCESS"); + if ((notifyOptions & NOTIFY_FAILURE) != 0) { + if (sb.length() != 0) + sb.append(','); + sb.append("FAILURE"); + } + if ((notifyOptions & NOTIFY_DELAY) != 0) { + if (sb.length() != 0) + sb.append(','); + sb.append("DELAY"); + } + return sb.toString(); + } + + /** + * Set return option to be used if server supports + * Delivery Status Notification + * (<A HREF="http://www.ietf.org/rfc/rfc1891.txt">RFC 1891</A>). + * Either <code>RETURN_FULL</code> or <code>RETURN_HDRS</code>. <p> + * + * If set, overrides the <code>mail.smtp.dsn.ret</code> property. + * + * @param option return option + */ + public void setReturnOption(int option) { + if (option < 0 || option > RETURN_HDRS) + throw new IllegalArgumentException("Bad return option"); + returnOption = option; + } + + /** + * Return return option. Returns zero if no option set. + * + * @return return option + */ + public int getReturnOption() { + return returnOption; + } + + /** + * Return return option as an RFC 1891 string. + * Returns null if no option set. + */ + String getDSNRet() { + return returnOptionString[returnOption]; + } + + /** + * If set to true, and the server supports the 8BITMIME extension, text + * parts of this message that use the "quoted-printable" or "base64" + * encodings are converted to use "8bit" encoding if they follow the + * RFC 2045 rules for 8bit text. <p> + * + * If true, overrides the <code>mail.smtp.allow8bitmime</code> property. + * + * @param allow allow 8-bit flag + */ + public void setAllow8bitMIME(boolean allow) { + allow8bitMIME = allow; + } + + /** + * Is use of the 8BITMIME extension is allowed? + * + * @return allow 8-bit flag + */ + public boolean getAllow8bitMIME() { + return allow8bitMIME; + } + + /** + * If set to true, and this message has some valid and some invalid + * addresses, send the message anyway, reporting the partial failure with + * a SendFailedException. If set to false (the default), the message is + * not sent to any of the recipients if there is an invalid recipient + * address. <p> + * + * If true, overrides the <code>mail.smtp.sendpartial</code> property. + * + * @param partial send partial flag + */ + public void setSendPartial(boolean partial) { + sendPartial = partial; + } + + /** + * Send message if some addresses are invalid? + * + * @return send partial flag + */ + public boolean getSendPartial() { + return sendPartial; + } + + /** + * Gets the submitter to be used for the RFC 2554 AUTH= value + * in the MAIL FROM command. + * + * @return the name of the submitter. + */ + public String getSubmitter() { + return submitter; + } + + /** + * Sets the submitter to be used for the RFC 2554 AUTH= value + * in the MAIL FROM command. Normally only used by a server + * that's relaying a message. Clients will typically not + * set a submitter. See + * <A HREF="http://www.ietf.org/rfc/rfc2554.txt">RFC 2554</A> + * for details. + * + * @param submitter the name of the submitter + */ + public void setSubmitter(String submitter) { + this.submitter = submitter; + } + + /** + * Gets the extension string to use with the MAIL command. + * + * @return the extension string + * + * @since JavaMail 1.3.2 + */ + public String getMailExtension() { + return extension; + } + + /** + * Set the extension string to use with the MAIL command. + * The extension string can be used to specify standard SMTP + * service extensions as well as vendor-specific extensions. + * Typically the application should use the + * {@link com.sun.mail.smtp.SMTPTransport SMTPTransport} + * method {@link com.sun.mail.smtp.SMTPTransport#supportsExtension + * supportsExtension} + * to verify that the server supports the desired service extension. + * See <A HREF="http://www.ietf.org/rfc/rfc1869.txt">RFC 1869</A> + * and other RFCs that define specific extensions. <p> + * + * For example: + * + * <blockquote><pre> + * if (smtpTransport.supportsExtension("DELIVERBY")) + * smtpMsg.setMailExtension("BY=60;R"); + * </pre></blockquote> + * + * @param extension the extension string + * @since JavaMail 1.3.2 + */ + public void setMailExtension(String extension) { + this.extension = extension; + } +}
diff --git a/mail/src/main/java/com/sun/mail/smtp/SMTPOutputStream.java b/mail/src/main/java/com/sun/mail/smtp/SMTPOutputStream.java new file mode 100644 index 0000000..d2fd75b --- /dev/null +++ b/mail/src/main/java/com/sun/mail/smtp/SMTPOutputStream.java
@@ -0,0 +1,95 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.smtp; + +import java.io.*; +import com.sun.mail.util.CRLFOutputStream; + +/** + * In addition to converting lines into the canonical format, + * i.e., terminating lines with the CRLF sequence, escapes the "." + * by adding another "." to any "." that appears in the beginning + * of a line. See RFC821 section 4.5.2. + * + * @author Max Spivak + * @see CRLFOutputStream + */ +public class SMTPOutputStream extends CRLFOutputStream { + public SMTPOutputStream(OutputStream os) { + super(os); + } + + @Override + public void write(int b) throws IOException { + // if that last character was a newline, and the current + // character is ".", we always write out an extra ".". + if ((lastb == '\n' || lastb == '\r' || lastb == -1) && b == '.') { + out.write('.'); + } + + super.write(b); + } + + /* + * This method has been added to improve performance. + */ + @Override + public void write(byte b[], int off, int len) throws IOException { + int lastc = (lastb == -1) ? '\n' : lastb; + int start = off; + + len += off; + for (int i = off; i < len; i++) { + if ((lastc == '\n' || lastc == '\r') && b[i] == '.') { + super.write(b, start, i - start); + out.write('.'); + start = i; + } + lastc = b[i]; + } + if ((len - start) > 0) + super.write(b, start, len - start); + } + + /** + * Override flush method in FilterOutputStream. + * + * The MimeMessage writeTo method flushes its buffer at the end, + * but we don't want to flush data out to the socket until we've + * also written the terminating "\r\n.\r\n". + * + * We buffer nothing so there's nothing to flush. We depend + * on the fact that CRLFOutputStream also buffers nothing. + * SMTPTransport will manually flush the socket before reading + * the response. + */ + @Override + public void flush() { + // do nothing + } + + /** + * Ensure we're at the beginning of a line. + * Write CRLF if not. + * + * @exception IOException if the write fails + */ + public void ensureAtBOL() throws IOException { + if (!atBOL) + super.writeln(); + } +}
diff --git a/mail/src/main/java/com/sun/mail/smtp/SMTPProvider.java b/mail/src/main/java/com/sun/mail/smtp/SMTPProvider.java new file mode 100644 index 0000000..3b5c1e9 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/smtp/SMTPProvider.java
@@ -0,0 +1,29 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.smtp; + +import javax.mail.Provider; + +/** + * The SMTP protocol provider. + */ +public class SMTPProvider extends Provider { + public SMTPProvider() { + super(Provider.Type.TRANSPORT, "smtp", SMTPTransport.class.getName(), + "Oracle", null); + } +}
diff --git a/mail/src/main/java/com/sun/mail/smtp/SMTPSSLProvider.java b/mail/src/main/java/com/sun/mail/smtp/SMTPSSLProvider.java new file mode 100644 index 0000000..40a2acd --- /dev/null +++ b/mail/src/main/java/com/sun/mail/smtp/SMTPSSLProvider.java
@@ -0,0 +1,29 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.smtp; + +import javax.mail.Provider; + +/** + * The SMTP SSL protocol provider. + */ +public class SMTPSSLProvider extends Provider { + public SMTPSSLProvider() { + super(Provider.Type.TRANSPORT, "smtps", + SMTPSSLTransport.class.getName(), "Oracle", null); + } +}
diff --git a/mail/src/main/java/com/sun/mail/smtp/SMTPSSLTransport.java b/mail/src/main/java/com/sun/mail/smtp/SMTPSSLTransport.java new file mode 100644 index 0000000..f3a7300 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/smtp/SMTPSSLTransport.java
@@ -0,0 +1,39 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.smtp; + +import javax.mail.*; + +/** + * This class implements the Transport abstract class using SMTP + * over SSL for message submission and transport. + * + * @author Bill Shannon + */ + +public class SMTPSSLTransport extends SMTPTransport { + + /** + * Constructor. + * + * @param session the Session + * @param urlname the URLName of this transport + */ + public SMTPSSLTransport(Session session, URLName urlname) { + super(session, urlname, "smtps", true); + } +}
diff --git a/mail/src/main/java/com/sun/mail/smtp/SMTPSaslAuthenticator.java b/mail/src/main/java/com/sun/mail/smtp/SMTPSaslAuthenticator.java new file mode 100644 index 0000000..70545a5 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/smtp/SMTPSaslAuthenticator.java
@@ -0,0 +1,228 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.smtp; + +import java.io.*; +import java.util.*; +import java.util.logging.Level; +import javax.security.sasl.*; +import javax.security.auth.callback.*; +import javax.mail.MessagingException; + +import com.sun.mail.util.MailLogger; +import com.sun.mail.util.ASCIIUtility; +import com.sun.mail.util.BASE64EncoderStream; +import com.sun.mail.util.BASE64DecoderStream; + +/** + * This class contains a single method that does authentication using + * SASL. This is in a separate class so that it can be compiled with + * J2SE 1.5. Eventually it should be merged into SMTPTransport.java. + */ + +public class SMTPSaslAuthenticator implements SaslAuthenticator { + + private SMTPTransport pr; + private String name; + private Properties props; + private MailLogger logger; + private String host; + + /* + * This is a hack to initialize the OAUTH SASL provider just before, + * and only if, we might need it. This avoids the need for the user + * to initialize it explicitly, or manually configure the security + * providers file. + */ + static { + try { + com.sun.mail.auth.OAuth2SaslClientFactory.init(); + } catch (Throwable t) { } + } + + public SMTPSaslAuthenticator(SMTPTransport pr, String name, + Properties props, MailLogger logger, String host) { + this.pr = pr; + this.name = name; + this.props = props; + this.logger = logger; + this.host = host; + } + + @Override + public boolean authenticate(String[] mechs, final String realm, + final String authzid, final String u, + final String p) throws MessagingException { + + boolean done = false; + if (logger.isLoggable(Level.FINE)) { + logger.fine("SASL Mechanisms:"); + for (int i = 0; i < mechs.length; i++) + logger.fine(" " + mechs[i]); + logger.fine(""); + } + + SaslClient sc; + CallbackHandler cbh = new CallbackHandler() { + @Override + public void handle(Callback[] callbacks) { + if (logger.isLoggable(Level.FINE)) + logger.fine("SASL callback length: " + callbacks.length); + for (int i = 0; i < callbacks.length; i++) { + if (logger.isLoggable(Level.FINE)) + logger.fine("SASL callback " + i + ": " + callbacks[i]); + if (callbacks[i] instanceof NameCallback) { + NameCallback ncb = (NameCallback)callbacks[i]; + ncb.setName(u); + } else if (callbacks[i] instanceof PasswordCallback) { + PasswordCallback pcb = (PasswordCallback)callbacks[i]; + pcb.setPassword(p.toCharArray()); + } else if (callbacks[i] instanceof RealmCallback) { + RealmCallback rcb = (RealmCallback)callbacks[i]; + rcb.setText(realm != null ? + realm : rcb.getDefaultText()); + } else if (callbacks[i] instanceof RealmChoiceCallback) { + RealmChoiceCallback rcb = + (RealmChoiceCallback)callbacks[i]; + if (realm == null) + rcb.setSelectedIndex(rcb.getDefaultChoice()); + else { + // need to find specified realm in list + String[] choices = rcb.getChoices(); + for (int k = 0; k < choices.length; k++) { + if (choices[k].equals(realm)) { + rcb.setSelectedIndex(k); + break; + } + } + } + } + } + } + }; + + try { + @SuppressWarnings("unchecked") + Map<String, ?> propsMap = (Map) props; + sc = Sasl.createSaslClient(mechs, authzid, name, host, + propsMap, cbh); + } catch (SaslException sex) { + logger.log(Level.FINE, "Failed to create SASL client", sex); + throw new UnsupportedOperationException(sex.getMessage(), sex); + } + if (sc == null) { + logger.fine("No SASL support"); + throw new UnsupportedOperationException("No SASL support"); + } + if (logger.isLoggable(Level.FINE)) + logger.fine("SASL client " + sc.getMechanismName()); + + int resp; + try { + String mech = sc.getMechanismName(); + String ir = null; + if (sc.hasInitialResponse()) { + byte[] ba = sc.evaluateChallenge(new byte[0]); + if (ba.length > 0) { + ba = BASE64EncoderStream.encode(ba); + ir = ASCIIUtility.toString(ba, 0, ba.length); + } else + ir = "="; + } + if (ir != null) + resp = pr.simpleCommand("AUTH " + mech + " " + ir); + else + resp = pr.simpleCommand("AUTH " + mech); + + /* + * A 530 response indicates that the server wants us to + * issue a STARTTLS command first. Do that and try again. + */ + if (resp == 530) { + pr.startTLS(); + if (ir != null) + resp = pr.simpleCommand("AUTH " + mech + " " + ir); + else + resp = pr.simpleCommand("AUTH " + mech); + } + + if (resp == 235) + return true; // success already! + + if (resp != 334) + return false; + } catch (Exception ex) { + logger.log(Level.FINE, "SASL AUTHENTICATE Exception", ex); + return false; + } + + while (!done) { // loop till we are done + try { + if (resp == 334) { + byte[] ba = null; + if (!sc.isComplete()) { + ba = ASCIIUtility.getBytes(responseText(pr)); + if (ba.length > 0) + ba = BASE64DecoderStream.decode(ba); + if (logger.isLoggable(Level.FINE)) + logger.fine("SASL challenge: " + + ASCIIUtility.toString(ba, 0, ba.length) + " :"); + ba = sc.evaluateChallenge(ba); + } + if (ba == null) { + logger.fine("SASL: no response"); + resp = pr.simpleCommand(""); + } else { + if (logger.isLoggable(Level.FINE)) + logger.fine("SASL response: " + + ASCIIUtility.toString(ba, 0, ba.length) + " :"); + ba = BASE64EncoderStream.encode(ba); + resp = pr.simpleCommand(ba); + } + } else + done = true; + } catch (Exception ioex) { + logger.log(Level.FINE, "SASL Exception", ioex); + done = true; + // XXX - ultimately return true??? + } + } + if (resp != 235) + return false; + + if (sc.isComplete() /*&& res.status == SUCCESS*/) { + String qop = (String)sc.getNegotiatedProperty(Sasl.QOP); + if (qop != null && (qop.equalsIgnoreCase("auth-int") || + qop.equalsIgnoreCase("auth-conf"))) { + // XXX - NOT SUPPORTED!!! + logger.fine( + "SASL Mechanism requires integrity or confidentiality"); + return false; + } + } + + return true; + } + + private static final String responseText(SMTPTransport pr) { + String resp = pr.getLastServerResponse().trim(); + if (resp.length() > 4) + return resp.substring(4); + else + return ""; + } +}
diff --git a/mail/src/main/java/com/sun/mail/smtp/SMTPSendFailedException.java b/mail/src/main/java/com/sun/mail/smtp/SMTPSendFailedException.java new file mode 100644 index 0000000..d8cf3e6 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/smtp/SMTPSendFailedException.java
@@ -0,0 +1,82 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.smtp; + +import javax.mail.Address; +import javax.mail.SendFailedException; +import javax.mail.internet.InternetAddress; + +/** + * This exception is thrown when the message cannot be sent. <p> + * + * This exception will usually appear first in a chained list of exceptions, + * followed by SMTPAddressFailedExceptions and/or + * SMTPAddressSucceededExceptions, * one per address. + * This exception corresponds to one of the SMTP commands used to + * send a message, such as the MAIL, DATA, and "end of data" commands, + * but not including the RCPT command. + * + * @since JavaMail 1.3.2 + */ + +public class SMTPSendFailedException extends SendFailedException { + protected InternetAddress addr; // address that failed + protected String cmd; // command issued to server + protected int rc; // return code from SMTP server + + private static final long serialVersionUID = 8049122628728932894L; + + /** + * Constructs an SMTPSendFailedException with the specified + * address, return code, and error string. + * + * @param cmd the command that was sent to the SMTP server + * @param rc the SMTP return code indicating the failure + * @param err the error string from the SMTP server + * @param ex a chained exception + * @param vs the valid addresses the message was sent to + * @param vus the valid addresses the message was not sent to + * @param inv the invalid addresses + */ + public SMTPSendFailedException(String cmd, int rc, String err, Exception ex, + Address[] vs, Address[] vus, Address[] inv) { + super(err, ex, vs, vus, inv); + this.cmd = cmd; + this.rc = rc; + } + + /** + * Return the command that failed. + * + * @return the command + */ + public String getCommand() { + return cmd; + } + + /** + * Return the return code from the SMTP server that indicates the + * reason for the failure. See + * <A HREF="http://www.ietf.org/rfc/rfc821.txt">RFC 821</A> + * for interpretation of the return code. + * + * @return the return code + */ + public int getReturnCode() { + return rc; + } +}
diff --git a/mail/src/main/java/com/sun/mail/smtp/SMTPSenderFailedException.java b/mail/src/main/java/com/sun/mail/smtp/SMTPSenderFailedException.java new file mode 100644 index 0000000..3139c79 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/smtp/SMTPSenderFailedException.java
@@ -0,0 +1,85 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.smtp; + +import javax.mail.SendFailedException; +import javax.mail.internet.InternetAddress; + +/** + * This exception is thrown when the message cannot be sent. <p> + * + * The exception includes the sender's address, which the mail server + * rejected. + * + * @since JavaMail 1.4.4 + */ + +public class SMTPSenderFailedException extends SendFailedException { + protected InternetAddress addr; // address that failed + protected String cmd; // command issued to server + protected int rc; // return code from SMTP server + + private static final long serialVersionUID = 514540454964476947L; + + /** + * Constructs an SMTPSenderFailedException with the specified + * address, return code, and error string. + * + * @param addr the address that failed + * @param cmd the command that was sent to the SMTP server + * @param rc the SMTP return code indicating the failure + * @param err the error string from the SMTP server + */ + public SMTPSenderFailedException(InternetAddress addr, String cmd, int rc, + String err) { + super(err); + this.addr = addr; + this.cmd = cmd; + this.rc = rc; + } + + /** + * Return the address that failed. + * + * @return the address + */ + public InternetAddress getAddress() { + return addr; + } + + /** + * Return the command that failed. + * + * @return the command + */ + public String getCommand() { + return cmd; + } + + + /** + * Return the return code from the SMTP server that indicates the + * reason for the failure. See + * <A HREF="http://www.ietf.org/rfc/rfc821.txt">RFC 821</A> + * for interpretation of the return code. + * + * @return the return code + */ + public int getReturnCode() { + return rc; + } +}
diff --git a/mail/src/main/java/com/sun/mail/smtp/SMTPTransport.java b/mail/src/main/java/com/sun/mail/smtp/SMTPTransport.java new file mode 100644 index 0000000..6b2acfb --- /dev/null +++ b/mail/src/main/java/com/sun/mail/smtp/SMTPTransport.java
@@ -0,0 +1,2791 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.smtp; + +import java.io.*; +import java.net.*; +import java.util.*; +import java.util.logging.Level; +import java.lang.reflect.*; +import java.nio.charset.StandardCharsets; +import javax.net.ssl.SSLSocket; + +import javax.mail.*; +import javax.mail.event.*; +import javax.mail.internet.*; + +import com.sun.mail.util.PropUtil; +import com.sun.mail.util.MailLogger; +import com.sun.mail.util.ASCIIUtility; +import com.sun.mail.util.SocketFetcher; +import com.sun.mail.util.MailConnectException; +import com.sun.mail.util.SocketConnectException; +import com.sun.mail.util.BASE64EncoderStream; +import com.sun.mail.util.LineInputStream; +import com.sun.mail.util.TraceInputStream; +import com.sun.mail.util.TraceOutputStream; +import com.sun.mail.auth.Ntlm; + +/** + * This class implements the Transport abstract class using SMTP for + * message submission and transport. <p> + * + * See the <a href="package-summary.html">com.sun.mail.smtp</a> package + * documentation for further information on the SMTP protocol provider. <p> + * + * This class includes many protected methods that allow a subclass to + * extend this class and add support for non-standard SMTP commands. + * The {@link #issueCommand} and {@link #sendCommand} methods can be + * used to send simple SMTP commands. Other methods such as the + * {@link #mailFrom} and {@link #data} methods can be overridden to + * insert new commands before or after the corresponding SMTP commands. + * For example, a subclass could do this to send the XACT command + * before sending the DATA command: + * <pre> + * protected OutputStream data() throws MessagingException { + * if (supportsExtension("XACCOUNTING")) + * issueCommand("XACT", 25); + * return super.data(); + * } + * </pre> + * + * @author Max Spivak + * @author Bill Shannon + * @author Dean Gibson (DIGEST-MD5 authentication) + * @author Lu\u00EDs Serralheiro (NTLM authentication) + * + * @see javax.mail.event.ConnectionEvent + * @see javax.mail.event.TransportEvent + */ + +public class SMTPTransport extends Transport { + + private String name = "smtp"; // Name of this protocol + private int defaultPort = 25; // default SMTP port + private boolean isSSL = false; // use SSL? + private String host; // host we're connected to + + // Following fields valid only during the sendMessage method. + private MimeMessage message; // Message to be sent + private Address[] addresses; // Addresses to which to send the msg + // Valid sent, valid unsent and invalid addresses + private Address[] validSentAddr, validUnsentAddr, invalidAddr; + // Did we send the message even though some addresses were invalid? + private boolean sendPartiallyFailed = false; + // If so, here's an exception we need to throw + private MessagingException exception; + // stream where message data is written + private SMTPOutputStream dataStream; + + // Map of SMTP service extensions supported by server, if EHLO used. + private Hashtable<String, String> extMap; + + private Map<String, Authenticator> authenticators + = new HashMap<>(); + private String defaultAuthenticationMechanisms; // set in constructor + + private boolean quitWait = false; // true if we should wait + + private String saslRealm = UNKNOWN; + private String authorizationID = UNKNOWN; + private boolean enableSASL = false; // enable SASL authentication + private boolean useCanonicalHostName = false; // use canonical host name? + private String[] saslMechanisms = UNKNOWN_SA; + + private String ntlmDomain = UNKNOWN; // for ntlm authentication + + private boolean reportSuccess; // throw an exception even on success + private boolean useStartTLS; // use STARTTLS command + private boolean requireStartTLS; // require STARTTLS command + private boolean useRset; // use RSET instead of NOOP + private boolean noopStrict = true; // NOOP must return 250 for success + + private MailLogger logger; // debug logger + private MailLogger traceLogger; // protocol trace logger + private String localHostName; // our own host name + private String lastServerResponse; // last SMTP response + private int lastReturnCode; // last SMTP return code + private boolean notificationDone; // only notify once per send + + private SaslAuthenticator saslAuthenticator; // if SASL is being used + + private boolean noauthdebug = true; // hide auth info in debug output + private boolean debugusername; // include username in debug output? + private boolean debugpassword; // include password in debug output? + private boolean allowutf8; // allow UTF-8 usernames and passwords? + private int chunkSize; // chunk size if CHUNKING supported + + /** Headers that should not be included when sending */ + private static final String[] ignoreList = { "Bcc", "Content-Length" }; + private static final byte[] CRLF = { (byte)'\r', (byte)'\n' }; + private static final String UNKNOWN = "UNKNOWN"; // place holder + private static final String[] UNKNOWN_SA = new String[0]; // place holder + + /** + * Constructor that takes a Session object and a URLName + * that represents a specific SMTP server. + * + * @param session the Session + * @param urlname the URLName of this transport + */ + public SMTPTransport(Session session, URLName urlname) { + this(session, urlname, "smtp", false); + } + + /** + * Constructor used by this class and by SMTPSSLTransport subclass. + * + * @param session the Session + * @param urlname the URLName of this transport + * @param name the protocol name of this transport + * @param isSSL use SSL to connect? + */ + protected SMTPTransport(Session session, URLName urlname, + String name, boolean isSSL) { + super(session, urlname); + Properties props = session.getProperties(); + + logger = new MailLogger(this.getClass(), "DEBUG SMTP", + session.getDebug(), session.getDebugOut()); + traceLogger = logger.getSubLogger("protocol", null); + noauthdebug = !PropUtil.getBooleanProperty(props, + "mail.debug.auth", false); + debugusername = PropUtil.getBooleanProperty(props, + "mail.debug.auth.username", true); + debugpassword = PropUtil.getBooleanProperty(props, + "mail.debug.auth.password", false); + if (urlname != null) + name = urlname.getProtocol(); + this.name = name; + if (!isSSL) + isSSL = PropUtil.getBooleanProperty(props, + "mail." + name + ".ssl.enable", false); + if (isSSL) + this.defaultPort = 465; + else + this.defaultPort = 25; + this.isSSL = isSSL; + + // setting mail.smtp.quitwait to false causes us to not wait for the + // response from the QUIT command + quitWait = PropUtil.getBooleanProperty(props, + "mail." + name + ".quitwait", true); + + // mail.smtp.reportsuccess causes us to throw an exception on success + reportSuccess = PropUtil.getBooleanProperty(props, + "mail." + name + ".reportsuccess", false); + + // mail.smtp.starttls.enable enables use of STARTTLS command + useStartTLS = PropUtil.getBooleanProperty(props, + "mail." + name + ".starttls.enable", false); + + // mail.smtp.starttls.required requires use of STARTTLS command + requireStartTLS = PropUtil.getBooleanProperty(props, + "mail." + name + ".starttls.required", false); + + // mail.smtp.userset causes us to use RSET instead of NOOP + // for isConnected + useRset = PropUtil.getBooleanProperty(props, + "mail." + name + ".userset", false); + + // mail.smtp.noop.strict requires 250 response to indicate success + noopStrict = PropUtil.getBooleanProperty(props, + "mail." + name + ".noop.strict", true); + + // check if SASL is enabled + enableSASL = PropUtil.getBooleanProperty(props, + "mail." + name + ".sasl.enable", false); + if (enableSASL) + logger.config("enable SASL"); + useCanonicalHostName = PropUtil.getBooleanProperty(props, + "mail." + name + ".sasl.usecanonicalhostname", false); + if (useCanonicalHostName) + logger.config("use canonical host name"); + + allowutf8 = PropUtil.getBooleanProperty(props, + "mail.mime.allowutf8", false); + if (allowutf8) + logger.config("allow UTF-8"); + + chunkSize = PropUtil.getIntProperty(props, + "mail." + name + ".chunksize", -1); + if (chunkSize > 0 && logger.isLoggable(Level.CONFIG)) + logger.config("chunk size " + chunkSize); + + // created here, because they're inner classes that reference "this" + Authenticator[] a = new Authenticator[] { + new LoginAuthenticator(), + new PlainAuthenticator(), + new DigestMD5Authenticator(), + new NtlmAuthenticator(), + new OAuth2Authenticator() + }; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < a.length; i++) { + authenticators.put(a[i].getMechanism(), a[i]); + sb.append(a[i].getMechanism()).append(' '); + } + defaultAuthenticationMechanisms = sb.toString(); + } + + /** + * Get the name of the local host, for use in the EHLO and HELO commands. + * The property mail.smtp.localhost overrides mail.smtp.localaddress, + * which overrides what InetAddress would tell us. + * + * @return the local host name + */ + public synchronized String getLocalHost() { + // get our hostname and cache it for future use + if (localHostName == null || localHostName.length() <= 0) + localHostName = + session.getProperty("mail." + name + ".localhost"); + if (localHostName == null || localHostName.length() <= 0) + localHostName = + session.getProperty("mail." + name + ".localaddress"); + try { + if (localHostName == null || localHostName.length() <= 0) { + InetAddress localHost = InetAddress.getLocalHost(); + localHostName = localHost.getCanonicalHostName(); + // if we can't get our name, use local address literal + if (localHostName == null) + // XXX - not correct for IPv6 + localHostName = "[" + localHost.getHostAddress() + "]"; + } + } catch (UnknownHostException uhex) { + } + + // last chance, try to get our address from our socket + if (localHostName == null || localHostName.length() <= 0) { + if (serverSocket != null && serverSocket.isBound()) { + InetAddress localHost = serverSocket.getLocalAddress(); + localHostName = localHost.getCanonicalHostName(); + // if we can't get our name, use local address literal + if (localHostName == null) + // XXX - not correct for IPv6 + localHostName = "[" + localHost.getHostAddress() + "]"; + } + } + return localHostName; + } + + /** + * Set the name of the local host, for use in the EHLO and HELO commands. + * + * @param localhost the local host name + * @since JavaMail 1.3.1 + */ + public synchronized void setLocalHost(String localhost) { + localHostName = localhost; + } + + /** + * Start the SMTP protocol on the given socket, which was already + * connected by the caller. Useful for implementing the SMTP ATRN + * command (RFC 2645) where an existing connection is used when + * the server reverses roles and becomes the client. + * + * @param socket the already connected socket + * @exception MessagingException for failures + * @since JavaMail 1.3.3 + */ + public synchronized void connect(Socket socket) throws MessagingException { + serverSocket = socket; + super.connect(); + } + + /** + * Gets the authorization ID to be used for authentication. + * + * @return the authorization ID to use for authentication. + * + * @since JavaMail 1.4.4 + */ + public synchronized String getAuthorizationId() { + if (authorizationID == UNKNOWN) { + authorizationID = + session.getProperty("mail." + name + ".sasl.authorizationid"); + } + return authorizationID; + } + + /** + * Sets the authorization ID to be used for authentication. + * + * @param authzid the authorization ID to use for + * authentication. + * + * @since JavaMail 1.4.4 + */ + public synchronized void setAuthorizationID(String authzid) { + this.authorizationID = authzid; + } + + /** + * Is SASL authentication enabled? + * + * @return true if SASL authentication is enabled + * + * @since JavaMail 1.4.4 + */ + public synchronized boolean getSASLEnabled() { + return enableSASL; + } + + /** + * Set whether SASL authentication is enabled. + * + * @param enableSASL should we enable SASL authentication? + * + * @since JavaMail 1.4.4 + */ + public synchronized void setSASLEnabled(boolean enableSASL) { + this.enableSASL = enableSASL; + } + + /** + * Gets the SASL realm to be used for DIGEST-MD5 authentication. + * + * @return the name of the realm to use for SASL authentication. + * + * @since JavaMail 1.3.1 + */ + public synchronized String getSASLRealm() { + if (saslRealm == UNKNOWN) { + saslRealm = session.getProperty("mail." + name + ".sasl.realm"); + if (saslRealm == null) // try old name + saslRealm = session.getProperty("mail." + name + ".saslrealm"); + } + return saslRealm; + } + + /** + * Sets the SASL realm to be used for DIGEST-MD5 authentication. + * + * @param saslRealm the name of the realm to use for + * SASL authentication. + * + * @since JavaMail 1.3.1 + */ + public synchronized void setSASLRealm(String saslRealm) { + this.saslRealm = saslRealm; + } + + /** + * Should SASL use the canonical host name? + * + * @return true if SASL should use the canonical host name + * + * @since JavaMail 1.5.2 + */ + public synchronized boolean getUseCanonicalHostName() { + return useCanonicalHostName; + } + + /** + * Set whether SASL should use the canonical host name. + * + * @param useCanonicalHostName should SASL use the canonical host name? + * + * @since JavaMail 1.5.2 + */ + public synchronized void setUseCanonicalHostName( + boolean useCanonicalHostName) { + this.useCanonicalHostName = useCanonicalHostName; + } + + /** + * Get the list of SASL mechanisms to consider if SASL authentication + * is enabled. If the list is empty or null, all available SASL mechanisms + * are considered. + * + * @return the array of SASL mechanisms to consider + * + * @since JavaMail 1.4.4 + */ + public synchronized String[] getSASLMechanisms() { + if (saslMechanisms == UNKNOWN_SA) { + List<String> v = new ArrayList<>(5); + String s = session.getProperty("mail." + name + ".sasl.mechanisms"); + if (s != null && s.length() > 0) { + if (logger.isLoggable(Level.FINE)) + logger.fine("SASL mechanisms allowed: " + s); + StringTokenizer st = new StringTokenizer(s, " ,"); + while (st.hasMoreTokens()) { + String m = st.nextToken(); + if (m.length() > 0) + v.add(m); + } + } + saslMechanisms = new String[v.size()]; + v.toArray(saslMechanisms); + } + if (saslMechanisms == null) + return null; + return saslMechanisms.clone(); + } + + /** + * Set the list of SASL mechanisms to consider if SASL authentication + * is enabled. If the list is empty or null, all available SASL mechanisms + * are considered. + * + * @param mechanisms the array of SASL mechanisms to consider + * + * @since JavaMail 1.4.4 + */ + public synchronized void setSASLMechanisms(String[] mechanisms) { + if (mechanisms != null) + mechanisms = mechanisms.clone(); + this.saslMechanisms = mechanisms; + } + + /** + * Gets the NTLM domain to be used for NTLM authentication. + * + * @return the name of the domain to use for NTLM authentication. + * + * @since JavaMail 1.4.3 + */ + public synchronized String getNTLMDomain() { + if (ntlmDomain == UNKNOWN) { + ntlmDomain = + session.getProperty("mail." + name + ".auth.ntlm.domain"); + } + return ntlmDomain; + } + + /** + * Sets the NTLM domain to be used for NTLM authentication. + * + * @param ntlmDomain the name of the domain to use for + * NTLM authentication. + * + * @since JavaMail 1.4.3 + */ + public synchronized void setNTLMDomain(String ntlmDomain) { + this.ntlmDomain = ntlmDomain; + } + + /** + * Should we report even successful sends by throwing an exception? + * If so, a <code>SendFailedException</code> will always be thrown and + * an {@link com.sun.mail.smtp.SMTPAddressSucceededException + * SMTPAddressSucceededException} will be included in the exception + * chain for each successful address, along with the usual + * {@link com.sun.mail.smtp.SMTPAddressFailedException + * SMTPAddressFailedException} for each unsuccessful address. + * + * @return true if an exception will be thrown on successful sends. + * + * @since JavaMail 1.3.2 + */ + public synchronized boolean getReportSuccess() { + return reportSuccess; + } + + /** + * Set whether successful sends should be reported by throwing + * an exception. + * + * @param reportSuccess should we throw an exception on success? + * + * @since JavaMail 1.3.2 + */ + public synchronized void setReportSuccess(boolean reportSuccess) { + this.reportSuccess = reportSuccess; + } + + /** + * Should we use the STARTTLS command to secure the connection + * if the server supports it? + * + * @return true if the STARTTLS command will be used + * + * @since JavaMail 1.3.2 + */ + public synchronized boolean getStartTLS() { + return useStartTLS; + } + + /** + * Set whether the STARTTLS command should be used. + * + * @param useStartTLS should we use the STARTTLS command? + * + * @since JavaMail 1.3.2 + */ + public synchronized void setStartTLS(boolean useStartTLS) { + this.useStartTLS = useStartTLS; + } + + /** + * Should we require the STARTTLS command to secure the connection? + * + * @return true if the STARTTLS command will be required + * + * @since JavaMail 1.4.2 + */ + public synchronized boolean getRequireStartTLS() { + return requireStartTLS; + } + + /** + * Set whether the STARTTLS command should be required. + * + * @param requireStartTLS should we require the STARTTLS command? + * + * @since JavaMail 1.4.2 + */ + public synchronized void setRequireStartTLS(boolean requireStartTLS) { + this.requireStartTLS = requireStartTLS; + } + + /** + * Is this Transport using SSL to connect to the server? + * + * @return true if using SSL + * @since JavaMail 1.4.6 + */ + public synchronized boolean isSSL() { + return serverSocket instanceof SSLSocket; + } + + /** + * Should we use the RSET command instead of the NOOP command + * in the @{link #isConnected isConnected} method? + * + * @return true if RSET will be used + * + * @since JavaMail 1.4 + */ + public synchronized boolean getUseRset() { + return useRset; + } + + /** + * Set whether the RSET command should be used instead of the + * NOOP command in the @{link #isConnected isConnected} method. + * + * @param useRset should we use the RSET command? + * + * @since JavaMail 1.4 + */ + public synchronized void setUseRset(boolean useRset) { + this.useRset = useRset; + } + + /** + * Is the NOOP command required to return a response code + * of 250 to indicate success? + * + * @return true if NOOP must return 250 + * + * @since JavaMail 1.4.3 + */ + public synchronized boolean getNoopStrict() { + return noopStrict; + } + + /** + * Set whether the NOOP command is required to return a response code + * of 250 to indicate success. + * + * @param noopStrict is NOOP required to return 250? + * + * @since JavaMail 1.4.3 + */ + public synchronized void setNoopStrict(boolean noopStrict) { + this.noopStrict = noopStrict; + } + + /** + * Return the last response we got from the server. + * A failed send is often followed by an RSET command, + * but the response from the RSET command is not saved. + * Instead, this returns the response from the command + * before the RSET command. + * + * @return last response from server + * + * @since JavaMail 1.3.2 + */ + public synchronized String getLastServerResponse() { + return lastServerResponse; + } + + /** + * Return the return code from the last response we got from the server. + * + * @return return code from last response from server + * + * @since JavaMail 1.4.1 + */ + public synchronized int getLastReturnCode() { + return lastReturnCode; + } + + /** + * Performs the actual protocol-specific connection attempt. + * Will attempt to connect to "localhost" if the host was null. <p> + * + * Unless mail.smtp.ehlo is set to false, we'll try to identify + * ourselves using the ESMTP command EHLO. + * + * If mail.smtp.auth is set to true, we insist on having a username + * and password, and will try to authenticate ourselves if the server + * supports the AUTH extension (RFC 2554). + * + * @param host the name of the host to connect to + * @param port the port to use (-1 means use default port) + * @param user the name of the user to login as + * @param password the user's password + * @return true if connection successful, false if authentication failed + * @exception MessagingException for non-authentication failures + */ + @Override + protected synchronized boolean protocolConnect(String host, int port, + String user, String password) + throws MessagingException { + Properties props = session.getProperties(); + + // setting mail.smtp.auth to true enables attempts to use AUTH + boolean useAuth = PropUtil.getBooleanProperty(props, + "mail." + name + ".auth", false); + + /* + * If mail.smtp.auth is set, make sure we have a valid username + * and password, even if we might not end up using it (e.g., + * because the server doesn't support ESMTP or doesn't support + * the AUTH extension). + */ + if (useAuth && (user == null || password == null)) { + if (logger.isLoggable(Level.FINE)) { + logger.fine("need username and password for authentication"); + logger.fine("protocolConnect returning false" + + ", host=" + host + + ", user=" + traceUser(user) + + ", password=" + tracePassword(password)); + } + return false; + } + + // setting mail.smtp.ehlo to false disables attempts to use EHLO + boolean useEhlo = PropUtil.getBooleanProperty(props, + "mail." + name + ".ehlo", true); + if (logger.isLoggable(Level.FINE)) + logger.fine("useEhlo " + useEhlo + ", useAuth " + useAuth); + + /* + * If port is not specified, set it to value of mail.smtp.port + * property if it exists, otherwise default to 25. + */ + if (port == -1) + port = PropUtil.getIntProperty(props, + "mail." + name + ".port", -1); + if (port == -1) + port = defaultPort; + + if (host == null || host.length() == 0) + host = "localhost"; + + /* + * If anything goes wrong, we need to be sure + * to close the connection. + */ + boolean connected = false; + try { + + if (serverSocket != null) + openServer(); // only happens from connect(socket) + else + openServer(host, port); + + boolean succeed = false; + if (useEhlo) + succeed = ehlo(getLocalHost()); + if (!succeed) + helo(getLocalHost()); + + if (useStartTLS || requireStartTLS) { + if (serverSocket instanceof SSLSocket) { + logger.fine("STARTTLS requested but already using SSL"); + } else if (supportsExtension("STARTTLS")) { + startTLS(); + /* + * Have to issue another EHLO to update list of extensions + * supported, especially authentication mechanisms. + * Don't know if this could ever fail, but we ignore + * failure. + */ + ehlo(getLocalHost()); + } else if (requireStartTLS) { + logger.fine("STARTTLS required but not supported"); + throw new MessagingException( + "STARTTLS is required but " + + "host does not support STARTTLS"); + } + } + + if (allowutf8 && !supportsExtension("SMTPUTF8")) + logger.log(Level.INFO, "mail.mime.allowutf8 set " + + "but server doesn't advertise SMTPUTF8 support"); + + if ((useAuth || (user != null && password != null)) && + (supportsExtension("AUTH") || + supportsExtension("AUTH=LOGIN"))) { + if (logger.isLoggable(Level.FINE)) + logger.fine("protocolConnect login" + + ", host=" + host + + ", user=" + traceUser(user) + + ", password=" + tracePassword(password)); + connected = authenticate(user, password); + return connected; + } + + // we connected correctly + connected = true; + return true; + + } finally { + // if we didn't connect successfully, + // make sure the connection is closed + if (!connected) { + try { + closeConnection(); + } catch (MessagingException mex) { + // ignore it + } + } + } + } + + /** + * Authenticate to the server. + */ + private boolean authenticate(String user, String passwd) + throws MessagingException { + // setting mail.smtp.auth.mechanisms controls which mechanisms will + // be used, and in what order they'll be considered. only the first + // match is used. + String mechs = session.getProperty("mail." + name + ".auth.mechanisms"); + if (mechs == null) + mechs = defaultAuthenticationMechanisms; + + String authzid = getAuthorizationId(); + if (authzid == null) + authzid = user; + if (enableSASL) { + logger.fine("Authenticate with SASL"); + try { + if (sasllogin(getSASLMechanisms(), getSASLRealm(), authzid, + user, passwd)) { + return true; // success + } else { + logger.fine("SASL authentication failed"); + return false; + } + } catch (UnsupportedOperationException ex) { + logger.log(Level.FINE, "SASL support failed", ex); + // if the SASL support fails, fall back to non-SASL + } + } + + if (logger.isLoggable(Level.FINE)) + logger.fine("Attempt to authenticate using mechanisms: " + mechs); + + /* + * Loop through the list of mechanisms supplied by the user + * (or defaulted) and try each in turn. If the server supports + * the mechanism and we have an authenticator for the mechanism, + * and it hasn't been disabled, use it. + */ + StringTokenizer st = new StringTokenizer(mechs); + while (st.hasMoreTokens()) { + String m = st.nextToken(); + m = m.toUpperCase(Locale.ENGLISH); + Authenticator a = authenticators.get(m); + if (a == null) { + logger.log(Level.FINE, "no authenticator for mechanism {0}", m); + continue; + } + + if (!supportsAuthentication(m)) { + logger.log(Level.FINE, "mechanism {0} not supported by server", + m); + continue; + } + + /* + * If using the default mechanisms, check if this one is disabled. + */ + if (mechs == defaultAuthenticationMechanisms) { + String dprop = "mail." + name + ".auth." + + m.toLowerCase(Locale.ENGLISH) + ".disable"; + boolean disabled = PropUtil.getBooleanProperty( + session.getProperties(), + dprop, !a.enabled()); + if (disabled) { + if (logger.isLoggable(Level.FINE)) + logger.fine("mechanism " + m + + " disabled by property: " + dprop); + continue; + } + } + + // only the first supported and enabled mechanism is used + logger.log(Level.FINE, "Using mechanism {0}", m); + return a.authenticate(host, authzid, user, passwd); + } + + // if no authentication mechanism found, fail + throw new AuthenticationFailedException( + "No authentication mechanisms supported by both server and client"); + } + + /** + * Abstract base class for SMTP authentication mechanism implementations. + */ + private abstract class Authenticator { + protected int resp; // the response code, used by subclasses + private final String mech; // the mechanism name, set in the constructor + private final boolean enabled; // is this mechanism enabled by default? + + Authenticator(String mech) { + this(mech, true); + } + + Authenticator(String mech, boolean enabled) { + this.mech = mech.toUpperCase(Locale.ENGLISH); + this.enabled = enabled; + } + + String getMechanism() { + return mech; + } + + boolean enabled() { + return enabled; + } + + /** + * Start the authentication handshake by issuing the AUTH command. + * Delegate to the doAuth method to do the mechanism-specific + * part of the handshake. + */ + boolean authenticate(String host, String authzid, + String user, String passwd) throws MessagingException { + Throwable thrown = null; + try { + // use "initial response" capability, if supported + String ir = getInitialResponse(host, authzid, user, passwd); + if (noauthdebug && isTracing()) { + logger.fine("AUTH " + mech + " command trace suppressed"); + suspendTracing(); + } + if (ir != null) + resp = simpleCommand("AUTH " + mech + " " + + (ir.length() == 0 ? "=" : ir)); + else + resp = simpleCommand("AUTH " + mech); + + /* + * A 530 response indicates that the server wants us to + * issue a STARTTLS command first. Do that and try again. + */ + if (resp == 530) { + startTLS(); + if (ir != null) + resp = simpleCommand("AUTH " + mech + " " + ir); + else + resp = simpleCommand("AUTH " + mech); + } + if (resp == 334) + doAuth(host, authzid, user, passwd); + } catch (IOException ex) { // should never happen, ignore + logger.log(Level.FINE, "AUTH " + mech + " failed", ex); + } catch (Throwable t) { // crypto can't be initialized? + logger.log(Level.FINE, "AUTH " + mech + " failed", t); + thrown = t; + } finally { + if (noauthdebug && isTracing()) + logger.fine("AUTH " + mech + " " + + (resp == 235 ? "succeeded" : "failed")); + resumeTracing(); + if (resp != 235) { + closeConnection(); + if (thrown != null) { + if (thrown instanceof Error) + throw (Error)thrown; + if (thrown instanceof Exception) + throw new AuthenticationFailedException( + getLastServerResponse(), + (Exception)thrown); + assert false : "unknown Throwable"; // can't happen + } + throw new AuthenticationFailedException( + getLastServerResponse()); + } + } + return true; + } + + /** + * Provide the initial response to use in the AUTH command, + * or null if not supported. Subclasses that support the + * initial response capability will override this method. + */ + String getInitialResponse(String host, String authzid, String user, + String passwd) throws MessagingException, IOException { + return null; + } + + abstract void doAuth(String host, String authzid, String user, + String passwd) throws MessagingException, IOException; + } + + /** + * Perform the authentication handshake for LOGIN authentication. + */ + private class LoginAuthenticator extends Authenticator { + LoginAuthenticator() { + super("LOGIN"); + } + + @Override + void doAuth(String host, String authzid, String user, String passwd) + throws MessagingException, IOException { + // send username + resp = simpleCommand(BASE64EncoderStream.encode( + user.getBytes(StandardCharsets.UTF_8))); + if (resp == 334) { + // send passwd + resp = simpleCommand(BASE64EncoderStream.encode( + passwd.getBytes(StandardCharsets.UTF_8))); + } + } + } + + /** + * Perform the authentication handshake for PLAIN authentication. + */ + private class PlainAuthenticator extends Authenticator { + PlainAuthenticator() { + super("PLAIN"); + } + + @Override + String getInitialResponse(String host, String authzid, String user, + String passwd) throws MessagingException, IOException { + // return "authzid<NUL>user<NUL>passwd" + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + OutputStream b64os = + new BASE64EncoderStream(bos, Integer.MAX_VALUE); + if (authzid != null) + b64os.write(authzid.getBytes(StandardCharsets.UTF_8)); + b64os.write(0); + b64os.write(user.getBytes(StandardCharsets.UTF_8)); + b64os.write(0); + b64os.write(passwd.getBytes(StandardCharsets.UTF_8)); + b64os.flush(); // complete the encoding + + return ASCIIUtility.toString(bos.toByteArray()); + } + + @Override + void doAuth(String host, String authzid, String user, String passwd) + throws MessagingException, IOException { + // should never get here + throw new AuthenticationFailedException("PLAIN asked for more"); + } + } + + /** + * Perform the authentication handshake for DIGEST-MD5 authentication. + */ + private class DigestMD5Authenticator extends Authenticator { + private DigestMD5 md5support; // only create if needed + + DigestMD5Authenticator() { + super("DIGEST-MD5"); + } + + private synchronized DigestMD5 getMD5() { + if (md5support == null) + md5support = new DigestMD5(logger); + return md5support; + } + + @Override + void doAuth(String host, String authzid, String user, String passwd) + throws MessagingException, IOException { + DigestMD5 md5 = getMD5(); + assert md5 != null; + + byte[] b = md5.authClient(host, user, passwd, getSASLRealm(), + getLastServerResponse()); + resp = simpleCommand(b); + if (resp == 334) { // client authenticated by server + if (!md5.authServer(getLastServerResponse())) { + // server NOT authenticated by client !!! + resp = -1; + } else { + // send null response + resp = simpleCommand(new byte[0]); + } + } + } + } + + /** + * Perform the authentication handshake for NTLM authentication. + */ + private class NtlmAuthenticator extends Authenticator { + private Ntlm ntlm; + private int flags; + + NtlmAuthenticator() { + super("NTLM"); + } + + @Override + String getInitialResponse(String host, String authzid, String user, + String passwd) throws MessagingException, IOException { + ntlm = new Ntlm(getNTLMDomain(), getLocalHost(), + user, passwd, logger); + + flags = PropUtil.getIntProperty( + session.getProperties(), + "mail." + name + ".auth.ntlm.flags", 0); + + String type1 = ntlm.generateType1Msg(flags); + return type1; + } + + @Override + void doAuth(String host, String authzid, String user, String passwd) + throws MessagingException, IOException { + assert ntlm != null; + String type3 = ntlm.generateType3Msg( + getLastServerResponse().substring(4).trim()); + + resp = simpleCommand(type3); + } + } + + /** + * Perform the authentication handshake for XOAUTH2 authentication. + */ + private class OAuth2Authenticator extends Authenticator { + + OAuth2Authenticator() { + super("XOAUTH2", false); // disabled by default + } + + @Override + String getInitialResponse(String host, String authzid, String user, + String passwd) throws MessagingException, IOException { + String resp = "user=" + user + "\001auth=Bearer " + + passwd + "\001\001"; + byte[] b = BASE64EncoderStream.encode( + resp.getBytes(StandardCharsets.UTF_8)); + return ASCIIUtility.toString(b); + } + + @Override + void doAuth(String host, String authzid, String user, String passwd) + throws MessagingException, IOException { + // should never get here + throw new AuthenticationFailedException("OAUTH2 asked for more"); + } + } + + /** + * SASL-based login. + * + * @param allowed the allowed SASL mechanisms + * @param realm the SASL realm + * @param authzid the authorization ID + * @param u the user name for authentication + * @param p the password for authentication + * @return true for success + * @exception MessagingException for failures + */ + private boolean sasllogin(String[] allowed, String realm, String authzid, + String u, String p) throws MessagingException { + String serviceHost; + if (useCanonicalHostName) + serviceHost = serverSocket.getInetAddress().getCanonicalHostName(); + else + serviceHost = host; + if (saslAuthenticator == null) { + try { + Class<?> sac = Class.forName( + "com.sun.mail.smtp.SMTPSaslAuthenticator"); + Constructor<?> c = sac.getConstructor(new Class<?>[] { + SMTPTransport.class, + String.class, + Properties.class, + MailLogger.class, + String.class + }); + saslAuthenticator = (SaslAuthenticator)c.newInstance( + new Object[] { + this, + name, + session.getProperties(), + logger, + serviceHost + }); + } catch (Exception ex) { + logger.log(Level.FINE, "Can't load SASL authenticator", ex); + // probably because we're running on a system without SASL + return false; // not authenticated, try without SASL + } + } + + // were any allowed mechanisms specified? + List<String> v; + if (allowed != null && allowed.length > 0) { + // remove anything not supported by the server + v = new ArrayList<>(allowed.length); + for (int i = 0; i < allowed.length; i++) + if (supportsAuthentication(allowed[i])) // XXX - case must match + v.add(allowed[i]); + } else { + // everything is allowed + v = new ArrayList<>(); + if (extMap != null) { + String a = extMap.get("AUTH"); + if (a != null) { + StringTokenizer st = new StringTokenizer(a); + while (st.hasMoreTokens()) + v.add(st.nextToken()); + } + } + } + String[] mechs = v.toArray(new String[v.size()]); + try { + if (noauthdebug && isTracing()) { + logger.fine("SASL AUTH command trace suppressed"); + suspendTracing(); + } + return saslAuthenticator.authenticate(mechs, realm, authzid, u, p); + } finally { + resumeTracing(); + } + } + + /** + * Send the Message to the specified list of addresses.<p> + * + * If all the <code>addresses</code> succeed the SMTP check + * using the <code>RCPT TO:</code> command, we attempt to send the message. + * A TransportEvent of type MESSAGE_DELIVERED is fired indicating the + * successful submission of a message to the SMTP host.<p> + * + * If some of the <code>addresses</code> fail the SMTP check, + * and the <code>mail.smtp.sendpartial</code> property is not set, + * sending is aborted. The TransportEvent of type MESSAGE_NOT_DELIVERED + * is fired containing the valid and invalid addresses. The + * SendFailedException is also thrown. <p> + * + * If some of the <code>addresses</code> fail the SMTP check, + * and the <code>mail.smtp.sendpartial</code> property is set to true, + * the message is sent. The TransportEvent of type + * MESSAGE_PARTIALLY_DELIVERED + * is fired containing the valid and invalid addresses. The + * SMTPSendFailedException is also thrown. <p> + * + * MessagingException is thrown if the message can't write out + * an RFC822-compliant stream using its <code>writeTo</code> method. <p> + * + * @param message The MimeMessage to be sent + * @param addresses List of addresses to send this message to + * @see javax.mail.event.TransportEvent + * @exception SMTPSendFailedException if the send failed because of + * an SMTP command error + * @exception SendFailedException if the send failed because of + * invalid addresses. + * @exception MessagingException if the connection is dead + * or not in the connected state or if the message is + * not a MimeMessage. + */ + @Override + public synchronized void sendMessage(Message message, Address[] addresses) + throws MessagingException, SendFailedException { + + sendMessageStart(message != null ? message.getSubject() : ""); + checkConnected(); + + // check if the message is a valid MIME/RFC822 message and that + // it has all valid InternetAddresses; fail if not + if (!(message instanceof MimeMessage)) { + logger.fine("Can only send RFC822 msgs"); + throw new MessagingException("SMTP can only send RFC822 messages"); + } + for (int i = 0; i < addresses.length; i++) { + if (!(addresses[i] instanceof InternetAddress)) { + throw new MessagingException(addresses[i] + + " is not an InternetAddress"); + } + } + if (addresses.length == 0) + throw new SendFailedException("No recipient addresses"); + + this.message = (MimeMessage)message; + this.addresses = addresses; + validUnsentAddr = addresses; // until we know better + expandGroups(); + + boolean use8bit = false; + if (message instanceof SMTPMessage) + use8bit = ((SMTPMessage)message).getAllow8bitMIME(); + if (!use8bit) + use8bit = PropUtil.getBooleanProperty(session.getProperties(), + "mail." + name + ".allow8bitmime", false); + if (logger.isLoggable(Level.FINE)) + logger.fine("use8bit " + use8bit); + if (use8bit && supportsExtension("8BITMIME")) { + if (convertTo8Bit(this.message)) { + // in case we made any changes, save those changes + // XXX - this will change the Message-ID + try { + this.message.saveChanges(); + } catch (MessagingException mex) { + // ignore it + } + } + } + + try { + mailFrom(); + rcptTo(); + if (chunkSize > 0 && supportsExtension("CHUNKING")) { + /* + * Use BDAT to send the data in chunks. + * Note that even though the BDAT command is able to send + * messages that contain binary data, we can't use it to + * do that because a) we still need to canonicalize the + * line terminators for text data, which we can't tell apart + * from the message content, and b) the message content is + * encoded before we even know that we can use BDAT. + */ + this.message.writeTo(bdat(), ignoreList); + finishBdat(); + } else { + this.message.writeTo(data(), ignoreList); + finishData(); + } + if (sendPartiallyFailed) { + // throw the exception, + // fire TransportEvent.MESSAGE_PARTIALLY_DELIVERED event + logger.fine("Sending partially failed " + + "because of invalid destination addresses"); + notifyTransportListeners( + TransportEvent.MESSAGE_PARTIALLY_DELIVERED, + validSentAddr, validUnsentAddr, invalidAddr, + this.message); + + throw new SMTPSendFailedException(".", lastReturnCode, + lastServerResponse, exception, + validSentAddr, validUnsentAddr, invalidAddr); + } + logger.fine("message successfully delivered to mail server"); + notifyTransportListeners(TransportEvent.MESSAGE_DELIVERED, + validSentAddr, validUnsentAddr, + invalidAddr, this.message); + } catch (MessagingException mex) { + logger.log(Level.FINE, "MessagingException while sending", mex); + // the MessagingException might be wrapping an IOException + if (mex.getNextException() instanceof IOException) { + // if we catch an IOException, it means that we want + // to drop the connection so that the message isn't sent + logger.fine("nested IOException, closing"); + try { + closeConnection(); + } catch (MessagingException cex) { /* ignore it */ } + } + addressesFailed(); + notifyTransportListeners(TransportEvent.MESSAGE_NOT_DELIVERED, + validSentAddr, validUnsentAddr, + invalidAddr, this.message); + + throw mex; + } catch (IOException ex) { + logger.log(Level.FINE, "IOException while sending, closing", ex); + // if we catch an IOException, it means that we want + // to drop the connection so that the message isn't sent + try { + closeConnection(); + } catch (MessagingException mex) { /* ignore it */ } + addressesFailed(); + notifyTransportListeners(TransportEvent.MESSAGE_NOT_DELIVERED, + validSentAddr, validUnsentAddr, + invalidAddr, this.message); + + throw new MessagingException("IOException while sending message", + ex); + } finally { + // no reason to keep this data around + validSentAddr = validUnsentAddr = invalidAddr = null; + this.addresses = null; + this.message = null; + this.exception = null; + sendPartiallyFailed = false; + notificationDone = false; // reset for next send + } + sendMessageEnd(); + } + + /** + * The send failed, fix the address arrays to report the failure correctly. + */ + private void addressesFailed() { + if (validSentAddr != null) { + if (validUnsentAddr != null) { + Address newa[] = + new Address[validSentAddr.length + validUnsentAddr.length]; + System.arraycopy(validSentAddr, 0, + newa, 0, validSentAddr.length); + System.arraycopy(validUnsentAddr, 0, + newa, validSentAddr.length, validUnsentAddr.length); + validSentAddr = null; + validUnsentAddr = newa; + } else { + validUnsentAddr = validSentAddr; + validSentAddr = null; + } + } + } + + /** + * Close the Transport and terminate the connection to the server. + */ + @Override + public synchronized void close() throws MessagingException { + if (!super.isConnected()) // Already closed. + return; + try { + if (serverSocket != null) { + sendCommand("QUIT"); + if (quitWait) { + int resp = readServerResponse(); + if (resp != 221 && resp != -1 && + logger.isLoggable(Level.FINE)) + logger.fine("QUIT failed with " + resp); + } + } + } finally { + closeConnection(); + } + } + + private void closeConnection() throws MessagingException { + try { + if (serverSocket != null) + serverSocket.close(); + } catch (IOException ioex) { // shouldn't happen + throw new MessagingException("Server Close Failed", ioex); + } finally { + serverSocket = null; + serverOutput = null; + serverInput = null; + lineInputStream = null; + if (super.isConnected()) // only notify if already connected + super.close(); + } + } + + /** + * Check whether the transport is connected. Override superclass + * method, to actually ping our server connection. + */ + @Override + public synchronized boolean isConnected() { + if (!super.isConnected()) + // if we haven't been connected at all, don't bother with NOOP + return false; + + try { + // sendmail may respond slowly to NOOP after many requests + // so if mail.smtp.userset is set we use RSET instead of NOOP. + if (useRset) + sendCommand("RSET"); + else + sendCommand("NOOP"); + int resp = readServerResponse(); + + /* + * NOOP should return 250 on success, however, SIMS 3.2 returns + * 200, so we work around it. + * + * Hotmail didn't used to implement the NOOP command at all so + * assume any kind of response means we're still connected. + * That is, any response except 421, which means the server + * is shutting down the connection. + * + * Some versions of Exchange return 451 instead of 421 when + * timing out a connection. + * + * Argh! + * + * If mail.smtp.noop.strict is set to false, be tolerant of + * servers that return the wrong response code for success. + */ + if (resp >= 0 && (noopStrict ? resp == 250 : resp != 421)) { + return true; + } else { + try { + closeConnection(); + } catch (MessagingException mex) { + // ignore it + } + return false; + } + } catch (Exception ex) { + try { + closeConnection(); + } catch (MessagingException mex) { + // ignore it + } + return false; + } + } + + /** + * Notify all TransportListeners. Keep track of whether notification + * has been done so as to only notify once per send. + * + * @since JavaMail 1.4.2 + */ + @Override + protected void notifyTransportListeners(int type, Address[] validSent, + Address[] validUnsent, + Address[] invalid, Message msg) { + + if (!notificationDone) { + super.notifyTransportListeners(type, validSent, validUnsent, + invalid, msg); + notificationDone = true; + } + } + + /** + * Expand any group addresses. + */ + private void expandGroups() { + List<Address> groups = null; + for (int i = 0; i < addresses.length; i++) { + InternetAddress a = (InternetAddress)addresses[i]; + if (a.isGroup()) { + if (groups == null) { + // first group, catch up with where we are + groups = new ArrayList<>(); + for (int k = 0; k < i; k++) + groups.add(addresses[k]); + } + // parse it and add each individual address + try { + InternetAddress[] ia = a.getGroup(true); + if (ia != null) { + for (int j = 0; j < ia.length; j++) + groups.add(ia[j]); + } else + groups.add(a); + } catch (ParseException pex) { + // parse failed, add the whole thing + groups.add(a); + } + } else { + // if we've started accumulating a list, add this to it + if (groups != null) + groups.add(a); + } + } + + // if we have a new list, convert it back to an array + if (groups != null) { + InternetAddress[] newa = new InternetAddress[groups.size()]; + groups.toArray(newa); + addresses = newa; + } + } + + /** + * If the Part is a text part and has a Content-Transfer-Encoding + * of "quoted-printable" or "base64", and it obeys the rules for + * "8bit" encoding, change the encoding to "8bit". If the part is + * a multipart, recursively process all its parts. + * + * @return true if any changes were made + * + * XXX - This is really quite a hack. + */ + private boolean convertTo8Bit(MimePart part) { + boolean changed = false; + try { + if (part.isMimeType("text/*")) { + String enc = part.getEncoding(); + if (enc != null && (enc.equalsIgnoreCase("quoted-printable") || + enc.equalsIgnoreCase("base64"))) { + InputStream is = null; + try { + is = part.getInputStream(); + if (is8Bit(is)) { + /* + * If the message was created using an InputStream + * then we have to extract the content as an object + * and set it back as an object so that the content + * will be re-encoded. + * + * If the message was not created using an + * InputStream, the following should have no effect. + */ + part.setContent(part.getContent(), + part.getContentType()); + part.setHeader("Content-Transfer-Encoding", "8bit"); + changed = true; + } + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException ex2) { + // ignore it + } + } + } + } + } else if (part.isMimeType("multipart/*")) { + MimeMultipart mp = (MimeMultipart)part.getContent(); + int count = mp.getCount(); + for (int i = 0; i < count; i++) { + if (convertTo8Bit((MimePart)mp.getBodyPart(i))) + changed = true; + } + } + } catch (IOException ioex) { + // any exception causes us to give up + } catch (MessagingException mex) { + // any exception causes us to give up + } + return changed; + } + + /** + * Check whether the data in the given InputStream follows the + * rules for 8bit text. Lines have to be 998 characters or less + * and no NULs are allowed. CR and LF must occur in pairs but we + * don't check that because we assume this is text and we convert + * all CR/LF combinations into canonical CRLF later. + */ + private boolean is8Bit(InputStream is) { + int b; + int linelen = 0; + boolean need8bit = false; + try { + while ((b = is.read()) >= 0) { + b &= 0xff; + if (b == '\r' || b == '\n') + linelen = 0; + else if (b == 0) + return false; + else { + linelen++; + if (linelen > 998) // 1000 - CRLF + return false; + } + if (b > 0x7f) + need8bit = true; + } + } catch (IOException ex) { + return false; + } + if (need8bit) + logger.fine("found an 8bit part"); + return need8bit; + } + + @Override + protected void finalize() throws Throwable { + try { + closeConnection(); + } catch (MessagingException mex) { + // ignore it + } finally { + super.finalize(); + } + } + + ///////////////////// smtp stuff /////////////////////// + private BufferedInputStream serverInput; + private LineInputStream lineInputStream; + private OutputStream serverOutput; + private Socket serverSocket; + private TraceInputStream traceInput; + private TraceOutputStream traceOutput; + + /////// smtp protocol ////// + + /** + * Issue the <code>HELO</code> command. + * + * @param domain our domain + * @exception MessagingException for failures + * @since JavaMail 1.4.1 + */ + protected void helo(String domain) throws MessagingException { + if (domain != null) + issueCommand("HELO " + domain, 250); + else + issueCommand("HELO", 250); + } + + /** + * Issue the <code>EHLO</code> command. + * Collect the returned list of service extensions. + * + * @param domain our domain + * @return true if command succeeds + * @exception MessagingException for failures + * @since JavaMail 1.4.1 + */ + protected boolean ehlo(String domain) throws MessagingException { + String cmd; + if (domain != null) + cmd = "EHLO " + domain; + else + cmd = "EHLO"; + sendCommand(cmd); + int resp = readServerResponse(); + if (resp == 250) { + // extract the supported service extensions + BufferedReader rd = + new BufferedReader(new StringReader(lastServerResponse)); + String line; + extMap = new Hashtable<>(); + try { + boolean first = true; + while ((line = rd.readLine()) != null) { + if (first) { // skip first line which is the greeting + first = false; + continue; + } + if (line.length() < 5) + continue; // shouldn't happen + line = line.substring(4); // skip response code + int i = line.indexOf(' '); + String arg = ""; + if (i > 0) { + arg = line.substring(i + 1); + line = line.substring(0, i); + } + if (logger.isLoggable(Level.FINE)) + logger.fine("Found extension \"" + + line + "\", arg \"" + arg + "\""); + extMap.put(line.toUpperCase(Locale.ENGLISH), arg); + } + } catch (IOException ex) { } // can't happen + } + return resp == 250; + } + + /** + * Issue the <code>MAIL FROM:</code> command to start sending a message. <p> + * + * Gets the sender's address in the following order: + * <ol> + * <li>SMTPMessage.getEnvelopeFrom()</li> + * <li>mail.smtp.from property</li> + * <li>From: header in the message</li> + * <li>System username using the + * InternetAddress.getLocalAddress() method</li> + * </ol> + * + * @exception MessagingException for failures + * @since JavaMail 1.4.1 + */ + protected void mailFrom() throws MessagingException { + String from = null; + if (message instanceof SMTPMessage) + from = ((SMTPMessage)message).getEnvelopeFrom(); + if (from == null || from.length() <= 0) + from = session.getProperty("mail." + name + ".from"); + if (from == null || from.length() <= 0) { + Address[] fa; + Address me; + if (message != null && (fa = message.getFrom()) != null && + fa.length > 0) + me = fa[0]; + else + me = InternetAddress.getLocalAddress(session); + + if (me != null) + from = ((InternetAddress)me).getAddress(); + else + throw new MessagingException( + "can't determine local email address"); + } + + String cmd = "MAIL FROM:" + normalizeAddress(from); + + if (allowutf8 && supportsExtension("SMTPUTF8")) + cmd += " SMTPUTF8"; + + // request delivery status notification? + if (supportsExtension("DSN")) { + String ret = null; + if (message instanceof SMTPMessage) + ret = ((SMTPMessage)message).getDSNRet(); + if (ret == null) + ret = session.getProperty("mail." + name + ".dsn.ret"); + // XXX - check for legal syntax? + if (ret != null) + cmd += " RET=" + ret; + } + + /* + * If an RFC 2554 submitter has been specified, and the server + * supports the AUTH extension, include the AUTH= element on + * the MAIL FROM command. + */ + if (supportsExtension("AUTH")) { + String submitter = null; + if (message instanceof SMTPMessage) + submitter = ((SMTPMessage)message).getSubmitter(); + if (submitter == null) + submitter = session.getProperty("mail." + name + ".submitter"); + // XXX - check for legal syntax? + if (submitter != null) { + try { + String s = xtext(submitter, + allowutf8 && supportsExtension("SMTPUTF8")); + cmd += " AUTH=" + s; + } catch (IllegalArgumentException ex) { + if (logger.isLoggable(Level.FINE)) + logger.log(Level.FINE, "ignoring invalid submitter: " + + submitter, ex); + } + } + } + + /* + * Have any extensions to the MAIL command been specified? + */ + String ext = null; + if (message instanceof SMTPMessage) + ext = ((SMTPMessage)message).getMailExtension(); + if (ext == null) + ext = session.getProperty("mail." + name + ".mailextension"); + if (ext != null && ext.length() > 0) + cmd += " " + ext; + + try { + issueSendCommand(cmd, 250); + } catch (SMTPSendFailedException ex) { + int retCode = ex.getReturnCode(); + switch (retCode) { + case 550: case 553: case 503: case 551: case 501: + // given address is invalid + try { + ex.setNextException(new SMTPSenderFailedException( + new InternetAddress(from), cmd, + retCode, ex.getMessage())); + } catch (AddressException aex) { + // oh well... + } + break; + default: + break; + } + throw ex; + } + } + + /** + * Sends each address to the SMTP host using the <code>RCPT TO:</code> + * command and copies the address either into + * the validSentAddr or invalidAddr arrays. + * Sets the <code>sendFailed</code> + * flag to true if any addresses failed. + * + * @exception MessagingException for failures + * @since JavaMail 1.4.1 + */ + /* + * success/failure/error possibilities from the RCPT command + * from rfc821, section 4.3 + * S: 250, 251 + * F: 550, 551, 552, 553, 450, 451, 452 + * E: 500, 501, 503, 421 + * + * and how we map the above error/failure conditions to valid/invalid + * address lists that are reported in the thrown exception: + * invalid addr: 550, 501, 503, 551, 553 + * valid addr: 552 (quota), 450, 451, 452 (quota), 421 (srvr abort) + */ + protected void rcptTo() throws MessagingException { + List<InternetAddress> valid = new ArrayList<>(); + List<InternetAddress> validUnsent = new ArrayList<>(); + List<InternetAddress> invalid = new ArrayList<>(); + int retCode = -1; + MessagingException mex = null; + boolean sendFailed = false; + MessagingException sfex = null; + validSentAddr = validUnsentAddr = invalidAddr = null; + boolean sendPartial = false; + if (message instanceof SMTPMessage) + sendPartial = ((SMTPMessage)message).getSendPartial(); + if (!sendPartial) + sendPartial = PropUtil.getBooleanProperty(session.getProperties(), + "mail." + name + ".sendpartial", false); + if (sendPartial) + logger.fine("sendPartial set"); + + boolean dsn = false; + String notify = null; + if (supportsExtension("DSN")) { + if (message instanceof SMTPMessage) + notify = ((SMTPMessage)message).getDSNNotify(); + if (notify == null) + notify = session.getProperty("mail." + name + ".dsn.notify"); + // XXX - check for legal syntax? + if (notify != null) + dsn = true; + } + + // try the addresses one at a time + for (int i = 0; i < addresses.length; i++) { + + sfex = null; + InternetAddress ia = (InternetAddress)addresses[i]; + String cmd = "RCPT TO:" + normalizeAddress(ia.getAddress()); + if (dsn) + cmd += " NOTIFY=" + notify; + // send the addresses to the SMTP server + sendCommand(cmd); + // check the server's response for address validity + retCode = readServerResponse(); + switch (retCode) { + case 250: case 251: + valid.add(ia); + if (!reportSuccess) + break; + + // user wants exception even when successful, including + // details of the return code + + // create and chain the exception + sfex = new SMTPAddressSucceededException(ia, cmd, retCode, + lastServerResponse); + if (mex == null) + mex = sfex; + else + mex.setNextException(sfex); + break; + + case 550: case 553: case 503: case 551: case 501: + // given address is invalid + if (!sendPartial) + sendFailed = true; + invalid.add(ia); + // create and chain the exception + sfex = new SMTPAddressFailedException(ia, cmd, retCode, + lastServerResponse); + if (mex == null) + mex = sfex; + else + mex.setNextException(sfex); + break; + + case 552: case 450: case 451: case 452: + // given address is valid + if (!sendPartial) + sendFailed = true; + validUnsent.add(ia); + // create and chain the exception + sfex = new SMTPAddressFailedException(ia, cmd, retCode, + lastServerResponse); + if (mex == null) + mex = sfex; + else + mex.setNextException(sfex); + break; + + default: + // handle remaining 4xy & 5xy codes + if (retCode >= 400 && retCode <= 499) { + // assume address is valid, although we don't really know + validUnsent.add(ia); + } else if (retCode >= 500 && retCode <= 599) { + // assume address is invalid, although we don't really know + invalid.add(ia); + } else { + // completely unexpected response, just give up + if (logger.isLoggable(Level.FINE)) + logger.fine("got response code " + retCode + + ", with response: " + lastServerResponse); + String _lsr = lastServerResponse; // else rset will nuke it + int _lrc = lastReturnCode; + if (serverSocket != null) // hasn't already been closed + issueCommand("RSET", -1); + lastServerResponse = _lsr; // restore, for get + lastReturnCode = _lrc; + throw new SMTPAddressFailedException(ia, cmd, retCode, + _lsr); + } + if (!sendPartial) + sendFailed = true; + // create and chain the exception + sfex = new SMTPAddressFailedException(ia, cmd, retCode, + lastServerResponse); + if (mex == null) + mex = sfex; + else + mex.setNextException(sfex); + break; + } + } + + // if we're willing to send to a partial list, and we found no + // valid addresses, that's complete failure + if (sendPartial && valid.size() == 0) + sendFailed = true; + + // copy the lists into appropriate arrays + if (sendFailed) { + // copy invalid addrs + invalidAddr = new Address[invalid.size()]; + invalid.toArray(invalidAddr); + + // copy all valid addresses to validUnsent, since something failed + validUnsentAddr = new Address[valid.size() + validUnsent.size()]; + int i = 0; + for (int j = 0; j < valid.size(); j++) + validUnsentAddr[i++] = (Address)valid.get(j); + for (int j = 0; j < validUnsent.size(); j++) + validUnsentAddr[i++] = (Address)validUnsent.get(j); + } else if (reportSuccess || (sendPartial && + (invalid.size() > 0 || validUnsent.size() > 0))) { + // we'll go on to send the message, but after sending we'll + // throw an exception with this exception nested + sendPartiallyFailed = true; + exception = mex; + + // copy invalid addrs + invalidAddr = new Address[invalid.size()]; + invalid.toArray(invalidAddr); + + // copy valid unsent addresses to validUnsent + validUnsentAddr = new Address[validUnsent.size()]; + validUnsent.toArray(validUnsentAddr); + + // copy valid addresses to validSent + validSentAddr = new Address[valid.size()]; + valid.toArray(validSentAddr); + } else { // all addresses pass + validSentAddr = addresses; + } + + + // print out the debug info + if (logger.isLoggable(Level.FINE)) { + if (validSentAddr != null && validSentAddr.length > 0) { + logger.fine("Verified Addresses"); + for (int l = 0; l < validSentAddr.length; l++) { + logger.fine(" " + validSentAddr[l]); + } + } + if (validUnsentAddr != null && validUnsentAddr.length > 0) { + logger.fine("Valid Unsent Addresses"); + for (int j = 0; j < validUnsentAddr.length; j++) { + logger.fine(" " + validUnsentAddr[j]); + } + } + if (invalidAddr != null && invalidAddr.length > 0) { + logger.fine("Invalid Addresses"); + for (int k = 0; k < invalidAddr.length; k++) { + logger.fine(" " + invalidAddr[k]); + } + } + } + + // throw the exception, fire TransportEvent.MESSAGE_NOT_DELIVERED event + if (sendFailed) { + logger.fine( + "Sending failed because of invalid destination addresses"); + notifyTransportListeners(TransportEvent.MESSAGE_NOT_DELIVERED, + validSentAddr, validUnsentAddr, + invalidAddr, this.message); + + // reset the connection so more sends are allowed + String lsr = lastServerResponse; // save, for get + int lrc = lastReturnCode; + try { + if (serverSocket != null) + issueCommand("RSET", -1); + } catch (MessagingException ex) { + // if can't reset, best to close the connection + try { + close(); + } catch (MessagingException ex2) { + // thrown by close()--ignore, will close() later anyway + logger.log(Level.FINE, "close failed", ex2); + } + } finally { + lastServerResponse = lsr; // restore + lastReturnCode = lrc; + } + + throw new SendFailedException("Invalid Addresses", mex, + validSentAddr, + validUnsentAddr, invalidAddr); + } + } + + /** + * Send the <code>DATA</code> command to the SMTP host and return + * an OutputStream to which the data is to be written. + * + * @return the stream to write to + * @exception MessagingException for failures + * @since JavaMail 1.4.1 + */ + protected OutputStream data() throws MessagingException { + assert Thread.holdsLock(this); + issueSendCommand("DATA", 354); + dataStream = new SMTPOutputStream(serverOutput); + return dataStream; + } + + /** + * Terminate the sent data. + * + * @exception IOException for I/O errors + * @exception MessagingException for other failures + * @since JavaMail 1.4.1 + */ + protected void finishData() throws IOException, MessagingException { + assert Thread.holdsLock(this); + dataStream.ensureAtBOL(); + issueSendCommand(".", 250); + } + + /** + * Return a stream that will use the SMTP BDAT command to send data. + * + * @return the stream to write to + * @exception MessagingException for failures + * @since JavaMail 1.6.0 + */ + protected OutputStream bdat() throws MessagingException { + assert Thread.holdsLock(this); + dataStream = new BDATOutputStream(serverOutput, chunkSize); + return dataStream; + } + + /** + * Terminate the sent data. + * + * @exception IOException for I/O errors + * @exception MessagingException for other failures + * @since JavaMail 1.6.0 + */ + protected void finishBdat() throws IOException, MessagingException { + assert Thread.holdsLock(this); + dataStream.ensureAtBOL(); + dataStream.close(); // doesn't close underlying socket + } + + /** + * Issue the <code>STARTTLS</code> command and switch the socket to + * TLS mode if it succeeds. + * + * @exception MessagingException for failures + * @since JavaMail 1.4.1 + */ + protected void startTLS() throws MessagingException { + issueCommand("STARTTLS", 220); + // it worked, now switch the socket into TLS mode + try { + serverSocket = SocketFetcher.startTLS(serverSocket, host, + session.getProperties(), "mail." + name); + initStreams(); + } catch (IOException ioex) { + closeConnection(); + throw new MessagingException("Could not convert socket to TLS", + ioex); + } + } + + /////// primitives /////// + + /** + * Connect to host on port and start the SMTP protocol. + */ + private void openServer(String host, int port) + throws MessagingException { + + if (logger.isLoggable(Level.FINE)) + logger.fine("trying to connect to host \"" + host + + "\", port " + port + ", isSSL " + isSSL); + + try { + Properties props = session.getProperties(); + + serverSocket = SocketFetcher.getSocket(host, port, + props, "mail." + name, isSSL); + + // socket factory may've chosen a different port, + // update it for the debug messages that follow + port = serverSocket.getPort(); + // save host name for startTLS + this.host = host; + + initStreams(); + + int r = -1; + if ((r = readServerResponse()) != 220) { + serverSocket.close(); + serverSocket = null; + serverOutput = null; + serverInput = null; + lineInputStream = null; + if (logger.isLoggable(Level.FINE)) + logger.fine("could not connect to host \"" + + host + "\", port: " + port + + ", response: " + r); + throw new MessagingException( + "Could not connect to SMTP host: " + host + + ", port: " + port + + ", response: " + r); + } else { + if (logger.isLoggable(Level.FINE)) + logger.fine("connected to host \"" + + host + "\", port: " + port); + } + } catch (UnknownHostException uhex) { + throw new MessagingException("Unknown SMTP host: " + host, uhex); + } catch (SocketConnectException scex) { + throw new MailConnectException(scex); + } catch (IOException ioe) { + throw new MessagingException("Could not connect to SMTP host: " + + host + ", port: " + port, ioe); + } + } + + /** + * Start the protocol to the server on serverSocket, + * assumed to be provided and connected by the caller. + */ + private void openServer() throws MessagingException { + int port = -1; + host = "UNKNOWN"; + try { + port = serverSocket.getPort(); + host = serverSocket.getInetAddress().getHostName(); + if (logger.isLoggable(Level.FINE)) + logger.fine("starting protocol to host \"" + + host + "\", port " + port); + + initStreams(); + + int r = -1; + if ((r = readServerResponse()) != 220) { + serverSocket.close(); + serverSocket = null; + serverOutput = null; + serverInput = null; + lineInputStream = null; + if (logger.isLoggable(Level.FINE)) + logger.fine("got bad greeting from host \"" + + host + "\", port: " + port + + ", response: " + r); + throw new MessagingException( + "Got bad greeting from SMTP host: " + host + + ", port: " + port + + ", response: " + r); + } else { + if (logger.isLoggable(Level.FINE)) + logger.fine("protocol started to host \"" + + host + "\", port: " + port); + } + } catch (IOException ioe) { + throw new MessagingException( + "Could not start protocol to SMTP host: " + + host + ", port: " + port, ioe); + } + } + + + private void initStreams() throws IOException { + boolean quote = PropUtil.getBooleanProperty(session.getProperties(), + "mail.debug.quote", false); + + traceInput = + new TraceInputStream(serverSocket.getInputStream(), traceLogger); + traceInput.setQuote(quote); + + traceOutput = + new TraceOutputStream(serverSocket.getOutputStream(), traceLogger); + traceOutput.setQuote(quote); + + serverOutput = + new BufferedOutputStream(traceOutput); + serverInput = + new BufferedInputStream(traceInput); + lineInputStream = new LineInputStream(serverInput); + } + + /** + * Is protocol tracing enabled? + */ + private boolean isTracing() { + return traceLogger.isLoggable(Level.FINEST); + } + + /** + * Temporarily turn off protocol tracing, e.g., to prevent + * tracing the authentication sequence, including the password. + */ + private void suspendTracing() { + if (traceLogger.isLoggable(Level.FINEST)) { + traceInput.setTrace(false); + traceOutput.setTrace(false); + } + } + + /** + * Resume protocol tracing, if it was enabled to begin with. + */ + private void resumeTracing() { + if (traceLogger.isLoggable(Level.FINEST)) { + traceInput.setTrace(true); + traceOutput.setTrace(true); + } + } + + /** + * Send the command to the server. If the expected response code + * is not received, throw a MessagingException. + * + * @param cmd the command to send + * @param expect the expected response code (-1 means don't care) + * @exception MessagingException for failures + * @since JavaMail 1.4.1 + */ + public synchronized void issueCommand(String cmd, int expect) + throws MessagingException { + sendCommand(cmd); + + // if server responded with an unexpected return code, + // throw the exception, notifying the client of the response + int resp = readServerResponse(); + if (expect != -1 && resp != expect) + throw new MessagingException(lastServerResponse); + } + + /** + * Issue a command that's part of sending a message. + */ + private void issueSendCommand(String cmd, int expect) + throws MessagingException { + sendCommand(cmd); + + // if server responded with an unexpected return code, + // throw the exception, notifying the client of the response + int ret; + if ((ret = readServerResponse()) != expect) { + // assume message was not sent to anyone, + // combine valid sent & unsent addresses + int vsl = validSentAddr == null ? 0 : validSentAddr.length; + int vul = validUnsentAddr == null ? 0 : validUnsentAddr.length; + Address[] valid = new Address[vsl + vul]; + if (vsl > 0) + System.arraycopy(validSentAddr, 0, valid, 0, vsl); + if (vul > 0) + System.arraycopy(validUnsentAddr, 0, valid, vsl, vul); + validSentAddr = null; + validUnsentAddr = valid; + if (logger.isLoggable(Level.FINE)) + logger.fine("got response code " + ret + + ", with response: " + lastServerResponse); + String _lsr = lastServerResponse; // else rset will nuke it + int _lrc = lastReturnCode; + if (serverSocket != null) // hasn't already been closed + issueCommand("RSET", -1); + lastServerResponse = _lsr; // restore, for get + lastReturnCode = _lrc; + throw new SMTPSendFailedException(cmd, ret, lastServerResponse, + exception, validSentAddr, validUnsentAddr, invalidAddr); + } + } + + /** + * Send the command to the server and return the response code + * from the server. + * + * @param cmd the command + * @return the response code + * @exception MessagingException for failures + * @since JavaMail 1.4.1 + */ + public synchronized int simpleCommand(String cmd) + throws MessagingException { + sendCommand(cmd); + return readServerResponse(); + } + + /** + * Send the command to the server and return the response code + * from the server. + * + * @param cmd the command + * @return the response code + * @exception MessagingException for failures + * @since JavaMail 1.4.1 + */ + protected int simpleCommand(byte[] cmd) throws MessagingException { + assert Thread.holdsLock(this); + sendCommand(cmd); + return readServerResponse(); + } + + /** + * Sends command <code>cmd</code> to the server terminating + * it with <code>CRLF</code>. + * + * @param cmd the command + * @exception MessagingException for failures + * @since JavaMail 1.4.1 + */ + protected void sendCommand(String cmd) throws MessagingException { + sendCommand(toBytes(cmd)); + } + + private void sendCommand(byte[] cmdBytes) throws MessagingException { + assert Thread.holdsLock(this); + //if (logger.isLoggable(Level.FINE)) + //logger.fine("SENT: " + new String(cmdBytes, 0)); + + try { + serverOutput.write(cmdBytes); + serverOutput.write(CRLF); + serverOutput.flush(); + } catch (IOException ex) { + throw new MessagingException("Can't send command to SMTP host", ex); + } + } + + /** + * Reads server reponse returning the <code>returnCode</code> + * as the number. Returns -1 on failure. Sets + * <code>lastServerResponse</code> and <code>lastReturnCode</code>. + * + * @return server response code + * @exception MessagingException for failures + * @since JavaMail 1.4.1 + */ + protected int readServerResponse() throws MessagingException { + assert Thread.holdsLock(this); + String serverResponse = ""; + int returnCode = 0; + StringBuilder buf = new StringBuilder(100); + + // read the server response line(s) and add them to the buffer + // that stores the response + try { + String line = null; + + do { + line = lineInputStream.readLine(); + if (line == null) { + serverResponse = buf.toString(); + if (serverResponse.length() == 0) + serverResponse = "[EOF]"; + lastServerResponse = serverResponse; + lastReturnCode = -1; + logger.log(Level.FINE, "EOF: {0}", serverResponse); + return -1; + } + buf.append(line); + buf.append("\n"); + } while (isNotLastLine(line)); + + serverResponse = buf.toString(); + } catch (IOException ioex) { + logger.log(Level.FINE, "exception reading response", ioex); + //ioex.printStackTrace(out); + lastServerResponse = ""; + lastReturnCode = 0; + throw new MessagingException("Exception reading response", ioex); + //returnCode = -1; + } + + // print debug info + //if (logger.isLoggable(Level.FINE)) + //logger.fine("RCVD: " + serverResponse); + + // parse out the return code + if (serverResponse.length() >= 3) { + try { + returnCode = Integer.parseInt(serverResponse.substring(0, 3)); + } catch (NumberFormatException nfe) { + try { + close(); + } catch (MessagingException mex) { + // thrown by close()--ignore, will close() later anyway + logger.log(Level.FINE, "close failed", mex); + } + returnCode = -1; + } catch (StringIndexOutOfBoundsException ex) { + try { + close(); + } catch (MessagingException mex) { + // thrown by close()--ignore, will close() later anyway + logger.log(Level.FINE, "close failed", mex); + } + returnCode = -1; + } + } else { + returnCode = -1; + } + if (returnCode == -1) + logger.log(Level.FINE, "bad server response: {0}", serverResponse); + + lastServerResponse = serverResponse; + lastReturnCode = returnCode; + return returnCode; + } + + /** + * Check if we're in the connected state. Don't bother checking + * whether the server is still alive, that will be detected later. + * + * @exception IllegalStateException if not connected + * + * @since JavaMail 1.4.1 + */ + protected void checkConnected() { + if (!super.isConnected()) + throw new IllegalStateException("Not connected"); + } + + // tests if the <code>line</code> is an intermediate line according to SMTP + private boolean isNotLastLine(String line) { + return line != null && line.length() >= 4 && line.charAt(3) == '-'; + } + + // wraps an address in "<>"'s if necessary + private String normalizeAddress(String addr) { + if ((!addr.startsWith("<")) && (!addr.endsWith(">"))) + return "<" + addr + ">"; + else + return addr; + } + + /** + * Return true if the SMTP server supports the specified service + * extension. Extensions are reported as results of the EHLO + * command when connecting to the server. See + * <A HREF="http://www.ietf.org/rfc/rfc1869.txt">RFC 1869</A> + * and other RFCs that define specific extensions. + * + * @param ext the service extension name + * @return true if the extension is supported + * + * @since JavaMail 1.3.2 + */ + public boolean supportsExtension(String ext) { + return extMap != null && + extMap.get(ext.toUpperCase(Locale.ENGLISH)) != null; + } + + /** + * Return the parameter the server provided for the specified + * service extension, or null if the extension isn't supported. + * + * @param ext the service extension name + * @return the extension parameter + * + * @since JavaMail 1.3.2 + */ + public String getExtensionParameter(String ext) { + return extMap == null ? null : + extMap.get(ext.toUpperCase(Locale.ENGLISH)); + } + + /** + * Does the server we're connected to support the specified + * authentication mechanism? Uses the extension information + * returned by the server from the EHLO command. + * + * @param auth the authentication mechanism + * @return true if the authentication mechanism is supported + * + * @since JavaMail 1.4.1 + */ + protected boolean supportsAuthentication(String auth) { + assert Thread.holdsLock(this); + if (extMap == null) + return false; + String a = extMap.get("AUTH"); + if (a == null) + return false; + StringTokenizer st = new StringTokenizer(a); + while (st.hasMoreTokens()) { + String tok = st.nextToken(); + if (tok.equalsIgnoreCase(auth)) + return true; + } + // hack for buggy servers that advertise capability incorrectly + if (auth.equalsIgnoreCase("LOGIN") && supportsExtension("AUTH=LOGIN")) { + logger.fine("use AUTH=LOGIN hack"); + return true; + } + return false; + } + + private static char[] hexchar = { + '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' + }; + + /** + * Convert a string to RFC 1891 xtext format. + * + * <pre> + * xtext = *( xchar / hexchar ) + * + * xchar = any ASCII CHAR between "!" (33) and "~" (126) inclusive, + * except for "+" and "=". + * + * ; "hexchar"s are intended to encode octets that cannot appear + * ; as ASCII characters within an esmtp-value. + * + * hexchar = ASCII "+" immediately followed by two upper case + * hexadecimal digits + * </pre> + * + * @param s the string to convert + * @return the xtext format string + * @since JavaMail 1.4.1 + */ + // XXX - keeping this around only for compatibility + protected static String xtext(String s) { + return xtext(s, false); + } + + /** + * Like xtext(s), but allow UTF-8 strings. + * + * @param s the string to convert + * @param utf8 convert string to UTF-8 first? + * @return the xtext format string + * @since JavaMail 1.6.0 + */ + protected static String xtext(String s, boolean utf8) { + StringBuilder sb = null; + byte[] bytes; + if (utf8) + bytes = s.getBytes(StandardCharsets.UTF_8); + else + bytes = ASCIIUtility.getBytes(s); + for (int i = 0; i < bytes.length; i++) { + char c = (char)(((int)bytes[i])&0xff); + if (!utf8 && c >= 128) // not ASCII + throw new IllegalArgumentException( + "Non-ASCII character in SMTP submitter: " + s); + if (c < '!' || c > '~' || c == '+' || c == '=') { + // not printable ASCII + if (sb == null) { + sb = new StringBuilder(s.length() + 4); + sb.append(s.substring(0, i)); + } + sb.append('+'); + sb.append(hexchar[(((int)c)& 0xf0) >> 4]); + sb.append(hexchar[((int)c)& 0x0f]); + } else { + if (sb != null) + sb.append(c); + } + } + return sb != null ? sb.toString() : s; + } + + private String traceUser(String user) { + return debugusername ? user : "<user name suppressed>"; + } + + private String tracePassword(String password) { + return debugpassword ? password : + (password == null ? "<null>" : "<non-null>"); + } + + /** + * Convert the String to either ASCII or UTF-8 bytes + * depending on allowutf8. + */ + private byte[] toBytes(String s) { + if (allowutf8) + return s.getBytes(StandardCharsets.UTF_8); + else + // don't use StandardCharsets.US_ASCII because it rejects non-ASCII + return ASCIIUtility.getBytes(s); + } + + /* + * Probe points for GlassFish monitoring. + */ + private void sendMessageStart(String subject) { } + private void sendMessageEnd() { } + + + /** + * An SMTPOutputStream that wraps a ChunkedOutputStream. + */ + private class BDATOutputStream extends SMTPOutputStream { + + /** + * Create a BDATOutputStream that wraps a ChunkedOutputStream + * of the given size and built on top of the specified + * underlying output stream. + * + * @param out the underlying output stream + * @param size the chunk size + */ + public BDATOutputStream(OutputStream out, int size) { + super(new ChunkedOutputStream(out, size)); + } + + /** + * Close this output stream. + * + * @exception IOException for I/O errors + */ + @Override + public void close() throws IOException { + out.close(); + } + } + + /** + * An OutputStream that buffers data in chunks and uses the + * RFC 3030 BDAT SMTP command to send each chunk. + */ + private class ChunkedOutputStream extends OutputStream { + private final OutputStream out; + private final byte[] buf; + private int count = 0; + + /** + * Create a ChunkedOutputStream built on top of the specified + * underlying output stream. + * + * @param out the underlying output stream + * @param size the chunk size + */ + public ChunkedOutputStream(OutputStream out, int size) { + this.out = out; + buf = new byte[size]; + } + + /** + * Writes the specified <code>byte</code> to this output stream. + * + * @param b the byte to write + * @exception IOException for I/O errors + */ + @Override + public void write(int b) throws IOException { + buf[count++] = (byte)b; + if (count >= buf.length) + flush(); + } + + /** + * Writes len bytes to this output stream starting at off. + * + * @param b bytes to write + * @param off offset in array + * @param len number of bytes to write + * @exception IOException for I/O errors + */ + @Override + public void write(byte b[], int off, int len) throws IOException { + while (len > 0) { + int size = Math.min(buf.length - count, len); + if (size == buf.length) { + // avoid the copy + bdat(b, off, size, false); + } else { + System.arraycopy(b, off, buf, count, size); + count += size; + } + off += size; + len -= size; + if (count >= buf.length) + flush(); + } + } + + /** + * Flush this output stream. + * + * @exception IOException for I/O errors + */ + @Override + public void flush() throws IOException { + bdat(buf, 0, count, false); + count = 0; + } + + /** + * Close this output stream. + * + * @exception IOException for I/O errors + */ + @Override + public void close() throws IOException { + bdat(buf, 0, count, true); + count = 0; + } + + /** + * Send the specified bytes using the BDAT command. + */ + private void bdat(byte[] b, int off, int len, boolean last) + throws IOException { + if (len > 0 || last) { + try { + if (last) + sendCommand("BDAT " + len + " LAST"); + else + sendCommand("BDAT " + len); + out.write(b, off, len); + out.flush(); + int ret = readServerResponse(); + if (ret != 250) + throw new IOException(lastServerResponse); + } catch (MessagingException mex) { + throw new IOException("BDAT write exception", mex); + } + } + } + } +}
diff --git a/mail/src/main/java/com/sun/mail/smtp/SaslAuthenticator.java b/mail/src/main/java/com/sun/mail/smtp/SaslAuthenticator.java new file mode 100644 index 0000000..2c2a645 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/smtp/SaslAuthenticator.java
@@ -0,0 +1,29 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.smtp; + +import javax.mail.MessagingException; + +/** + * Interface to make it easier to call SMTPSaslAuthenticator. + */ + +public interface SaslAuthenticator { + public boolean authenticate(String[] mechs, String realm, String authzid, + String u, String p) throws MessagingException; + +}
diff --git a/mail/src/main/java/com/sun/mail/smtp/package.html b/mail/src/main/java/com/sun/mail/smtp/package.html new file mode 100644 index 0000000..13f608e --- /dev/null +++ b/mail/src/main/java/com/sun/mail/smtp/package.html
@@ -0,0 +1,822 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> +<HTML> +<HEAD> +<meta http-equiv="Content-Type" content="text/html; charset=utf-8"> +<!-- + + Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<TITLE>com.sun.mail.smtp package</TITLE> +</HEAD> +<BODY BGCOLOR="white"> + +An SMTP protocol provider for the JavaMail API +that provides access to an SMTP server. +Refer to <A HREF="http://www.ietf.org/rfc/rfc821.txt" TARGET="_top">RFC 821</A> +for more information. +<P> +When sending a message, detailed information on each address that +fails is available in an +{@link com.sun.mail.smtp.SMTPAddressFailedException SMTPAddressFailedException} +chained off the top level +{@link javax.mail.SendFailedException SendFailedException} +that is thrown. +In addition, if the <code>mail.smtp.reportsuccess</code> property +is set, an +{@link com.sun.mail.smtp.SMTPAddressSucceededException +SMTPAddressSucceededException} +will be included in the list for each address that is successful. +Note that this will cause a top level +{@link javax.mail.SendFailedException SendFailedException} +to be thrown even though the send was successful. +</P> +<P> +The SMTP provider also supports ESMTP +(<A HREF="http://www.ietf.org/rfc/rfc1651.txt" TARGET="_top">RFC 1651</A>). +It can optionally use SMTP Authentication +(<A HREF="http://www.ietf.org/rfc/rfc2554.txt" TARGET="_top">RFC 2554</A>) +using the LOGIN, PLAIN, DIGEST-MD5, and NTLM mechanisms +(<A HREF="http://www.ietf.org/rfc/rfc4616.txt" TARGET="_top">RFC 4616</A> +and <A HREF="http://www.ietf.org/rfc/rfc2831.txt" TARGET="_top">RFC 2831</A>). +</P> +<P> +To use SMTP authentication you'll need to set the <code>mail.smtp.auth</code> +property (see below) or provide the SMTP Transport +with a username and password when connecting to the SMTP server. You +can do this using one of the following approaches: +</P> +<UL> +<LI> +<P> +Provide an Authenticator object when creating your mail Session +and provide the username and password information during the +Authenticator callback. +</P> +<P> +Note that the <code>mail.smtp.user</code> property can be set to provide a +default username for the callback, but the password will still need to be +supplied explicitly. +</P> +<P> +This approach allows you to use the static Transport <code>send</code> method +to send messages. +</P> +</LI> +<LI> +<P> +Call the Transport <code>connect</code> method explicitly with username and +password arguments. +</P> +<P> +This approach requires you to explicitly manage a Transport object +and use the Transport <code>sendMessage</code> method to send the message. +The transport.java demo program demonstrates how to manage a Transport +object. The following is roughly equivalent to the static +Transport <code>send</code> method, but supplies the needed username and +password: +</P> +<BLOCKQUOTE><PRE> +Transport tr = session.getTransport("smtp"); +tr.connect(smtphost, username, password); +msg.saveChanges(); // don't forget this +tr.sendMessage(msg, msg.getAllRecipients()); +tr.close(); +</PRE></BLOCKQUOTE> +</LI> +</UL> +<P> +When using DIGEST-MD5 authentication, +you'll also need to supply an appropriate realm; +your mail server administrator can supply this information. +You can set this using the <code>mail.smtp.sasl.realm</code> property, +or the <code>setSASLRealm</code> method on <code>SMTPTransport</code>. +</P> +<P> +The SMTP protocol provider can use SASL +(<A HREF="http://www.ietf.org/rfc/rfc2222.txt" TARGET="_top">RFC 2222</A>) +authentication mechanisms on systems that support the +<CODE>javax.security.sasl</CODE> APIs, such as J2SE 5.0. +In addition to the SASL mechanisms that are built into +the SASL implementation, users can also provide additional +SASL mechanisms of their own design to support custom authentication +schemes. See the +<A HREF="http://java.sun.com/j2se/1.5.0/docs/guide/security/sasl/sasl-refguide.html" TARGET="_top"> +Java SASL API Programming and Deployment Guide</A> for details. +Note that the current implementation doesn't support SASL mechanisms +that provide their own integrity or confidentiality layer. +</P> +<P> +Support for OAuth 2.0 authentication via the +<A HREF="https://developers.google.com/gmail/xoauth2_protocol" TARGET="_top"> +XOAUTH2 authentication mechanism</A> is provided either through the SASL +support described above or as a built-in authentication mechanism in the +SMTP provider. +The OAuth 2.0 Access Token should be passed as the password for this mechanism. +See <A HREF="https://java.net/projects/javamail/pages/OAuth2" TARGET="_top"> +OAuth2 Support</A> for details. +</P> +<P> +SMTP can also optionally request Delivery Status Notifications +(<A HREF="http://www.ietf.org/rfc/rfc1891.txt" TARGET="_top">RFC 1891</A>). +The delivery status will typically be reported using +a "multipart/report" +(<A HREF="http://www.ietf.org/rfc/rfc1892.txt" TARGET="_top">RFC 1892</A>) +message type with a "message/delivery-status" +(<A HREF="http://www.ietf.org/rfc/rfc1894.txt" TARGET="_top">RFC 1894</A>) +part. +You can use the classes in the <code>com.sun.mail.dsn</code> package to +handle these MIME types. +Note that you'll need to include <code>dsn.jar</code> in your CLASSPATH +as this support is not included in <code>mail.jar</code>. +</P> +<P> +See below for the properties to enable these features. +</P> +<P> +Note also that <strong>THERE IS NOT SUFFICIENT DOCUMENTATION HERE TO USE THESE +FEATURES!!!</strong> You will need to read the appropriate RFCs mentioned above +to understand what these features do and how to use them. Don't just +start setting properties and then complain to us when it doesn't work +like you expect it to work. <strong>READ THE RFCs FIRST!!!</strong> +</P> +<P> +The SMTP protocol provider supports the CHUNKING extension defined in +<A HREF="http://www.ietf.org/rfc/rfc3030.txt" TARGET="_top">RFC 3030</A>. +Set the <code>mail.smtp.chunksize</code> property to the desired chunk +size in bytes. +If the server supports the CHUNKING extension, the BDAT command will be +used to send the message in chunksize pieces. Note that no pipelining is +done so this will be slower than sending the message in one piece. +Note also that the BINARYMIME extension described in RFC 3030 is NOT supported. +</P> +<A NAME="properties"><STRONG>Properties</STRONG></A> +<P> +The SMTP protocol provider supports the following properties, +which may be set in the JavaMail <code>Session</code> object. +The properties are always set as strings; the Type column describes +how the string is interpreted. For example, use +</P> +<PRE> + props.put("mail.smtp.port", "888"); +</PRE> +<P> +to set the <CODE>mail.smtp.port</CODE> property, which is of type int. +</P> +<P> +Note that if you're using the "smtps" protocol to access SMTP over SSL, +all the properties would be named "mail.smtps.*". +</P> +<TABLE BORDER SUMMARY="SMTP properties"> +<TR> +<TH>Name</TH> +<TH>Type</TH> +<TH>Description</TH> +</TR> + +<TR> +<TD><A NAME="mail.smtp.user">mail.smtp.user</A></TD> +<TD>String</TD> +<TD>Default user name for SMTP.</TD> +</TR> + +<TR> +<TD><A NAME="mail.smtp.host">mail.smtp.host</A></TD> +<TD>String</TD> +<TD>The SMTP server to connect to.</TD> +</TR> + +<TR> +<TD><A NAME="mail.smtp.port">mail.smtp.port</A></TD> +<TD>int</TD> +<TD>The SMTP server port to connect to, if the connect() method doesn't +explicitly specify one. Defaults to 25.</TD> +</TR> + +<TR> +<TD><A NAME="mail.smtp.connectiontimeout">mail.smtp.connectiontimeout</A></TD> +<TD>int</TD> +<TD>Socket connection timeout value in milliseconds. +This timeout is implemented by java.net.Socket. +Default is infinite timeout.</TD> +</TR> + +<TR> +<TD><A NAME="mail.smtp.timeout">mail.smtp.timeout</A></TD> +<TD>int</TD> +<TD>Socket read timeout value in milliseconds. +This timeout is implemented by java.net.Socket. +Default is infinite timeout.</TD> +</TR> + +<TR> +<TD><A NAME="mail.smtp.writetimeout">mail.smtp.writetimeout</A></TD> +<TD>int</TD> +<TD>Socket write timeout value in milliseconds. +This timeout is implemented by using a +java.util.concurrent.ScheduledExecutorService per connection +that schedules a thread to close the socket if the timeout expires. +Thus, the overhead of using this timeout is one thread per connection. +Default is infinite timeout.</TD> +</TR> + +<TR> +<TD><A NAME="mail.smtp.from">mail.smtp.from</A></TD> +<TD>String</TD> +<TD> +Email address to use for SMTP MAIL command. This sets the envelope +return address. Defaults to msg.getFrom() or +InternetAddress.getLocalAddress(). NOTE: mail.smtp.user was previously +used for this. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.smtp.localhost">mail.smtp.localhost</A></TD> +<TD>String</TD> +<TD> +Local host name used in the SMTP HELO or EHLO command. +Defaults to <code>InetAddress.getLocalHost().getHostName()</code>. +Should not normally need to +be set if your JDK and your name service are configured properly. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.smtp.localaddress">mail.smtp.localaddress</A></TD> +<TD>String</TD> +<TD> +Local address (host name) to bind to when creating the SMTP socket. +Defaults to the address picked by the Socket class. +Should not normally need to be set, but useful with multi-homed hosts +where it's important to pick a particular local address to bind to. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.smtp.localport">mail.smtp.localport</A></TD> +<TD>int</TD> +<TD> +Local port number to bind to when creating the SMTP socket. +Defaults to the port number picked by the Socket class. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.smtp.ehlo">mail.smtp.ehlo</A></TD> +<TD>boolean</TD> +<TD> +If false, do not attempt to sign on with the EHLO command. Defaults to +true. Normally failure of the EHLO command will fallback to the HELO +command; this property exists only for servers that don't fail EHLO +properly or don't implement EHLO properly. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.smtp.auth">mail.smtp.auth</A></TD> +<TD>boolean</TD> +<TD>If true, attempt to authenticate the user using the AUTH command. +Defaults to false.</TD> +</TR> + +<TR> +<TD><A NAME="mail.smtp.auth.mechanisms">mail.smtp.auth.mechanisms</A></TD> +<TD>String</TD> +<TD> +If set, lists the authentication mechanisms to consider, and the order +in which to consider them. Only mechanisms supported by the server and +supported by the current implementation will be used. +The default is <code>"LOGIN PLAIN DIGEST-MD5 NTLM"</code>, which includes all +the authentication mechanisms supported by the current implementation +except XOAUTH2. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.smtp.auth.login.disable">mail.smtp.auth.login.disable</A></TD> +<TD>boolean</TD> +<TD>If true, prevents use of the <code>AUTH LOGIN</code> command. +Default is false.</TD> +</TR> + +<TR> +<TD><A NAME="mail.smtp.auth.plain.disable">mail.smtp.auth.plain.disable</A></TD> +<TD>boolean</TD> +<TD>If true, prevents use of the <code>AUTH PLAIN</code> command. +Default is false.</TD> +</TR> + +<TR> +<TD><A NAME="mail.smtp.auth.digest-md5.disable">mail.smtp.auth.digest-md5.disable</A></TD> +<TD>boolean</TD> +<TD>If true, prevents use of the <code>AUTH DIGEST-MD5</code> command. +Default is false.</TD> +</TR> + +<TR> +<TD><A NAME="mail.smtp.auth.ntlm.disable">mail.smtp.auth.ntlm.disable</A></TD> +<TD>boolean</TD> +<TD>If true, prevents use of the <code>AUTH NTLM</code> command. +Default is false.</TD> +</TR> + +<TR> +<TD><A NAME="mail.smtp.auth.ntlm.domain">mail.smtp.auth.ntlm.domain</A></TD> +<TD>String</TD> +<TD> +The NTLM authentication domain. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.smtp.auth.ntlm.flags">mail.smtp.auth.ntlm.flags</A></TD> +<TD>int</TD> +<TD> +NTLM protocol-specific flags. +See <A HREF="http://curl.haxx.se/rfc/ntlm.html#theNtlmFlags" TARGET="_top"> +http://curl.haxx.se/rfc/ntlm.html#theNtlmFlags</A> for details. +</TD> +</TR> + +<!-- +<TR> +<TD><A NAME="mail.smtp.auth.ntlm.unicode">mail.smtp.auth.ntlm.unicode</A></TD> +<TD>boolean</TD> +<TD> +Set this to "true" if the username or password may use +Unicode UTF-8 encoded characters. Default is "true". +Currently has no effect. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.smtp.auth.ntlm.lmcompat">mail.smtp.auth.ntlm.lmcompat</A></TD> +<TD>int</TD> +<TD> +Sets the LM compatibility level, as described here: +<A HREF="http://curl.haxx.se/rfc/ntlm.html#ntlmVersion2" TARGET="_top"> +http://curl.haxx.se/rfc/ntlm.html#ntlmVersion2</A> +Defaults to "3". Currently not used. +</TD> +</TR> +--> + +<TR> +<TD><A NAME="mail.smtp.auth.xoauth2.disable">mail.smtp.auth.xoauth2.disable</A></TD> +<TD>boolean</TD> +<TD>If true, prevents use of the <code>AUTHENTICATE XOAUTH2</code> command. +Because the OAuth 2.0 protocol requires a special access token instead of +a password, this mechanism is disabled by default. Enable it by explicitly +setting this property to "false" or by setting the "mail.smtp.auth.mechanisms" +property to "XOAUTH2".</TD> +</TR> + +<TR> +<TD><A NAME="mail.smtp.submitter">mail.smtp.submitter</A></TD> +<TD>String</TD> +<TD>The submitter to use in the AUTH tag in the MAIL FROM command. +Typically used by a mail relay to pass along information about the +original submitter of the message. +See also the {@link com.sun.mail.smtp.SMTPMessage#setSubmitter setSubmitter} +method of {@link com.sun.mail.smtp.SMTPMessage SMTPMessage}. +Mail clients typically do not use this. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.smtp.dsn.notify">mail.smtp.dsn.notify</A></TD> +<TD>String</TD> +<TD>The NOTIFY option to the RCPT command. Either NEVER, or some +combination of SUCCESS, FAILURE, and DELAY (separated by commas).</TD> +</TR> + +<TR> +<TD><A NAME="mail.smtp.dsn.ret">mail.smtp.dsn.ret</A></TD> +<TD>String</TD> +<TD>The RET option to the MAIL command. Either FULL or HDRS.</TD> +</TR> + +<TR> +<TD><A NAME="mail.smtp.allow8bitmime">mail.smtp.allow8bitmime</A></TD> +<TD>boolean</TD> +<TD> +If set to true, and the server supports the 8BITMIME extension, text +parts of messages that use the "quoted-printable" or "base64" encodings +are converted to use "8bit" encoding if they follow the RFC2045 rules +for 8bit text. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.smtp.sendpartial">mail.smtp.sendpartial</A></TD> +<TD>boolean</TD> +<TD> +If set to true, and a message has some valid and some invalid +addresses, send the message anyway, reporting the partial failure with +a SendFailedException. If set to false (the default), the message is +not sent to any of the recipients if there is an invalid recipient +address. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.smtp.sasl.enable">mail.smtp.sasl.enable</A></TD> +<TD>boolean</TD> +<TD> +If set to true, attempt to use the javax.security.sasl package to +choose an authentication mechanism for login. +Defaults to false. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.smtp.sasl.mechanisms">mail.smtp.sasl.mechanisms</A></TD> +<TD>String</TD> +<TD> +A space or comma separated list of SASL mechanism names to try +to use. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.smtp.sasl.authorizationid">mail.smtp.sasl.authorizationid</A></TD> +<TD>String</TD> +<TD> +The authorization ID to use in the SASL authentication. +If not set, the authentication ID (user name) is used. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.smtp.sasl.realm">mail.smtp.sasl.realm</A></TD> +<TD>String</TD> +<TD>The realm to use with DIGEST-MD5 authentication.</TD> +</TR> + +<TR> +<TD><A NAME="mail.smtp.sasl.usecanonicalhostname">mail.smtp.sasl.usecanonicalhostname</A></TD> +<TD>boolean</TD> +<TD> +If set to true, the canonical host name returned by +{@link java.net.InetAddress#getCanonicalHostName InetAddress.getCanonicalHostName} +is passed to the SASL mechanism, instead of the host name used to connect. +Defaults to false. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.smtp.quitwait">mail.smtp.quitwait</A></TD> +<TD>boolean</TD> +<TD> +If set to false, the QUIT command is sent +and the connection is immediately closed. +If set to true (the default), causes the transport to wait +for the response to the QUIT command. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.smtp.reportsuccess">mail.smtp.reportsuccess</A></TD> +<TD>boolean</TD> +<TD> +If set to true, causes the transport to include an +{@link com.sun.mail.smtp.SMTPAddressSucceededException +SMTPAddressSucceededException} +for each address that is successful. +Note also that this will cause a +{@link javax.mail.SendFailedException SendFailedException} +to be thrown from the +{@link com.sun.mail.smtp.SMTPTransport#sendMessage sendMessage} +method of +{@link com.sun.mail.smtp.SMTPTransport SMTPTransport} +even if all addresses were correct and the message was sent +successfully. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.smtp.socketFactory">mail.smtp.socketFactory</A></TD> +<TD>SocketFactory</TD> +<TD> +If set to a class that implements the +<code>javax.net.SocketFactory</code> interface, this class +will be used to create SMTP sockets. Note that this is an +instance of a class, not a name, and must be set using the +<code>put</code> method, not the <code>setProperty</code> method. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.smtp.socketFactory.class">mail.smtp.socketFactory.class</A></TD> +<TD>String</TD> +<TD> +If set, specifies the name of a class that implements the +<code>javax.net.SocketFactory</code> interface. This class +will be used to create SMTP sockets. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.smtp.socketFactory.fallback">mail.smtp.socketFactory.fallback</A></TD> +<TD>boolean</TD> +<TD> +If set to true, failure to create a socket using the specified +socket factory class will cause the socket to be created using +the <code>java.net.Socket</code> class. +Defaults to true. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.smtp.socketFactory.port">mail.smtp.socketFactory.port</A></TD> +<TD>int</TD> +<TD> +Specifies the port to connect to when using the specified socket +factory. +If not set, the default port will be used. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.smtp.ssl.enable">mail.smtp.ssl.enable</A></TD> +<TD>boolean</TD> +<TD> +If set to true, use SSL to connect and use the SSL port by default. +Defaults to false for the "smtp" protocol and true for the "smtps" protocol. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.smtp.ssl.checkserveridentity">mail.smtp.ssl.checkserveridentity</A></TD> +<TD>boolean</TD> +<TD> +If set to true, check the server identity as specified by +<A HREF="http://www.ietf.org/rfc/rfc2595.txt" TARGET="_top">RFC 2595</A>. +These additional checks based on the content of the server's certificate +are intended to prevent man-in-the-middle attacks. +Defaults to false. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.smtp.ssl.trust">mail.smtp.ssl.trust</A></TD> +<TD>String</TD> +<TD> +If set, and a socket factory hasn't been specified, enables use of a +{@link com.sun.mail.util.MailSSLSocketFactory MailSSLSocketFactory}. +If set to "*", all hosts are trusted. +If set to a whitespace separated list of hosts, those hosts are trusted. +Otherwise, trust depends on the certificate the server presents. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.smtp.ssl.socketFactory">mail.smtp.ssl.socketFactory</A></TD> +<TD>SSLSocketFactory</TD> +<TD> +If set to a class that extends the +<code>javax.net.ssl.SSLSocketFactory</code> class, this class +will be used to create SMTP SSL sockets. Note that this is an +instance of a class, not a name, and must be set using the +<code>put</code> method, not the <code>setProperty</code> method. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.smtp.ssl.socketFactory.class">mail.smtp.ssl.socketFactory.class</A></TD> +<TD>String</TD> +<TD> +If set, specifies the name of a class that extends the +<code>javax.net.ssl.SSLSocketFactory</code> class. This class +will be used to create SMTP SSL sockets. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.smtp.ssl.socketFactory.port">mail.smtp.ssl.socketFactory.port</A></TD> +<TD>int</TD> +<TD> +Specifies the port to connect to when using the specified socket +factory. +If not set, the default port will be used. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.smtp.ssl.protocols">mail.smtp.ssl.protocols</A></TD> +<TD>string</TD> +<TD> +Specifies the SSL protocols that will be enabled for SSL connections. +The property value is a whitespace separated list of tokens acceptable +to the <code>javax.net.ssl.SSLSocket.setEnabledProtocols</code> method. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.smtp.ssl.ciphersuites">mail.smtp.ssl.ciphersuites</A></TD> +<TD>string</TD> +<TD> +Specifies the SSL cipher suites that will be enabled for SSL connections. +The property value is a whitespace separated list of tokens acceptable +to the <code>javax.net.ssl.SSLSocket.setEnabledCipherSuites</code> method. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.smtp.starttls.enable">mail.smtp.starttls.enable</A></TD> +<TD>boolean</TD> +<TD> +If true, enables the use of the <code>STARTTLS</code> command (if +supported by the server) to switch the connection to a TLS-protected +connection before issuing any login commands. +If the server does not support STARTTLS, the connection continues without +the use of TLS; see the +<A HREF="#mail.smtp.starttls.required"><code>mail.smtp.starttls.required</code></A> +property to fail if STARTTLS isn't supported. +Note that an appropriate trust store must configured so that the client +will trust the server's certificate. +Defaults to false. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.smtp.starttls.required">mail.smtp.starttls.required</A></TD> +<TD>boolean</TD> +<TD> +If true, requires the use of the <code>STARTTLS</code> command. +If the server doesn't support the STARTTLS command, or the command +fails, the connect method will fail. +Defaults to false. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.smtp.proxy.host">mail.smtp.proxy.host</A></TD> +<TD>string</TD> +<TD> +Specifies the host name of an HTTP web proxy server that will be used for +connections to the mail server. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.smtp.proxy.port">mail.smtp.proxy.port</A></TD> +<TD>string</TD> +<TD> +Specifies the port number for the HTTP web proxy server. +Defaults to port 80. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.smtp.proxy.user">mail.smtp.proxy.user</A></TD> +<TD>string</TD> +<TD> +Specifies the user name to use to authenticate with the HTTP web proxy server. +By default, no authentication is done. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.smtp.proxy.password">mail.smtp.proxy.password</A></TD> +<TD>string</TD> +<TD> +Specifies the password to use to authenticate with the HTTP web proxy server. +By default, no authentication is done. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.smtp.socks.host">mail.smtp.socks.host</A></TD> +<TD>string</TD> +<TD> +Specifies the host name of a SOCKS5 proxy server that will be used for +connections to the mail server. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.smtp.socks.port">mail.smtp.socks.port</A></TD> +<TD>string</TD> +<TD> +Specifies the port number for the SOCKS5 proxy server. +This should only need to be used if the proxy server is not using +the standard port number of 1080. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.smtp.mailextension">mail.smtp.mailextension</A></TD> +<TD>String</TD> +<TD> +Extension string to append to the MAIL command. +The extension string can be used to specify standard SMTP +service extensions as well as vendor-specific extensions. +Typically the application should use the +{@link com.sun.mail.smtp.SMTPTransport SMTPTransport} +method {@link com.sun.mail.smtp.SMTPTransport#supportsExtension +supportsExtension} +to verify that the server supports the desired service extension. +See <A HREF="http://www.ietf.org/rfc/rfc1869.txt" TARGET="_top">RFC 1869</A> +and other RFCs that define specific extensions. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.smtp.userset">mail.smtp.userset</A></TD> +<TD>boolean</TD> +<TD> +If set to true, use the RSET command instead of the NOOP command +in the {@link javax.mail.Transport#isConnected isConnected} method. +In some cases sendmail will respond slowly after many NOOP commands; +use of RSET avoids this sendmail issue. +Defaults to false. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.smtp.noop.strict">mail.smtp.noop.strict</A></TD> +<TD>boolean</TD> +<TD> +If set to true (the default), insist on a 250 response code from the NOOP +command to indicate success. The NOOP command is used by the +{@link javax.mail.Transport#isConnected isConnected} method to determine +if the connection is still alive. +Some older servers return the wrong response code on success, some +servers don't implement the NOOP command at all and so always return +a failure code. Set this property to false to handle servers +that are broken in this way. +Normally, when a server times out a connection, it will send a 421 +response code, which the client will see as the response to the next +command it issues. +Some servers send the wrong failure response code when timing out a +connection. +Do not set this property to false when dealing with servers that are +broken in this way. +</TD> +</TR> + +</TABLE> +<P> +In general, applications should not need to use the classes in this +package directly. Instead, they should use the APIs defined by +<code>javax.mail</code> package (and subpackages). Applications should +never construct instances of <code>SMTPTransport</code> directly. +Instead, they should use the +<code>Session</code> method <code>getTransport</code> to acquire an +appropriate <code>Transport</code> object. +</P> +<P> +In addition to printing debugging output as controlled by the +{@link javax.mail.Session Session} configuration, +the com.sun.mail.smtp provider logs the same information using +{@link java.util.logging.Logger} as described in the following table: +</P> +<TABLE BORDER SUMMARY="SMTP Loggers"> +<TR> +<TH>Logger Name</TH> +<TH>Logging Level</TH> +<TH>Purpose</TH> +</TR> + +<TR> +<TD>com.sun.mail.smtp</TD> +<TD>CONFIG</TD> +<TD>Configuration of the SMTPTransport</TD> +</TR> + +<TR> +<TD>com.sun.mail.smtp</TD> +<TD>FINE</TD> +<TD>General debugging output</TD> +</TR> + +<TR> +<TD>com.sun.mail.smtp.protocol</TD> +<TD>FINEST</TD> +<TD>Complete protocol trace</TD> +</TR> +</TABLE> +<P> +<strong>WARNING:</strong> The APIs unique to this package should be +considered <strong>EXPERIMENTAL</strong>. They may be changed in the +future in ways that are incompatible with applications using the +current APIs. +</P> + +</BODY> +</HTML>
diff --git a/mail/src/main/java/com/sun/mail/util/ASCIIUtility.java b/mail/src/main/java/com/sun/mail/util/ASCIIUtility.java new file mode 100644 index 0000000..4ab7a55 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/util/ASCIIUtility.java
@@ -0,0 +1,284 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.IOException; + +public class ASCIIUtility { + + // Private constructor so that this class is not instantiated + private ASCIIUtility() { } + + /** + * Convert the bytes within the specified range of the given byte + * array into a signed integer in the given radix . The range extends + * from <code>start</code> till, but not including <code>end</code>. <p> + * + * Based on java.lang.Integer.parseInt() + * + * @param b the bytes + * @param start the first byte offset + * @param end the last byte offset + * @param radix the radix + * @return the integer value + * @exception NumberFormatException for conversion errors + */ + public static int parseInt(byte[] b, int start, int end, int radix) + throws NumberFormatException { + if (b == null) + throw new NumberFormatException("null"); + + int result = 0; + boolean negative = false; + int i = start; + int limit; + int multmin; + int digit; + + if (end > start) { + if (b[i] == '-') { + negative = true; + limit = Integer.MIN_VALUE; + i++; + } else { + limit = -Integer.MAX_VALUE; + } + multmin = limit / radix; + if (i < end) { + digit = Character.digit((char)b[i++], radix); + if (digit < 0) { + throw new NumberFormatException( + "illegal number: " + toString(b, start, end) + ); + } else { + result = -digit; + } + } + while (i < end) { + // Accumulating negatively avoids surprises near MAX_VALUE + digit = Character.digit((char)b[i++], radix); + if (digit < 0) { + throw new NumberFormatException("illegal number"); + } + if (result < multmin) { + throw new NumberFormatException("illegal number"); + } + result *= radix; + if (result < limit + digit) { + throw new NumberFormatException("illegal number"); + } + result -= digit; + } + } else { + throw new NumberFormatException("illegal number"); + } + if (negative) { + if (i > start + 1) { + return result; + } else { /* Only got "-" */ + throw new NumberFormatException("illegal number"); + } + } else { + return -result; + } + } + + /** + * Convert the bytes within the specified range of the given byte + * array into a signed integer . The range extends from + * <code>start</code> till, but not including <code>end</code>. + * + * @param b the bytes + * @param start the first byte offset + * @param end the last byte offset + * @return the integer value + * @exception NumberFormatException for conversion errors + */ + public static int parseInt(byte[] b, int start, int end) + throws NumberFormatException { + return parseInt(b, start, end, 10); + } + + /** + * Convert the bytes within the specified range of the given byte + * array into a signed long in the given radix . The range extends + * from <code>start</code> till, but not including <code>end</code>. <p> + * + * Based on java.lang.Long.parseLong() + * + * @param b the bytes + * @param start the first byte offset + * @param end the last byte offset + * @param radix the radix + * @return the long value + * @exception NumberFormatException for conversion errors + */ + public static long parseLong(byte[] b, int start, int end, int radix) + throws NumberFormatException { + if (b == null) + throw new NumberFormatException("null"); + + long result = 0; + boolean negative = false; + int i = start; + long limit; + long multmin; + int digit; + + if (end > start) { + if (b[i] == '-') { + negative = true; + limit = Long.MIN_VALUE; + i++; + } else { + limit = -Long.MAX_VALUE; + } + multmin = limit / radix; + if (i < end) { + digit = Character.digit((char)b[i++], radix); + if (digit < 0) { + throw new NumberFormatException( + "illegal number: " + toString(b, start, end) + ); + } else { + result = -digit; + } + } + while (i < end) { + // Accumulating negatively avoids surprises near MAX_VALUE + digit = Character.digit((char)b[i++], radix); + if (digit < 0) { + throw new NumberFormatException("illegal number"); + } + if (result < multmin) { + throw new NumberFormatException("illegal number"); + } + result *= radix; + if (result < limit + digit) { + throw new NumberFormatException("illegal number"); + } + result -= digit; + } + } else { + throw new NumberFormatException("illegal number"); + } + if (negative) { + if (i > start + 1) { + return result; + } else { /* Only got "-" */ + throw new NumberFormatException("illegal number"); + } + } else { + return -result; + } + } + + /** + * Convert the bytes within the specified range of the given byte + * array into a signed long . The range extends from + * <code>start</code> till, but not including <code>end</code>. <p> + * + * @param b the bytes + * @param start the first byte offset + * @param end the last byte offset + * @return the long value + * @exception NumberFormatException for conversion errors + */ + public static long parseLong(byte[] b, int start, int end) + throws NumberFormatException { + return parseLong(b, start, end, 10); + } + + /** + * Convert the bytes within the specified range of the given byte + * array into a String. The range extends from <code>start</code> + * till, but not including <code>end</code>. + * + * @param b the bytes + * @param start the first byte offset + * @param end the last byte offset + * @return the String + */ + public static String toString(byte[] b, int start, int end) { + int size = end - start; + char[] theChars = new char[size]; + + for (int i = 0, j = start; i < size; ) + theChars[i++] = (char)(b[j++]&0xff); + + return new String(theChars); + } + + /** + * Convert the bytes into a String. + * + * @param b the bytes + * @return the String + * @since JavaMail 1.4.4 + */ + public static String toString(byte[] b) { + return toString(b, 0, b.length); + } + + public static String toString(ByteArrayInputStream is) { + int size = is.available(); + char[] theChars = new char[size]; + byte[] bytes = new byte[size]; + + is.read(bytes, 0, size); + for (int i = 0; i < size;) + theChars[i] = (char)(bytes[i++]&0xff); + + return new String(theChars); + } + + + public static byte[] getBytes(String s) { + char [] chars= s.toCharArray(); + int size = chars.length; + byte[] bytes = new byte[size]; + + for (int i = 0; i < size;) + bytes[i] = (byte) chars[i++]; + return bytes; + } + + public static byte[] getBytes(InputStream is) throws IOException { + + int len; + int size = 1024; + byte [] buf; + + + if (is instanceof ByteArrayInputStream) { + size = is.available(); + buf = new byte[size]; + len = is.read(buf, 0, size); + } + else { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + buf = new byte[size]; + while ((len = is.read(buf, 0, size)) != -1) + bos.write(buf, 0, len); + buf = bos.toByteArray(); + } + return buf; + } +}
diff --git a/mail/src/main/java/com/sun/mail/util/BASE64DecoderStream.java b/mail/src/main/java/com/sun/mail/util/BASE64DecoderStream.java new file mode 100644 index 0000000..11a8ede --- /dev/null +++ b/mail/src/main/java/com/sun/mail/util/BASE64DecoderStream.java
@@ -0,0 +1,454 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.*; + +/** + * This class implements a BASE64 Decoder. It is implemented as + * a FilterInputStream, so one can just wrap this class around + * any input stream and read bytes from this filter. The decoding + * is done as the bytes are read out. + * + * @author John Mani + * @author Bill Shannon + */ + +public class BASE64DecoderStream extends FilterInputStream { + // buffer of decoded bytes for single byte reads + private byte[] buffer = new byte[3]; + private int bufsize = 0; // size of the cache + private int index = 0; // index into the cache + + // buffer for almost 8K of typical 76 chars + CRLF lines, + // used by getByte method. this buffer contains encoded bytes. + private byte[] input_buffer = new byte[78*105]; + private int input_pos = 0; + private int input_len = 0;; + + private boolean ignoreErrors = false; + + /** + * Create a BASE64 decoder that decodes the specified input stream. + * The System property <code>mail.mime.base64.ignoreerrors</code> + * controls whether errors in the encoded data cause an exception + * or are ignored. The default is false (errors cause exception). + * + * @param in the input stream + */ + public BASE64DecoderStream(InputStream in) { + super(in); + // default to false + ignoreErrors = PropUtil.getBooleanSystemProperty( + "mail.mime.base64.ignoreerrors", false); + } + + /** + * Create a BASE64 decoder that decodes the specified input stream. + * + * @param in the input stream + * @param ignoreErrors ignore errors in encoded data? + */ + public BASE64DecoderStream(InputStream in, boolean ignoreErrors) { + super(in); + this.ignoreErrors = ignoreErrors; + } + + /** + * Read the next decoded byte from this input stream. The byte + * is returned as an <code>int</code> in the range <code>0</code> + * to <code>255</code>. If no byte is available because the end of + * the stream has been reached, the value <code>-1</code> is returned. + * This method blocks until input data is available, the end of the + * stream is detected, or an exception is thrown. + * + * @return next byte of data, or <code>-1</code> if the end of the + * stream is reached. + * @exception IOException if an I/O error occurs. + * @see java.io.FilterInputStream#in + */ + @Override + public int read() throws IOException { + if (index >= bufsize) { + bufsize = decode(buffer, 0, buffer.length); + if (bufsize <= 0) // buffer is empty + return -1; + index = 0; // reset index into buffer + } + return buffer[index++] & 0xff; // Zero off the MSB + } + + /** + * Reads up to <code>len</code> decoded bytes of data from this input stream + * into an array of bytes. This method blocks until some input is + * available. + * <p> + * + * @param buf the buffer into which the data is read. + * @param off the start offset of the data. + * @param len the maximum number of bytes read. + * @return the total number of bytes read into the buffer, or + * <code>-1</code> if there is no more data because the end of + * the stream has been reached. + * @exception IOException if an I/O error occurs. + */ + @Override + public int read(byte[] buf, int off, int len) throws IOException { + // empty out single byte read buffer + int off0 = off; + while (index < bufsize && len > 0) { + buf[off++] = buffer[index++]; + len--; + } + if (index >= bufsize) + bufsize = index = 0; + + int bsize = (len / 3) * 3; // round down to multiple of 3 bytes + if (bsize > 0) { + int size = decode(buf, off, bsize); + off += size; + len -= size; + + if (size != bsize) { // hit EOF? + if (off == off0) // haven't returned any data + return -1; + else // returned some data before hitting EOF + return off - off0; + } + } + + // finish up with a partial read if necessary + for (; len > 0; len--) { + int c = read(); + if (c == -1) // EOF + break; + buf[off++] = (byte)c; + } + + if (off == off0) // haven't returned any data + return -1; + else // returned some data before hitting EOF + return off - off0; + } + + /** + * Skips over and discards n bytes of data from this stream. + */ + @Override + public long skip(long n) throws IOException { + long skipped = 0; + while (n-- > 0 && read() >= 0) + skipped++; + return skipped; + } + + /** + * Tests if this input stream supports marks. Currently this class + * does not support marks + */ + @Override + public boolean markSupported() { + return false; // Maybe later .. + } + + /** + * Returns the number of bytes that can be read from this input + * stream without blocking. However, this figure is only + * a close approximation in case the original encoded stream + * contains embedded CRLFs; since the CRLFs are discarded, not decoded + */ + @Override + public int available() throws IOException { + // This is only an estimate, since in.available() + // might include CRLFs too .. + return ((in.available() * 3)/4 + (bufsize-index)); + } + + /** + * This character array provides the character to value map + * based on RFC1521. + */ + private final static char pem_array[] = { + 'A','B','C','D','E','F','G','H', // 0 + 'I','J','K','L','M','N','O','P', // 1 + 'Q','R','S','T','U','V','W','X', // 2 + 'Y','Z','a','b','c','d','e','f', // 3 + 'g','h','i','j','k','l','m','n', // 4 + 'o','p','q','r','s','t','u','v', // 5 + 'w','x','y','z','0','1','2','3', // 6 + '4','5','6','7','8','9','+','/' // 7 + }; + + private final static byte pem_convert_array[] = new byte[256]; + + static { + for (int i = 0; i < 255; i++) + pem_convert_array[i] = -1; + for (int i = 0; i < pem_array.length; i++) + pem_convert_array[pem_array[i]] = (byte)i; + } + + /** + * The decoder algorithm. Most of the complexity here is dealing + * with error cases. Returns the number of bytes decoded, which + * may be zero. Decoding is done by filling an int with 4 6-bit + * values by shifting them in from the bottom and then extracting + * 3 8-bit bytes from the int by shifting them out from the bottom. + * + * @param outbuf the buffer into which to put the decoded bytes + * @param pos position in the buffer to start filling + * @param len the number of bytes to fill + * @return the number of bytes filled, always a multiple + * of three, and may be zero + * @exception IOException if the data is incorrectly formatted + */ + private int decode(byte[] outbuf, int pos, int len) throws IOException { + int pos0 = pos; + while (len >= 3) { + /* + * We need 4 valid base64 characters before we start decoding. + * We skip anything that's not a valid base64 character (usually + * just CRLF). + */ + int got = 0; + int val = 0; + while (got < 4) { + int i = getByte(); + if (i == -1 || i == -2) { + boolean atEOF; + if (i == -1) { + if (got == 0) + return pos - pos0; + if (!ignoreErrors) + throw new DecodingException( + "BASE64Decoder: Error in encoded stream: " + + "needed 4 valid base64 characters " + + "but only got " + got + " before EOF" + + recentChars()); + atEOF = true; // don't read any more + } else { // i == -2 + // found a padding character, we're at EOF + // XXX - should do something to make EOF "sticky" + if (got < 2 && !ignoreErrors) + throw new DecodingException( + "BASE64Decoder: Error in encoded stream: " + + "needed at least 2 valid base64 characters," + + " but only got " + got + + " before padding character (=)" + + recentChars()); + + // didn't get any characters before padding character? + if (got == 0) + return pos - pos0; + atEOF = false; // need to keep reading + } + + // pad partial result with zeroes + + // how many bytes will we produce on output? + // (got always < 4, so size always < 3) + int size = got - 1; + if (size == 0) + size = 1; + + // handle the one padding character we've seen + got++; + val <<= 6; + + while (got < 4) { + if (!atEOF) { + // consume the rest of the padding characters, + // filling with zeroes + i = getByte(); + if (i == -1) { + if (!ignoreErrors) + throw new DecodingException( + "BASE64Decoder: Error in encoded " + + "stream: hit EOF while looking for " + + "padding characters (=)" + + recentChars()); + } else if (i != -2) { + if (!ignoreErrors) + throw new DecodingException( + "BASE64Decoder: Error in encoded " + + "stream: found valid base64 " + + "character after a padding character " + + "(=)" + recentChars()); + } + } + val <<= 6; + got++; + } + + // now pull out however many valid bytes we got + val >>= 8; // always skip first one + if (size == 2) + outbuf[pos + 1] = (byte)(val & 0xff); + val >>= 8; + outbuf[pos] = (byte)(val & 0xff); + // len -= size; // not needed, return below + pos += size; + return pos - pos0; + } else { + // got a valid byte + val <<= 6; + got++; + val |= i; + } + } + + // read 4 valid characters, now extract 3 bytes + outbuf[pos + 2] = (byte)(val & 0xff); + val >>= 8; + outbuf[pos + 1] = (byte)(val & 0xff); + val >>= 8; + outbuf[pos] = (byte)(val & 0xff); + len -= 3; + pos += 3; + } + return pos - pos0; + } + + /** + * Read the next valid byte from the input stream. + * Buffer lots of data from underlying stream in input_buffer, + * for efficiency. + * + * @return the next byte, -1 on EOF, or -2 if next byte is '=' + * (padding at end of encoded data) + */ + private int getByte() throws IOException { + int c; + do { + if (input_pos >= input_len) { + try { + input_len = in.read(input_buffer); + } catch (EOFException ex) { + return -1; + } + if (input_len <= 0) + return -1; + input_pos = 0; + } + // get the next byte in the buffer + c = input_buffer[input_pos++] & 0xff; + // is it a padding byte? + if (c == '=') + return -2; + // no, convert it + c = pem_convert_array[c]; + // loop until we get a legitimate byte + } while (c == -1); + return c; + } + + /** + * Return the most recent characters, for use in an error message. + */ + private String recentChars() { + // reach into the input buffer and extract up to 10 + // recent characters, to help in debugging. + String errstr = ""; + int nc = input_pos > 10 ? 10 : input_pos; + if (nc > 0) { + errstr += ", the " + nc + + " most recent characters were: \""; + for (int k = input_pos - nc; k < input_pos; k++) { + char c = (char)(input_buffer[k] & 0xff); + switch (c) { + case '\r': errstr += "\\r"; break; + case '\n': errstr += "\\n"; break; + case '\t': errstr += "\\t"; break; + default: + if (c >= ' ' && c < 0177) + errstr += c; + else + errstr += ("\\" + (int)c); + } + } + errstr += "\""; + } + return errstr; + } + + /** + * Base64 decode a byte array. No line breaks are allowed. + * This method is suitable for short strings, such as those + * in the IMAP AUTHENTICATE protocol, but not to decode the + * entire content of a MIME part. + * + * NOTE: inbuf may only contain valid base64 characters. + * Whitespace is not ignored. + * + * @param inbuf the byte array + * @return the decoded byte array + */ + public static byte[] decode(byte[] inbuf) { + int size = (inbuf.length / 4) * 3; + if (size == 0) + return inbuf; + + if (inbuf[inbuf.length - 1] == '=') { + size--; + if (inbuf[inbuf.length - 2] == '=') + size--; + } + byte[] outbuf = new byte[size]; + + int inpos = 0, outpos = 0; + size = inbuf.length; + while (size > 0) { + int val; + int osize = 3; + val = pem_convert_array[inbuf[inpos++] & 0xff]; + val <<= 6; + val |= pem_convert_array[inbuf[inpos++] & 0xff]; + val <<= 6; + if (inbuf[inpos] != '=') // End of this BASE64 encoding + val |= pem_convert_array[inbuf[inpos++] & 0xff]; + else + osize--; + val <<= 6; + if (inbuf[inpos] != '=') // End of this BASE64 encoding + val |= pem_convert_array[inbuf[inpos++] & 0xff]; + else + osize--; + if (osize > 2) + outbuf[outpos + 2] = (byte)(val & 0xff); + val >>= 8; + if (osize > 1) + outbuf[outpos + 1] = (byte)(val & 0xff); + val >>= 8; + outbuf[outpos] = (byte)(val & 0xff); + outpos += osize; + size -= 4; + } + return outbuf; + } + + /*** begin TEST program *** + public static void main(String argv[]) throws Exception { + FileInputStream infile = new FileInputStream(argv[0]); + BASE64DecoderStream decoder = new BASE64DecoderStream(infile); + int c; + + while ((c = decoder.read()) != -1) + System.out.print((char)c); + System.out.flush(); + } + *** end TEST program ***/ +}
diff --git a/mail/src/main/java/com/sun/mail/util/BASE64EncoderStream.java b/mail/src/main/java/com/sun/mail/util/BASE64EncoderStream.java new file mode 100644 index 0000000..036b534 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/util/BASE64EncoderStream.java
@@ -0,0 +1,306 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.*; + +/** + * This class implements a BASE64 encoder. It is implemented as + * a FilterOutputStream, so one can just wrap this class around + * any output stream and write bytes into this filter. The encoding + * is done as the bytes are written out. + * + * @author John Mani + * @author Bill Shannon + */ + +public class BASE64EncoderStream extends FilterOutputStream { + private byte[] buffer; // cache of bytes that are yet to be encoded + private int bufsize = 0; // size of the cache + private byte[] outbuf; // line size output buffer + private int count = 0; // number of bytes that have been output + private int bytesPerLine; // number of bytes per line + private int lineLimit; // number of input bytes to output bytesPerLine + private boolean noCRLF = false; + + private static byte[] newline = new byte[] { '\r', '\n' }; + + /** + * Create a BASE64 encoder that encodes the specified output stream. + * + * @param out the output stream + * @param bytesPerLine number of bytes per line. The encoder inserts + * a CRLF sequence after the specified number of bytes, + * unless bytesPerLine is Integer.MAX_VALUE, in which + * case no CRLF is inserted. bytesPerLine is rounded + * down to a multiple of 4. + */ + public BASE64EncoderStream(OutputStream out, int bytesPerLine) { + super(out); + buffer = new byte[3]; + if (bytesPerLine == Integer.MAX_VALUE || bytesPerLine < 4) { + noCRLF = true; + bytesPerLine = 76; + } + bytesPerLine = (bytesPerLine / 4) * 4; // Rounded down to 4n + this.bytesPerLine = bytesPerLine; // save it + lineLimit = bytesPerLine / 4 * 3; + + if (noCRLF) { + outbuf = new byte[bytesPerLine]; + } else { + outbuf = new byte[bytesPerLine + 2]; + outbuf[bytesPerLine] = (byte)'\r'; + outbuf[bytesPerLine + 1] = (byte)'\n'; + } + } + + /** + * Create a BASE64 encoder that encodes the specified input stream. + * Inserts the CRLF sequence after outputting 76 bytes. + * + * @param out the output stream + */ + public BASE64EncoderStream(OutputStream out) { + this(out, 76); + } + + /** + * Encodes <code>len</code> bytes from the specified + * <code>byte</code> array starting at offset <code>off</code> to + * this output stream. + * + * @param b the data. + * @param off the start offset in the data. + * @param len the number of bytes to write. + * @exception IOException if an I/O error occurs. + */ + @Override + public synchronized void write(byte[] b, int off, int len) + throws IOException { + int end = off + len; + + // finish off incomplete coding unit + while (bufsize != 0 && off < end) + write(b[off++]); + + // finish off line + int blen = ((bytesPerLine - count) / 4) * 3; + if (off + blen <= end) { + // number of bytes that will be produced in outbuf + int outlen = encodedSize(blen); + if (!noCRLF) { + outbuf[outlen++] = (byte)'\r'; + outbuf[outlen++] = (byte)'\n'; + } + out.write(encode(b, off, blen, outbuf), 0, outlen); + off += blen; + count = 0; + } + + // do bulk encoding a line at a time. + for (; off + lineLimit <= end; off += lineLimit) + out.write(encode(b, off, lineLimit, outbuf)); + + // handle remaining partial line + if (off + 3 <= end) { + blen = end - off; + blen = (blen / 3) * 3; // round down + // number of bytes that will be produced in outbuf + int outlen = encodedSize(blen); + out.write(encode(b, off, blen, outbuf), 0, outlen); + off += blen; + count += outlen; + } + + // start next coding unit + for (; off < end; off++) + write(b[off]); + } + + /** + * Encodes <code>b.length</code> bytes to this output stream. + * + * @param b the data to be written. + * @exception IOException if an I/O error occurs. + */ + @Override + public void write(byte[] b) throws IOException { + write(b, 0, b.length); + } + + /** + * Encodes the specified <code>byte</code> to this output stream. + * + * @param c the <code>byte</code>. + * @exception IOException if an I/O error occurs. + */ + @Override + public synchronized void write(int c) throws IOException { + buffer[bufsize++] = (byte)c; + if (bufsize == 3) { // Encoding unit = 3 bytes + encode(); + bufsize = 0; + } + } + + /** + * Flushes this output stream and forces any buffered output bytes + * to be encoded out to the stream. + * + * @exception IOException if an I/O error occurs. + */ + @Override + public synchronized void flush() throws IOException { + if (bufsize > 0) { // If there's unencoded characters in the buffer .. + encode(); // .. encode them + bufsize = 0; + } + out.flush(); + } + + /** + * Forces any buffered output bytes to be encoded out to the stream + * and closes this output stream + */ + @Override + public synchronized void close() throws IOException { + flush(); + if (count > 0 && !noCRLF) { + out.write(newline); + out.flush(); + } + out.close(); + } + + /** This array maps the characters to their 6 bit values */ + private final static char pem_array[] = { + 'A','B','C','D','E','F','G','H', // 0 + 'I','J','K','L','M','N','O','P', // 1 + 'Q','R','S','T','U','V','W','X', // 2 + 'Y','Z','a','b','c','d','e','f', // 3 + 'g','h','i','j','k','l','m','n', // 4 + 'o','p','q','r','s','t','u','v', // 5 + 'w','x','y','z','0','1','2','3', // 6 + '4','5','6','7','8','9','+','/' // 7 + }; + + /** + * Encode the data stored in <code>buffer</code>. + * Uses <code>outbuf</code> to store the encoded + * data before writing. + * + * @exception IOException if an I/O error occurs. + */ + private void encode() throws IOException { + int osize = encodedSize(bufsize); + out.write(encode(buffer, 0, bufsize, outbuf), 0, osize); + // increment count + count += osize; + // If writing out this encoded unit caused overflow, + // start a new line. + if (count >= bytesPerLine) { + if (!noCRLF) + out.write(newline); + count = 0; + } + } + + /** + * Base64 encode a byte array. No line breaks are inserted. + * This method is suitable for short strings, such as those + * in the IMAP AUTHENTICATE protocol, but not to encode the + * entire content of a MIME part. + * + * @param inbuf the byte array + * @return the encoded byte array + */ + public static byte[] encode(byte[] inbuf) { + if (inbuf.length == 0) + return inbuf; + return encode(inbuf, 0, inbuf.length, null); + } + + /** + * Internal use only version of encode. Allow specifying which + * part of the input buffer to encode. If outbuf is non-null, + * it's used as is. Otherwise, a new output buffer is allocated. + */ + private static byte[] encode(byte[] inbuf, int off, int size, + byte[] outbuf) { + if (outbuf == null) + outbuf = new byte[encodedSize(size)]; + int inpos, outpos; + int val; + for (inpos = off, outpos = 0; size >= 3; size -= 3, outpos += 4) { + val = inbuf[inpos++] & 0xff; + val <<= 8; + val |= inbuf[inpos++] & 0xff; + val <<= 8; + val |= inbuf[inpos++] & 0xff; + outbuf[outpos+3] = (byte)pem_array[val & 0x3f]; + val >>= 6; + outbuf[outpos+2] = (byte)pem_array[val & 0x3f]; + val >>= 6; + outbuf[outpos+1] = (byte)pem_array[val & 0x3f]; + val >>= 6; + outbuf[outpos+0] = (byte)pem_array[val & 0x3f]; + } + // done with groups of three, finish up any odd bytes left + if (size == 1) { + val = inbuf[inpos++] & 0xff; + val <<= 4; + outbuf[outpos+3] = (byte)'='; // pad character; + outbuf[outpos+2] = (byte)'='; // pad character; + outbuf[outpos+1] = (byte)pem_array[val & 0x3f]; + val >>= 6; + outbuf[outpos+0] = (byte)pem_array[val & 0x3f]; + } else if (size == 2) { + val = inbuf[inpos++] & 0xff; + val <<= 8; + val |= inbuf[inpos++] & 0xff; + val <<= 2; + outbuf[outpos+3] = (byte)'='; // pad character; + outbuf[outpos+2] = (byte)pem_array[val & 0x3f]; + val >>= 6; + outbuf[outpos+1] = (byte)pem_array[val & 0x3f]; + val >>= 6; + outbuf[outpos+0] = (byte)pem_array[val & 0x3f]; + } + return outbuf; + } + + /** + * Return the corresponding encoded size for the given number + * of bytes, not including any CRLF. + */ + private static int encodedSize(int size) { + return ((size + 2) / 3) * 4; + } + + /*** begin TEST program + public static void main(String argv[]) throws Exception { + FileInputStream infile = new FileInputStream(argv[0]); + BASE64EncoderStream encoder = new BASE64EncoderStream(System.out); + int c; + + while ((c = infile.read()) != -1) + encoder.write(c); + encoder.close(); + } + *** end TEST program **/ +}
diff --git a/mail/src/main/java/com/sun/mail/util/BEncoderStream.java b/mail/src/main/java/com/sun/mail/util/BEncoderStream.java new file mode 100644 index 0000000..5a43dd6 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/util/BEncoderStream.java
@@ -0,0 +1,50 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.*; + +/** + * This class implements a 'B' Encoder as defined by RFC2047 for + * encoding MIME headers. It subclasses the BASE64EncoderStream + * class. + * + * @author John Mani + */ + +public class BEncoderStream extends BASE64EncoderStream { + + /** + * Create a 'B' encoder that encodes the specified input stream. + * @param out the output stream + */ + public BEncoderStream(OutputStream out) { + super(out, Integer.MAX_VALUE); // MAX_VALUE is 2^31, should + // suffice (!) to indicate that + // CRLFs should not be inserted + } + + /** + * Returns the length of the encoded version of this byte array. + * + * @param b the byte array + * @return the length + */ + public static int encodedLength(byte[] b) { + return ((b.length + 2)/3) * 4; + } +}
diff --git a/mail/src/main/java/com/sun/mail/util/CRLFOutputStream.java b/mail/src/main/java/com/sun/mail/util/CRLFOutputStream.java new file mode 100644 index 0000000..7cca3d7 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/util/CRLFOutputStream.java
@@ -0,0 +1,88 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.*; + + +/** + * Convert lines into the canonical format, that is, terminate lines with the + * CRLF sequence. + * + * @author John Mani + */ +public class CRLFOutputStream extends FilterOutputStream { + protected int lastb = -1; + protected boolean atBOL = true; // at beginning of line? + private static final byte[] newline = { (byte)'\r', (byte)'\n' }; + + public CRLFOutputStream(OutputStream os) { + super(os); + } + + @Override + public void write(int b) throws IOException { + if (b == '\r') { + writeln(); + } else if (b == '\n') { + if (lastb != '\r') + writeln(); + } else { + out.write(b); + atBOL = false; + } + lastb = b; + } + + @Override + public void write(byte b[]) throws IOException { + write(b, 0, b.length); + } + + @Override + public void write(byte b[], int off, int len) throws IOException { + int start = off; + + len += off; + for (int i = start; i < len ; i++) { + if (b[i] == '\r') { + out.write(b, start, i - start); + writeln(); + start = i + 1; + } else if (b[i] == '\n') { + if (lastb != '\r') { + out.write(b, start, i - start); + writeln(); + } + start = i + 1; + } + lastb = b[i]; + } + if ((len - start) > 0) { + out.write(b, start, len - start); + atBOL = false; + } + } + + /* + * Just write out a new line, something similar to out.println() + */ + public void writeln() throws IOException { + out.write(newline); + atBOL = true; + } +}
diff --git a/mail/src/main/java/com/sun/mail/util/DecodingException.java b/mail/src/main/java/com/sun/mail/util/DecodingException.java new file mode 100644 index 0000000..fddd84f --- /dev/null +++ b/mail/src/main/java/com/sun/mail/util/DecodingException.java
@@ -0,0 +1,41 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.IOException; + +/** + * A special IOException that indicates a failure to decode data due + * to an error in the formatting of the data. This allows applications + * to distinguish decoding errors from other I/O errors. + * + * @author Bill Shannon + */ + +public class DecodingException extends IOException { + + private static final long serialVersionUID = -6913647794421459390L; + + /** + * Constructor. + * + * @param s the exception message + */ + public DecodingException(String s) { + super(s); + } +}
diff --git a/mail/src/main/java/com/sun/mail/util/FolderClosedIOException.java b/mail/src/main/java/com/sun/mail/util/FolderClosedIOException.java new file mode 100644 index 0000000..5a4ef10 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/util/FolderClosedIOException.java
@@ -0,0 +1,61 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.IOException; +import javax.mail.Folder; + +/** + * A variant of FolderClosedException that can be thrown from methods + * that only throw IOException. The getContent method will catch this + * exception and translate it back to FolderClosedException. + * + * @author Bill Shannon + */ + +public class FolderClosedIOException extends IOException { + transient private Folder folder; + + private static final long serialVersionUID = 4281122580365555735L; + + /** + * Constructor + * @param folder the Folder + */ + public FolderClosedIOException(Folder folder) { + this(folder, null); + } + + /** + * Constructor + * @param folder the Folder + * @param message the detailed error message + */ + public FolderClosedIOException(Folder folder, String message) { + super(message); + this.folder = folder; + } + + /** + * Returns the dead Folder object + * + * @return the dead Folder + */ + public Folder getFolder() { + return folder; + } +}
diff --git a/mail/src/main/java/com/sun/mail/util/LineInputStream.java b/mail/src/main/java/com/sun/mail/util/LineInputStream.java new file mode 100644 index 0000000..9cb7fb6 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/util/LineInputStream.java
@@ -0,0 +1,143 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.*; +import java.nio.charset.StandardCharsets; + +/** + * This class is to support reading CRLF terminated lines that + * contain only US-ASCII characters from an input stream. Provides + * functionality that is similar to the deprecated + * <code>DataInputStream.readLine()</code>. Expected use is to read + * lines as String objects from a RFC822 stream. + * + * It is implemented as a FilterInputStream, so one can just wrap + * this class around any input stream and read bytes from this filter. + * + * @author John Mani + * @author Bill Shannon + */ + +public class LineInputStream extends FilterInputStream { + + private boolean allowutf8; + private byte[] lineBuffer = null; // reusable byte buffer + private static int MAX_INCR = 1024*1024; // 1MB + + public LineInputStream(InputStream in) { + this(in, false); + } + + /** + * @param in the InputStream + * @param allowutf8 allow UTF-8 characters? + * @since JavaMail 1.6 + */ + public LineInputStream(InputStream in, boolean allowutf8) { + super(in); + this.allowutf8 = allowutf8; + } + + /** + * Read a line containing only ASCII characters from the input + * stream. A line is terminated by a CR or NL or CR-NL sequence. + * A common error is a CR-CR-NL sequence, which will also terminate + * a line. + * The line terminator is not returned as part of the returned + * String. Returns null if no data is available. <p> + * + * This class is similar to the deprecated + * <code>DataInputStream.readLine()</code> + * + * @return the line + * @exception IOException for I/O errors + */ + @SuppressWarnings("deprecation") // for old String constructor + public String readLine() throws IOException { + //InputStream in = this.in; + byte[] buf = lineBuffer; + + if (buf == null) + buf = lineBuffer = new byte[128]; + + int c1; + int room = buf.length; + int offset = 0; + + while ((c1 = in.read()) != -1) { + if (c1 == '\n') // Got NL, outa here. + break; + else if (c1 == '\r') { + // Got CR, is the next char NL ? + boolean twoCRs = false; + if (in.markSupported()) + in.mark(2); + int c2 = in.read(); + if (c2 == '\r') { // discard extraneous CR + twoCRs = true; + c2 = in.read(); + } + if (c2 != '\n') { + /* + * If the stream supports it (which we hope will always + * be the case), reset to after the first CR. Otherwise, + * we wrap a PushbackInputStream around the stream so we + * can unread the characters we don't need. The only + * problem with that is that the caller might stop + * reading from this LineInputStream, throw it away, + * and then start reading from the underlying stream. + * If that happens, the pushed back characters will be + * lost forever. + */ + if (in.markSupported()) + in.reset(); + else { + if (!(in instanceof PushbackInputStream)) + in /*= this.in*/ = new PushbackInputStream(in, 2); + if (c2 != -1) + ((PushbackInputStream)in).unread(c2); + if (twoCRs) + ((PushbackInputStream)in).unread('\r'); + } + } + break; // outa here. + } + + // Not CR, NL or CR-NL ... + // .. Insert the byte into our byte buffer + if (--room < 0) { // No room, need to grow. + if (buf.length < MAX_INCR) + buf = new byte[buf.length * 2]; + else + buf = new byte[buf.length + MAX_INCR]; + room = buf.length - offset - 1; + System.arraycopy(lineBuffer, 0, buf, 0, offset); + lineBuffer = buf; + } + buf[offset++] = (byte)c1; + } + + if ((c1 == -1) && (offset == 0)) + return null; + + if (allowutf8) + return new String(buf, 0, offset, StandardCharsets.UTF_8); + else + return new String(buf, 0, 0, offset); + } +}
diff --git a/mail/src/main/java/com/sun/mail/util/LineOutputStream.java b/mail/src/main/java/com/sun/mail/util/LineOutputStream.java new file mode 100644 index 0000000..d86f5ec --- /dev/null +++ b/mail/src/main/java/com/sun/mail/util/LineOutputStream.java
@@ -0,0 +1,72 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.*; +import java.nio.charset.StandardCharsets; + +/** + * This class is to support writing out Strings as a sequence of bytes + * terminated by a CRLF sequence. The String must contain only US-ASCII + * characters.<p> + * + * The expected use is to write out RFC822 style headers to an output + * stream. <p> + * + * @author John Mani + * @author Bill Shannon + */ + +public class LineOutputStream extends FilterOutputStream { + private boolean allowutf8; + + private static byte[] newline; + + static { + newline = new byte[2]; + newline[0] = (byte)'\r'; + newline[1] = (byte)'\n'; + } + + public LineOutputStream(OutputStream out) { + this(out, false); + } + + /** + * @param out the OutputStream + * @param allowutf8 allow UTF-8 characters? + * @since JavaMail 1.6 + */ + public LineOutputStream(OutputStream out, boolean allowutf8) { + super(out); + this.allowutf8 = allowutf8; + } + + public void writeln(String s) throws IOException { + byte[] bytes; + if (allowutf8) + bytes = s.getBytes(StandardCharsets.UTF_8); + else + bytes = ASCIIUtility.getBytes(s); + out.write(bytes); + out.write(newline); + } + + public void writeln() throws IOException { + out.write(newline); + } +}
diff --git a/mail/src/main/java/com/sun/mail/util/LogOutputStream.java b/mail/src/main/java/com/sun/mail/util/LogOutputStream.java new file mode 100644 index 0000000..c1c3153 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/util/LogOutputStream.java
@@ -0,0 +1,128 @@ +/* + * Copyright (c) 2008, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.logging.Level; + +/** + * Capture output lines and send them to the mail logger. + */ +public class LogOutputStream extends OutputStream { + protected MailLogger logger; + protected Level level; + + private int lastb = -1; + private byte[] buf = new byte[80]; + private int pos = 0; + + /** + * Log to the specified logger. + * + * @param logger the MailLogger + */ + public LogOutputStream(MailLogger logger) { + this.logger = logger; + this.level = Level.FINEST; + } + + @Override + public void write(int b) throws IOException { + if (!logger.isLoggable(level)) + return; + + if (b == '\r') { + logBuf(); + } else if (b == '\n') { + if (lastb != '\r') + logBuf(); + } else { + expandCapacity(1); + buf[pos++] = (byte)b; + } + lastb = b; + } + + @Override + public void write(byte b[]) throws IOException { + write(b, 0, b.length); + } + + @Override + public void write(byte b[], int off, int len) throws IOException { + int start = off; + + if (!logger.isLoggable(level)) + return; + len += off; + for (int i = start; i < len ; i++) { + if (b[i] == '\r') { + expandCapacity(i - start); + System.arraycopy(b, start, buf, pos, i - start); + pos += i - start; + logBuf(); + start = i + 1; + } else if (b[i] == '\n') { + if (lastb != '\r') { + expandCapacity(i - start); + System.arraycopy(b, start, buf, pos, i - start); + pos += i - start; + logBuf(); + } + start = i + 1; + } + lastb = b[i]; + } + if ((len - start) > 0) { + expandCapacity(len - start); + System.arraycopy(b, start, buf, pos, len - start); + pos += len - start; + } + } + + /** + * Log the specified message. + * Can be overridden by subclass to do different logging. + * + * @param msg the message to log + */ + protected void log(String msg) { + logger.log(level, msg); + } + + /** + * Convert the buffer to a string and log it. + */ + private void logBuf() { + String msg = new String(buf, 0, pos); + pos = 0; + log(msg); + } + + /** + * Ensure that the buffer can hold at least len bytes + * beyond the current position. + */ + private void expandCapacity(int len) { + while (pos + len > buf.length) { + byte[] nb = new byte[buf.length * 2]; + System.arraycopy(buf, 0, nb, 0, pos); + buf = nb; + } + } +}
diff --git a/mail/src/main/java/com/sun/mail/util/MailConnectException.java b/mail/src/main/java/com/sun/mail/util/MailConnectException.java new file mode 100644 index 0000000..768e90f --- /dev/null +++ b/mail/src/main/java/com/sun/mail/util/MailConnectException.java
@@ -0,0 +1,83 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import javax.mail.MessagingException; + +/** + * A MessagingException that indicates a socket connection attempt failed. + * Unlike java.net.ConnectException, it includes details of what we + * were trying to connect to. The underlying exception is available + * as the "cause" of this exception. + * + * @see java.net.ConnectException + * @author Bill Shannon + * @since JavaMail 1.5.0 + */ + +public class MailConnectException extends MessagingException { + private String host; + private int port; + private int cto; + + private static final long serialVersionUID = -3818807731125317729L; + + /** + * Constructs a MailConnectException. + * + * @param cex the SocketConnectException with the details + */ + public MailConnectException(SocketConnectException cex) { + super( + "Couldn't connect to host, port: " + + cex.getHost() + ", " + cex.getPort() + + "; timeout " + cex.getConnectionTimeout() + + (cex.getMessage() != null ? ("; " + cex.getMessage()) : "")); + // extract the details and save them here + this.host = cex.getHost(); + this.port = cex.getPort(); + this.cto = cex.getConnectionTimeout(); + setNextException(cex.getException()); + } + + /** + * The host we were trying to connect to. + * + * @return the host + */ + public String getHost() { + return host; + } + + /** + * The port we were trying to connect to. + * + * @return the port + */ + public int getPort() { + return port; + } + + /** + * The timeout used for the connection attempt. + * + * @return the connection timeout + */ + public int getConnectionTimeout() { + return cto; + } +}
diff --git a/mail/src/main/java/com/sun/mail/util/MailLogger.java b/mail/src/main/java/com/sun/mail/util/MailLogger.java new file mode 100644 index 0000000..29363ca --- /dev/null +++ b/mail/src/main/java/com/sun/mail/util/MailLogger.java
@@ -0,0 +1,419 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.PrintStream; +import java.text.MessageFormat; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.mail.Session; + +/** + * A simplified logger used by JavaMail to handle logging to a + * PrintStream and logging through a java.util.logging.Logger. + * If debug is set, messages are written to the PrintStream and + * prefixed by the specified prefix (which is not included in + * Logger messages). + * Messages are logged by the Logger based on the configuration + * of the logging system. + */ + +/* + * It would be so much simpler to just subclass Logger and override + * the log(LogRecord) method, as the javadocs suggest, but that doesn't + * work because Logger makes the decision about whether to log the message + * or not before calling the log(LogRecord) method. Instead, we just + * provide the few log methods we need here. + */ + +public final class MailLogger { + /** + * For log messages. + */ + private final Logger logger; + /** + * For debug output. + */ + private final String prefix; + /** + * Produce debug output? + */ + private final boolean debug; + /** + * Stream for debug output. + */ + private final PrintStream out; + + /** + * Construct a new MailLogger using the specified Logger name, + * debug prefix (e.g., "DEBUG"), debug flag, and PrintStream. + * + * @param name the Logger name + * @param prefix the prefix for debug output, or null for none + * @param debug if true, write to PrintStream + * @param out the PrintStream to write to + */ + public MailLogger(String name, String prefix, boolean debug, + PrintStream out) { + logger = Logger.getLogger(name); + this.prefix = prefix; + this.debug = debug; + this.out = out != null ? out : System.out; + } + + /** + * Construct a new MailLogger using the specified class' package + * name as the Logger name, + * debug prefix (e.g., "DEBUG"), debug flag, and PrintStream. + * + * @param clazz the Logger name is the package name of this class + * @param prefix the prefix for debug output, or null for none + * @param debug if true, write to PrintStream + * @param out the PrintStream to write to + */ + public MailLogger(Class<?> clazz, String prefix, boolean debug, + PrintStream out) { + String name = packageOf(clazz); + logger = Logger.getLogger(name); + this.prefix = prefix; + this.debug = debug; + this.out = out != null ? out : System.out; + } + + /** + * Construct a new MailLogger using the specified class' package + * name combined with the specified subname as the Logger name, + * debug prefix (e.g., "DEBUG"), debug flag, and PrintStream. + * + * @param clazz the Logger name is the package name of this class + * @param subname the Logger name relative to this Logger name + * @param prefix the prefix for debug output, or null for none + * @param debug if true, write to PrintStream + * @param out the PrintStream to write to + */ + public MailLogger(Class<?> clazz, String subname, String prefix, boolean debug, + PrintStream out) { + String name = packageOf(clazz) + "." + subname; + logger = Logger.getLogger(name); + this.prefix = prefix; + this.debug = debug; + this.out = out != null ? out : System.out; + } + + /** + * Construct a new MailLogger using the specified Logger name and + * debug prefix (e.g., "DEBUG"). Get the debug flag and PrintStream + * from the Session. + * + * @param name the Logger name + * @param prefix the prefix for debug output, or null for none + * @param session where to get the debug flag and PrintStream + */ + @Deprecated + public MailLogger(String name, String prefix, Session session) { + this(name, prefix, session.getDebug(), session.getDebugOut()); + } + + /** + * Construct a new MailLogger using the specified class' package + * name as the Logger name and the specified + * debug prefix (e.g., "DEBUG"). Get the debug flag and PrintStream + * from the Session. + * + * @param clazz the Logger name is the package name of this class + * @param prefix the prefix for debug output, or null for none + * @param session where to get the debug flag and PrintStream + */ + @Deprecated + public MailLogger(Class<?> clazz, String prefix, Session session) { + this(clazz, prefix, session.getDebug(), session.getDebugOut()); + } + + /** + * Create a MailLogger that uses a Logger with the specified name + * and prefix. The new MailLogger uses the same debug flag and + * PrintStream as this MailLogger. + * + * @param name the Logger name + * @param prefix the prefix for debug output, or null for none + * @return a MailLogger for the given name and prefix. + */ + public MailLogger getLogger(String name, String prefix) { + return new MailLogger(name, prefix, debug, out); + } + + /** + * Create a MailLogger using the specified class' package + * name as the Logger name and the specified prefix. + * The new MailLogger uses the same debug flag and + * PrintStream as this MailLogger. + * + * @param clazz the Logger name is the package name of this class + * @param prefix the prefix for debug output, or null for none + * @return a MailLogger for the given name and prefix. + */ + public MailLogger getLogger(Class<?> clazz, String prefix) { + return new MailLogger(clazz, prefix, debug, out); + } + + /** + * Create a MailLogger that uses a Logger whose name is composed + * of this MailLogger's name plus the specified sub-name, separated + * by a dot. The new MailLogger uses the new prefix for debug output. + * This is used primarily by the protocol trace code that wants a + * different prefix (none). + * + * @param subname the Logger name relative to this Logger name + * @param prefix the prefix for debug output, or null for none + * @return a MailLogger for the given name and prefix. + */ + public MailLogger getSubLogger(String subname, String prefix) { + return new MailLogger(logger.getName() + "." + subname, prefix, + debug, out); + } + + /** + * Create a MailLogger that uses a Logger whose name is composed + * of this MailLogger's name plus the specified sub-name, separated + * by a dot. The new MailLogger uses the new prefix for debug output. + * This is used primarily by the protocol trace code that wants a + * different prefix (none). + * + * @param subname the Logger name relative to this Logger name + * @param prefix the prefix for debug output, or null for none + * @param debug the debug flag for the sub-logger + * @return a MailLogger for the given name and prefix. + */ + public MailLogger getSubLogger(String subname, String prefix, + boolean debug) { + return new MailLogger(logger.getName() + "." + subname, prefix, + debug, out); + } + + /** + * Log the message at the specified level. + * @param level the log level. + * @param msg the message. + */ + public void log(Level level, String msg) { + ifDebugOut(msg); + if (logger.isLoggable(level)) { + final StackTraceElement frame = inferCaller(); + logger.logp(level, frame.getClassName(), frame.getMethodName(), msg); + } + } + + /** + * Log the message at the specified level. + * @param level the log level. + * @param msg the message. + * @param param1 the additional parameter. + */ + public void log(Level level, String msg, Object param1) { + if (debug) { + msg = MessageFormat.format(msg, new Object[] { param1 }); + debugOut(msg); + } + + if (logger.isLoggable(level)) { + final StackTraceElement frame = inferCaller(); + logger.logp(level, frame.getClassName(), frame.getMethodName(), msg, param1); + } + } + + /** + * Log the message at the specified level. + * @param level the log level. + * @param msg the message. + * @param params the message parameters. + */ + public void log(Level level, String msg, Object... params) { + if (debug) { + msg = MessageFormat.format(msg, params); + debugOut(msg); + } + + if (logger.isLoggable(level)) { + final StackTraceElement frame = inferCaller(); + logger.logp(level, frame.getClassName(), frame.getMethodName(), msg, params); + } + } + + /** + * Log the message at the specified level using a format string. + * @param level the log level. + * @param msg the message format string. + * @param params the message parameters. + * + * @since JavaMail 1.5.4 + */ + public void logf(Level level, String msg, Object... params) { + msg = String.format(msg, params); + ifDebugOut(msg); + logger.log(level, msg); + } + + /** + * Log the message at the specified level. + * @param level the log level. + * @param msg the message. + * @param thrown the throwable to log. + */ + public void log(Level level, String msg, Throwable thrown) { + if (debug) { + if (thrown != null) { + debugOut(msg + ", THROW: "); + thrown.printStackTrace(out); + } else { + debugOut(msg); + } + } + + if (logger.isLoggable(level)) { + final StackTraceElement frame = inferCaller(); + logger.logp(level, frame.getClassName(), frame.getMethodName(), msg, thrown); + } + } + + /** + * Log a message at the CONFIG level. + * @param msg the message. + */ + public void config(String msg) { + log(Level.CONFIG, msg); + } + + /** + * Log a message at the FINE level. + * @param msg the message. + */ + public void fine(String msg) { + log(Level.FINE, msg); + } + + /** + * Log a message at the FINER level. + * @param msg the message. + */ + public void finer(String msg) { + log(Level.FINER, msg); + } + + /** + * Log a message at the FINEST level. + * @param msg the message. + */ + public void finest(String msg) { + log(Level.FINEST, msg); + } + + /** + * If "debug" is set, or our embedded Logger is loggable at the + * given level, return true. + * @param level the log level. + * @return true if loggable. + */ + public boolean isLoggable(Level level) { + return debug || logger.isLoggable(level); + } + + /** + * Common code to conditionally log debug statements. + * @param msg the message to log. + */ + private void ifDebugOut(String msg) { + if (debug) + debugOut(msg); + } + + /** + * Common formatting for debug output. + * @param msg the message to log. + */ + private void debugOut(String msg) { + if (prefix != null) + out.println(prefix + ": " + msg); + else + out.println(msg); + } + + /** + * Return the package name of the class. + * Sometimes there will be no Package object for the class, + * e.g., if the class loader hasn't created one (see Class.getPackage()). + * @param clazz the class source. + * @return the package name or an empty string. + */ + private String packageOf(Class<?> clazz) { + Package p = clazz.getPackage(); + if (p != null) + return p.getName(); // hopefully the common case + String cname = clazz.getName(); + int i = cname.lastIndexOf('.'); + if (i > 0) + return cname.substring(0, i); + // no package name, now what? + return ""; + } + + /** + * A disadvantage of not being able to use Logger directly in JavaMail + * code is that the "source class" information that Logger guesses will + * always refer to this class instead of our caller. This method + * duplicates what Logger does to try to find *our* caller, so that + * Logger doesn't have to do it (and get the wrong answer), and because + * our caller is what's wanted. + * @return StackTraceElement that logged the message. Treat as read-only. + */ + private StackTraceElement inferCaller() { + // Get the stack trace. + StackTraceElement stack[] = (new Throwable()).getStackTrace(); + // First, search back to a method in the Logger class. + int ix = 0; + while (ix < stack.length) { + StackTraceElement frame = stack[ix]; + String cname = frame.getClassName(); + if (isLoggerImplFrame(cname)) { + break; + } + ix++; + } + // Now search for the first frame before the "Logger" class. + while (ix < stack.length) { + StackTraceElement frame = stack[ix]; + String cname = frame.getClassName(); + if (!isLoggerImplFrame(cname)) { + // We've found the relevant frame. + return frame; + } + ix++; + } + // We haven't found a suitable frame, so just punt. This is + // OK as we are only committed to making a "best effort" here. + return new StackTraceElement(MailLogger.class.getName(), "log", + MailLogger.class.getName(), -1); + } + + /** + * Frames to ignore as part of the MailLogger to JUL bridge. + * @param cname the class name. + * @return true if the class name is part of the MailLogger bridge. + */ + private boolean isLoggerImplFrame(String cname) { + return MailLogger.class.getName().equals(cname); + } +}
diff --git a/mail/src/main/java/com/sun/mail/util/MailSSLSocketFactory.java b/mail/src/main/java/com/sun/mail/util/MailSSLSocketFactory.java new file mode 100644 index 0000000..b715f36 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/util/MailSSLSocketFactory.java
@@ -0,0 +1,356 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.*; +import java.net.*; +import java.security.*; +import java.security.cert.*; +import java.util.*; + +import javax.net.ssl.*; + +/** + * An SSL socket factory that makes it easier to specify trust. + * This socket factory can be configured to trust all hosts or + * trust a specific set of hosts, in which case the server's + * certificate isn't verified. Alternatively, a custom TrustManager + * can be supplied. <p> + * + * An instance of this factory can be set as the value of the + * <code>mail.<protocol>.ssl.socketFactory</code> property. + * + * @since JavaMail 1.4.2 + * @author Stephan Sann + * @author Bill Shannon + */ +public class MailSSLSocketFactory extends SSLSocketFactory { + + /** Should all hosts be trusted? */ + private boolean trustAllHosts; + + /** String-array of trusted hosts */ + private String[] trustedHosts = null; + + /** Holds a SSLContext to get SSLSocketFactories from */ + private SSLContext sslcontext; + + /** Holds the KeyManager array to use */ + private KeyManager[] keyManagers; + + /** Holds the TrustManager array to use */ + private TrustManager[] trustManagers; + + /** Holds the SecureRandom to use */ + private SecureRandom secureRandom; + + /** Holds a SSLSocketFactory to pass all API-method-calls to */ + private SSLSocketFactory adapteeFactory = null; + + /** + * Initializes a new MailSSLSocketFactory. + * + * @throws GeneralSecurityException for security errors + */ + public MailSSLSocketFactory() throws GeneralSecurityException { + this("TLS"); + } + + /** + * Initializes a new MailSSLSocketFactory with a given protocol. + * Normally the protocol will be specified as "TLS". + * + * @param protocol The protocol to use + * @throws NoSuchAlgorithmException if given protocol is not supported + * @throws GeneralSecurityException for security errors + */ + public MailSSLSocketFactory(String protocol) + throws GeneralSecurityException { + + // By default we do NOT trust all hosts. + trustAllHosts = false; + + // Get an instance of an SSLContext. + sslcontext = SSLContext.getInstance(protocol); + + // Default properties to init the SSLContext + keyManagers = null; + trustManagers = new TrustManager[] { new MailTrustManager() }; + secureRandom = null; + + // Assemble a default SSLSocketFactory to delegate all API-calls to. + newAdapteeFactory(); + } + + + /** + * Gets an SSLSocketFactory based on the given (or default) + * KeyManager array, TrustManager array and SecureRandom and + * sets it to the instance var adapteeFactory. + * + * @throws KeyManagementException for key manager errors + */ + private synchronized void newAdapteeFactory() + throws KeyManagementException { + sslcontext.init(keyManagers, trustManagers, secureRandom); + + // Get SocketFactory and save it in our instance var + adapteeFactory = sslcontext.getSocketFactory(); + } + + /** + * @return the keyManagers + */ + public synchronized KeyManager[] getKeyManagers() { + return keyManagers.clone(); + } + + /** + * @param keyManagers the keyManagers to set + * @throws GeneralSecurityException for security errors + */ + public synchronized void setKeyManagers(KeyManager[] keyManagers) + throws GeneralSecurityException { + this.keyManagers = keyManagers.clone(); + newAdapteeFactory(); + } + + /** + * @return the secureRandom + */ + public synchronized SecureRandom getSecureRandom() { + return secureRandom; + } + + /** + * @param secureRandom the secureRandom to set + * @throws GeneralSecurityException for security errors + */ + public synchronized void setSecureRandom(SecureRandom secureRandom) + throws GeneralSecurityException { + this.secureRandom = secureRandom; + newAdapteeFactory(); + } + + /** + * @return the trustManagers + */ + public synchronized TrustManager[] getTrustManagers() { + return trustManagers; + } + + /** + * @param trustManagers the trustManagers to set + * @throws GeneralSecurityException for security errors + */ + public synchronized void setTrustManagers(TrustManager[] trustManagers) + throws GeneralSecurityException { + this.trustManagers = trustManagers; + newAdapteeFactory(); + } + + /** + * @return true if all hosts should be trusted + */ + public synchronized boolean isTrustAllHosts() { + return trustAllHosts; + } + + /** + * @param trustAllHosts should all hosts be trusted? + */ + public synchronized void setTrustAllHosts(boolean trustAllHosts) { + this.trustAllHosts = trustAllHosts; + } + + /** + * @return the trusted hosts + */ + public synchronized String[] getTrustedHosts() { + if (trustedHosts == null) + return null; + else + return trustedHosts.clone(); + } + + /** + * @param trustedHosts the hosts to trust + */ + public synchronized void setTrustedHosts(String[] trustedHosts) { + if (trustedHosts == null) + this.trustedHosts = null; + else + this.trustedHosts = trustedHosts.clone(); + } + + /** + * After a successful conection to the server, this method is + * called to ensure that the server should be trusted. + * + * @param server name of the server we connected to + * @param sslSocket SSLSocket connected to the server + * @return true if "trustAllHosts" is set to true OR the server + * is contained in the "trustedHosts" array; + */ + public synchronized boolean isServerTrusted(String server, + SSLSocket sslSocket) { + + //System.out.println("DEBUG: isServerTrusted host " + server); + + // If "trustAllHosts" is set to true, we return true + if (trustAllHosts) + return true; + + // If the socket host is contained in the "trustedHosts" array, + // we return true + if (trustedHosts != null) + return Arrays.asList(trustedHosts).contains(server); // ignore case? + + // If we get here, trust of the server was verified by the trust manager + return true; + } + + + // SocketFactory methods + + /* (non-Javadoc) + * @see javax.net.ssl.SSLSocketFactory#createSocket(java.net.Socket, + * java.lang.String, int, boolean) + */ + @Override + public synchronized Socket createSocket(Socket socket, String s, int i, + boolean flag) throws IOException { + return adapteeFactory.createSocket(socket, s, i, flag); + } + + /* (non-Javadoc) + * @see javax.net.ssl.SSLSocketFactory#getDefaultCipherSuites() + */ + @Override + public synchronized String[] getDefaultCipherSuites() { + return adapteeFactory.getDefaultCipherSuites(); + } + + /* (non-Javadoc) + * @see javax.net.ssl.SSLSocketFactory#getSupportedCipherSuites() + */ + @Override + public synchronized String[] getSupportedCipherSuites() { + return adapteeFactory.getSupportedCipherSuites(); + } + + /* (non-Javadoc) + * @see javax.net.SocketFactory#createSocket() + */ + @Override + public synchronized Socket createSocket() throws IOException { + return adapteeFactory.createSocket(); + } + + /* (non-Javadoc) + * @see javax.net.SocketFactory#createSocket(java.net.InetAddress, int, + * java.net.InetAddress, int) + */ + @Override + public synchronized Socket createSocket(InetAddress inetaddress, int i, + InetAddress inetaddress1, int j) throws IOException { + return adapteeFactory.createSocket(inetaddress, i, inetaddress1, j); + } + + /* (non-Javadoc) + * @see javax.net.SocketFactory#createSocket(java.net.InetAddress, int) + */ + @Override + public synchronized Socket createSocket(InetAddress inetaddress, int i) + throws IOException { + return adapteeFactory.createSocket(inetaddress, i); + } + + /* (non-Javadoc) + * @see javax.net.SocketFactory#createSocket(java.lang.String, int, + * java.net.InetAddress, int) + */ + @Override + public synchronized Socket createSocket(String s, int i, + InetAddress inetaddress, int j) + throws IOException, UnknownHostException { + return adapteeFactory.createSocket(s, i, inetaddress, j); + } + + /* (non-Javadoc) + * @see javax.net.SocketFactory#createSocket(java.lang.String, int) + */ + @Override + public synchronized Socket createSocket(String s, int i) + throws IOException, UnknownHostException { + return adapteeFactory.createSocket(s, i); + } + + + // inner classes + + /** + * A default Trustmanager. + * + * @author Stephan Sann + */ + private class MailTrustManager implements X509TrustManager { + + /** A TrustManager to pass method calls to */ + private X509TrustManager adapteeTrustManager = null; + + /** + * Initializes a new TrustManager instance. + */ + private MailTrustManager() throws GeneralSecurityException { + TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509"); + tmf.init((KeyStore)null); + adapteeTrustManager = (X509TrustManager)tmf.getTrustManagers()[0]; + } + + /* (non-Javadoc) + * @see javax.net.ssl.X509TrustManager#checkClientTrusted( + * java.security.cert.X509Certificate[], java.lang.String) + */ + @Override + public void checkClientTrusted(X509Certificate[] certs, String authType) + throws CertificateException { + if (!(isTrustAllHosts() || getTrustedHosts() != null)) + adapteeTrustManager.checkClientTrusted(certs, authType); + } + + /* (non-Javadoc) + * @see javax.net.ssl.X509TrustManager#checkServerTrusted( + * java.security.cert.X509Certificate[], java.lang.String) + */ + @Override + public void checkServerTrusted(X509Certificate[] certs, String authType) + throws CertificateException { + + if (!(isTrustAllHosts() || getTrustedHosts() != null)) + adapteeTrustManager.checkServerTrusted(certs, authType); + } + + /* (non-Javadoc) + * @see javax.net.ssl.X509TrustManager#getAcceptedIssuers() + */ + @Override + public X509Certificate[] getAcceptedIssuers() { + return adapteeTrustManager.getAcceptedIssuers(); + } + } +}
diff --git a/mail/src/main/java/com/sun/mail/util/MessageRemovedIOException.java b/mail/src/main/java/com/sun/mail/util/MessageRemovedIOException.java new file mode 100644 index 0000000..96840f7 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/util/MessageRemovedIOException.java
@@ -0,0 +1,49 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.IOException; + +/** + * A variant of MessageRemovedException that can be thrown from methods + * that only throw IOException. The getContent method will catch this + * exception and translate it back to MessageRemovedException. + * + * @see javax.mail.Message#isExpunged() + * @see javax.mail.Message#getMessageNumber() + * @author Bill Shannon + */ + +public class MessageRemovedIOException extends IOException { + + private static final long serialVersionUID = 4280468026581616424L; + + /** + * Constructs a MessageRemovedIOException with no detail message. + */ + public MessageRemovedIOException() { + super(); + } + + /** + * Constructs a MessageRemovedIOException with the specified detail message. + * @param s the detail message + */ + public MessageRemovedIOException(String s) { + super(s); + } +}
diff --git a/mail/src/main/java/com/sun/mail/util/MimeUtil.java b/mail/src/main/java/com/sun/mail/util/MimeUtil.java new file mode 100644 index 0000000..6944249 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/util/MimeUtil.java
@@ -0,0 +1,104 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.lang.reflect.*; +import java.security.*; + +import javax.mail.internet.MimePart; + +/** + * General MIME-related utility methods. + * + * @author Bill Shannon + * @since JavaMail 1.4.4 + */ +public class MimeUtil { + + private static final Method cleanContentType; + + static { + Method meth = null; + try { + String cth = System.getProperty("mail.mime.contenttypehandler"); + if (cth != null) { + ClassLoader cl = getContextClassLoader(); + Class<?> clsHandler = null; + if (cl != null) { + try { + clsHandler = Class.forName(cth, false, cl); + } catch (ClassNotFoundException cex) { } + } + if (clsHandler == null) + clsHandler = Class.forName(cth); + meth = clsHandler.getMethod("cleanContentType", + new Class<?>[] { MimePart.class, String.class }); + } + } catch (ClassNotFoundException ex) { + // ignore it + } catch (NoSuchMethodException ex) { + // ignore it + } catch (RuntimeException ex) { + // ignore it + } finally { + cleanContentType = meth; + } + } + + // No one should instantiate this class. + private MimeUtil() { + } + + /** + * If a Content-Type handler has been specified, + * call it to clean up the Content-Type value. + * + * @param mp the MimePart + * @param contentType the Content-Type value + * @return the cleaned Content-Type value + */ + public static String cleanContentType(MimePart mp, String contentType) { + if (cleanContentType != null) { + try { + return (String)cleanContentType.invoke(null, + new Object[] { mp, contentType }); + } catch (Exception ex) { + return contentType; + } + } else + return contentType; + } + + /** + * Convenience method to get our context class loader. + * Assert any privileges we might have and then call the + * Thread.getContextClassLoader method. + */ + private static ClassLoader getContextClassLoader() { + return + AccessController.doPrivileged(new PrivilegedAction<ClassLoader>() { + @Override + public ClassLoader run() { + ClassLoader cl = null; + try { + cl = Thread.currentThread().getContextClassLoader(); + } catch (SecurityException ex) { } + return cl; + } + }); + } +}
diff --git a/mail/src/main/java/com/sun/mail/util/PropUtil.java b/mail/src/main/java/com/sun/mail/util/PropUtil.java new file mode 100644 index 0000000..e1b793c --- /dev/null +++ b/mail/src/main/java/com/sun/mail/util/PropUtil.java
@@ -0,0 +1,169 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.util.*; +import javax.mail.Session; + +/** + * Utilities to make it easier to get property values. + * Properties can be strings or type-specific value objects. + * + * @author Bill Shannon + */ +public class PropUtil { + + // No one should instantiate this class. + private PropUtil() { + } + + /** + * Get an integer valued property. + * + * @param props the properties + * @param name the property name + * @param def default value if property not found + * @return the property value + */ + public static int getIntProperty(Properties props, String name, int def) { + return getInt(getProp(props, name), def); + } + + /** + * Get a boolean valued property. + * + * @param props the properties + * @param name the property name + * @param def default value if property not found + * @return the property value + */ + public static boolean getBooleanProperty(Properties props, + String name, boolean def) { + return getBoolean(getProp(props, name), def); + } + + /** + * Get an integer valued property. + * + * @param session the Session + * @param name the property name + * @param def default value if property not found + * @return the property value + */ + @Deprecated + public static int getIntSessionProperty(Session session, + String name, int def) { + return getInt(getProp(session.getProperties(), name), def); + } + + /** + * Get a boolean valued property. + * + * @param session the Session + * @param name the property name + * @param def default value if property not found + * @return the property value + */ + @Deprecated + public static boolean getBooleanSessionProperty(Session session, + String name, boolean def) { + return getBoolean(getProp(session.getProperties(), name), def); + } + + /** + * Get a boolean valued System property. + * + * @param name the property name + * @param def default value if property not found + * @return the property value + */ + public static boolean getBooleanSystemProperty(String name, boolean def) { + try { + return getBoolean(getProp(System.getProperties(), name), def); + } catch (SecurityException sex) { + // fall through... + } + + /* + * If we can't get the entire System Properties object because + * of a SecurityException, just ask for the specific property. + */ + try { + String value = System.getProperty(name); + if (value == null) + return def; + if (def) + return !value.equalsIgnoreCase("false"); + else + return value.equalsIgnoreCase("true"); + } catch (SecurityException sex) { + return def; + } + } + + /** + * Get the value of the specified property. + * If the "get" method returns null, use the getProperty method, + * which might cascade to a default Properties object. + */ + private static Object getProp(Properties props, String name) { + Object val = props.get(name); + if (val != null) + return val; + else + return props.getProperty(name); + } + + /** + * Interpret the value object as an integer, + * returning def if unable. + */ + private static int getInt(Object value, int def) { + if (value == null) + return def; + if (value instanceof String) { + try { + return Integer.parseInt((String)value); + } catch (NumberFormatException nfex) { } + } + if (value instanceof Integer) + return ((Integer)value).intValue(); + return def; + } + + /** + * Interpret the value object as a boolean, + * returning def if unable. + */ + private static boolean getBoolean(Object value, boolean def) { + if (value == null) + return def; + if (value instanceof String) { + /* + * If the default is true, only "false" turns it off. + * If the default is false, only "true" turns it on. + */ + if (def) + return !((String)value).equalsIgnoreCase("false"); + else + return ((String)value).equalsIgnoreCase("true"); + } + if (value instanceof Boolean) + return ((Boolean)value).booleanValue(); + return def; + } +}
diff --git a/mail/src/main/java/com/sun/mail/util/QDecoderStream.java b/mail/src/main/java/com/sun/mail/util/QDecoderStream.java new file mode 100644 index 0000000..19ea0c6 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/util/QDecoderStream.java
@@ -0,0 +1,70 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.*; + +/** + * This class implements a Q Decoder as defined in RFC 2047 + * for decoding MIME headers. It subclasses the QPDecoderStream class. + * + * @author John Mani + */ + +public class QDecoderStream extends QPDecoderStream { + + /** + * Create a Q-decoder that decodes the specified input stream. + * @param in the input stream + */ + public QDecoderStream(InputStream in) { + super(in); + } + + /** + * Read the next decoded byte from this input stream. The byte + * is returned as an <code>int</code> in the range <code>0</code> + * to <code>255</code>. If no byte is available because the end of + * the stream has been reached, the value <code>-1</code> is returned. + * This method blocks until input data is available, the end of the + * stream is detected, or an exception is thrown. + * + * @return the next byte of data, or <code>-1</code> if the end of the + * stream is reached. + * @exception IOException if an I/O error occurs. + */ + @Override + public int read() throws IOException { + int c = in.read(); + + if (c == '_') // Return '_' as ' ' + return ' '; + else if (c == '=') { + // QP Encoded atom. Get the next two bytes .. + ba[0] = (byte)in.read(); + ba[1] = (byte)in.read(); + // .. and decode them + try { + return ASCIIUtility.parseInt(ba, 0, 2, 16); + } catch (NumberFormatException nex) { + throw new DecodingException( + "QDecoder: Error in QP stream " + nex.getMessage()); + } + } else + return c; + } +}
diff --git a/mail/src/main/java/com/sun/mail/util/QEncoderStream.java b/mail/src/main/java/com/sun/mail/util/QEncoderStream.java new file mode 100644 index 0000000..659897a --- /dev/null +++ b/mail/src/main/java/com/sun/mail/util/QEncoderStream.java
@@ -0,0 +1,100 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.*; + +/** + * This class implements a Q Encoder as defined by RFC 2047 for + * encoding MIME headers. It subclasses the QPEncoderStream class. + * + * @author John Mani + */ + +public class QEncoderStream extends QPEncoderStream { + + private String specials; + private static String WORD_SPECIALS = "=_?\"#$%&'(),.:;<>@[\\]^`{|}~"; + private static String TEXT_SPECIALS = "=_?"; + + /** + * Create a Q encoder that encodes the specified input stream + * @param out the output stream + * @param encodingWord true if we are Q-encoding a word within a + * phrase. + */ + public QEncoderStream(OutputStream out, boolean encodingWord) { + super(out, Integer.MAX_VALUE); // MAX_VALUE is 2^31, should + // suffice (!) to indicate that + // CRLFs should not be inserted + // when encoding rfc822 headers + + // a RFC822 "word" token has more restrictions than a + // RFC822 "text" token. + specials = encodingWord ? WORD_SPECIALS : TEXT_SPECIALS; + } + + /** + * Encodes the specified <code>byte</code> to this output stream. + * @param c the <code>byte</code>. + * @exception IOException if an I/O error occurs. + */ + @Override + public void write(int c) throws IOException { + c = c & 0xff; // Turn off the MSB. + if (c == ' ') + output('_', false); + else if (c < 040 || c >= 0177 || specials.indexOf(c) >= 0) + // Encoding required. + output(c, true); + else // No encoding required + output(c, false); + } + + /** + * Returns the length of the encoded version of this byte array. + * + * @param b the byte array + * @param encodingWord true if encoding words, false if encoding text + * @return the length + */ + public static int encodedLength(byte[] b, boolean encodingWord) { + int len = 0; + String specials = encodingWord ? WORD_SPECIALS: TEXT_SPECIALS; + for (int i = 0; i < b.length; i++) { + int c = b[i] & 0xff; // Mask off MSB + if (c < 040 || c >= 0177 || specials.indexOf(c) >= 0) + // needs encoding + len += 3; // Q-encoding is 1 -> 3 conversion + else + len++; + } + return len; + } + + /**** begin TEST program *** + public static void main(String argv[]) throws Exception { + FileInputStream infile = new FileInputStream(argv[0]); + QEncoderStream encoder = new QEncoderStream(System.out); + int c; + + while ((c = infile.read()) != -1) + encoder.write(c); + encoder.close(); + } + *** end TEST program ***/ +}
diff --git a/mail/src/main/java/com/sun/mail/util/QPDecoderStream.java b/mail/src/main/java/com/sun/mail/util/QPDecoderStream.java new file mode 100644 index 0000000..284c614 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/util/QPDecoderStream.java
@@ -0,0 +1,198 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.*; + +/** + * This class implements a QP Decoder. It is implemented as + * a FilterInputStream, so one can just wrap this class around + * any input stream and read bytes from this filter. The decoding + * is done as the bytes are read out. + * + * @author John Mani + */ + +public class QPDecoderStream extends FilterInputStream { + protected byte[] ba = new byte[2]; + protected int spaces = 0; + + /** + * Create a Quoted Printable decoder that decodes the specified + * input stream. + * @param in the input stream + */ + public QPDecoderStream(InputStream in) { + super(new PushbackInputStream(in, 2)); // pushback of size=2 + } + + /** + * Read the next decoded byte from this input stream. The byte + * is returned as an <code>int</code> in the range <code>0</code> + * to <code>255</code>. If no byte is available because the end of + * the stream has been reached, the value <code>-1</code> is returned. + * This method blocks until input data is available, the end of the + * stream is detected, or an exception is thrown. + * + * @return the next byte of data, or <code>-1</code> if the end of the + * stream is reached. + * @exception IOException if an I/O error occurs. + */ + @Override + public int read() throws IOException { + if (spaces > 0) { + // We have cached space characters, return one + spaces--; + return ' '; + } + + int c = in.read(); + + if (c == ' ') { + // Got space, keep reading till we get a non-space char + while ((c = in.read()) == ' ') + spaces++; + + if (c == '\r' || c == '\n' || c == -1) + // If the non-space char is CR/LF/EOF, the spaces we got + // so far is junk introduced during transport. Junk 'em. + spaces = 0; + else { + // The non-space char is NOT CR/LF, the spaces are valid. + ((PushbackInputStream)in).unread(c); + c = ' '; + } + return c; // return either <SPACE> or <CR/LF> + } + else if (c == '=') { + // QP Encoded atom. Decode the next two bytes + int a = in.read(); + + if (a == '\n') { + /* Hmm ... not really confirming QP encoding, but lets + * allow this as a LF terminated encoded line .. and + * consider this a soft linebreak and recurse to fetch + * the next char. + */ + return read(); + } else if (a == '\r') { + // Expecting LF. This forms a soft linebreak to be ignored. + int b = in.read(); + if (b != '\n') + /* Not really confirming QP encoding, but + * lets allow this as well. + */ + ((PushbackInputStream)in).unread(b); + return read(); + } else if (a == -1) { + // Not valid QP encoding, but we be nice and tolerant here ! + return -1; + } else { + ba[0] = (byte)a; + ba[1] = (byte)in.read(); + try { + return ASCIIUtility.parseInt(ba, 0, 2, 16); + } catch (NumberFormatException nex) { + /* + System.err.println( + "Illegal characters in QP encoded stream: " + + ASCIIUtility.toString(ba, 0, 2) + ); + */ + + ((PushbackInputStream)in).unread(ba); + return c; + } + } + } + return c; + } + + /** + * Reads up to <code>len</code> decoded bytes of data from this input stream + * into an array of bytes. This method blocks until some input is + * available. + * <p> + * + * @param buf the buffer into which the data is read. + * @param off the start offset of the data. + * @param len the maximum number of bytes read. + * @return the total number of bytes read into the buffer, or + * <code>-1</code> if there is no more data because the end of + * the stream has been reached. + * @exception IOException if an I/O error occurs. + */ + @Override + public int read(byte[] buf, int off, int len) throws IOException { + int i, c; + for (i = 0; i < len; i++) { + if ((c = read()) == -1) { + if (i == 0) // At end of stream, so we should + i = -1; // return -1 , NOT 0. + break; + } + buf[off+i] = (byte)c; + } + return i; + } + + /** + * Skips over and discards n bytes of data from this stream. + */ + @Override + public long skip(long n) throws IOException { + long skipped = 0; + while (n-- > 0 && read() >= 0) + skipped++; + return skipped; + } + + /** + * Tests if this input stream supports marks. Currently this class + * does not support marks + */ + @Override + public boolean markSupported() { + return false; + } + + /** + * Returns the number of bytes that can be read from this input + * stream without blocking. The QP algorithm does not permit + * a priori knowledge of the number of bytes after decoding, so + * this method just invokes the <code>available</code> method + * of the original input stream. + */ + @Override + public int available() throws IOException { + // This is bogus ! We don't really know how much + // bytes are available *after* decoding + return in.available(); + } + + /**** begin TEST program + public static void main(String argv[]) throws Exception { + FileInputStream infile = new FileInputStream(argv[0]); + QPDecoderStream decoder = new QPDecoderStream(infile); + int c; + + while ((c = decoder.read()) != -1) + System.out.print((char)c); + System.out.println(); + } + *** end TEST program ****/ +}
diff --git a/mail/src/main/java/com/sun/mail/util/QPEncoderStream.java b/mail/src/main/java/com/sun/mail/util/QPEncoderStream.java new file mode 100644 index 0000000..d39102d --- /dev/null +++ b/mail/src/main/java/com/sun/mail/util/QPEncoderStream.java
@@ -0,0 +1,196 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.*; + +/** + * This class implements a Quoted Printable Encoder. It is implemented as + * a FilterOutputStream, so one can just wrap this class around + * any output stream and write bytes into this filter. The Encoding + * is done as the bytes are written out. + * + * @author John Mani + */ + +public class QPEncoderStream extends FilterOutputStream { + private int count = 0; // number of bytes that have been output + private int bytesPerLine; // number of bytes per line + private boolean gotSpace = false; + private boolean gotCR = false; + + /** + * Create a QP encoder that encodes the specified input stream + * @param out the output stream + * @param bytesPerLine the number of bytes per line. The encoder + * inserts a CRLF sequence after this many number + * of bytes. + */ + public QPEncoderStream(OutputStream out, int bytesPerLine) { + super(out); + // Subtract 1 to account for the '=' in the soft-return + // at the end of a line + this.bytesPerLine = bytesPerLine - 1; + } + + /** + * Create a QP encoder that encodes the specified input stream. + * Inserts the CRLF sequence after outputting 76 bytes. + * @param out the output stream + */ + public QPEncoderStream(OutputStream out) { + this(out, 76); + } + + /** + * Encodes <code>len</code> bytes from the specified + * <code>byte</code> array starting at offset <code>off</code> to + * this output stream. + * + * @param b the data. + * @param off the start offset in the data. + * @param len the number of bytes to write. + * @exception IOException if an I/O error occurs. + */ + @Override + public void write(byte[] b, int off, int len) throws IOException { + for (int i = 0; i < len; i++) + write(b[off + i]); + } + + /** + * Encodes <code>b.length</code> bytes to this output stream. + * @param b the data to be written. + * @exception IOException if an I/O error occurs. + */ + @Override + public void write(byte[] b) throws IOException { + write(b, 0, b.length); + } + + /** + * Encodes the specified <code>byte</code> to this output stream. + * @param c the <code>byte</code>. + * @exception IOException if an I/O error occurs. + */ + @Override + public void write(int c) throws IOException { + c = c & 0xff; // Turn off the MSB. + if (gotSpace) { // previous character was <SPACE> + if (c == '\r' || c == '\n') + // if CR/LF, we need to encode the <SPACE> char + output(' ', true); + else // no encoding required, just output the char + output(' ', false); + gotSpace = false; + } + + if (c == '\r') { + gotCR = true; + outputCRLF(); + } else { + if (c == '\n') { + if (gotCR) + // This is a CRLF sequence, we already output the + // corresponding CRLF when we got the CR, so ignore this + ; + else + outputCRLF(); + } else if (c == ' ') { + gotSpace = true; + } else if (c < 040 || c >= 0177 || c == '=') + // Encoding required. + output(c, true); + else // No encoding required + output(c, false); + // whatever it was, it wasn't a CR + gotCR = false; + } + } + + /** + * Flushes this output stream and forces any buffered output bytes + * to be encoded out to the stream. + * @exception IOException if an I/O error occurs. + */ + @Override + public void flush() throws IOException { + if (gotSpace) { + output(' ', true); + gotSpace = false; + } + out.flush(); + } + + /** + * Forces any buffered output bytes to be encoded out to the stream + * and closes this output stream. + * + * @exception IOException for I/O errors + */ + @Override + public void close() throws IOException { + flush(); + out.close(); + } + + private void outputCRLF() throws IOException { + out.write('\r'); + out.write('\n'); + count = 0; + } + + // The encoding table + private final static char hex[] = { + '0','1', '2', '3', '4', '5', '6', '7', + '8','9', 'A', 'B', 'C', 'D', 'E', 'F' + }; + + protected void output(int c, boolean encode) throws IOException { + if (encode) { + if ((count += 3) > bytesPerLine) { + out.write('='); + out.write('\r'); + out.write('\n'); + count = 3; // set the next line's length + } + out.write('='); + out.write(hex[c >> 4]); + out.write(hex[c & 0xf]); + } else { + if (++count > bytesPerLine) { + out.write('='); + out.write('\r'); + out.write('\n'); + count = 1; // set the next line's length + } + out.write(c); + } + } + + /**** begin TEST program *** + public static void main(String argv[]) throws Exception { + FileInputStream infile = new FileInputStream(argv[0]); + QPEncoderStream encoder = new QPEncoderStream(System.out); + int c; + + while ((c = infile.read()) != -1) + encoder.write(c); + encoder.close(); + } + *** end TEST program ***/ +}
diff --git a/mail/src/main/java/com/sun/mail/util/ReadableMime.java b/mail/src/main/java/com/sun/mail/util/ReadableMime.java new file mode 100644 index 0000000..ecba7e2 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/util/ReadableMime.java
@@ -0,0 +1,40 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.InputStream; + +import javax.mail.MessagingException; + +/** + * A Message or message Part whose data can be read as a MIME format + * stream. Note that the MIME stream will include both the headers + * and the body of the message or part. This should be the same data + * that is produced by the writeTo method, but in a readable form. + * + * @author Bill Shannon + * @since JavaMail 1.4.5 + */ +public interface ReadableMime { + /** + * Return the MIME format stream corresponding to this message part. + * + * @return the MIME format stream + * @exception MessagingException for failures + */ + public InputStream getMimeStream() throws MessagingException; +}
diff --git a/mail/src/main/java/com/sun/mail/util/SharedByteArrayOutputStream.java b/mail/src/main/java/com/sun/mail/util/SharedByteArrayOutputStream.java new file mode 100644 index 0000000..9fd617e --- /dev/null +++ b/mail/src/main/java/com/sun/mail/util/SharedByteArrayOutputStream.java
@@ -0,0 +1,40 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.InputStream; +import java.io.ByteArrayOutputStream; + +import javax.mail.util.SharedByteArrayInputStream; + +/** + * A ByteArrayOutputStream that allows us to share the byte array + * rather than copy it. Eventually could replace this with something + * that doesn't require a single contiguous byte array. + * + * @author Bill Shannon + * @since JavaMail 1.4.5 + */ +public class SharedByteArrayOutputStream extends ByteArrayOutputStream { + public SharedByteArrayOutputStream(int size) { + super(size); + } + + public InputStream toStream() { + return new SharedByteArrayInputStream(buf, 0, count); + } +}
diff --git a/mail/src/main/java/com/sun/mail/util/SocketConnectException.java b/mail/src/main/java/com/sun/mail/util/SocketConnectException.java new file mode 100644 index 0000000..6b3c16e --- /dev/null +++ b/mail/src/main/java/com/sun/mail/util/SocketConnectException.java
@@ -0,0 +1,105 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.IOException; + +/** + * An IOException that indicates a socket connection attempt failed. + * Unlike java.net.ConnectException, it includes details of what we + * were trying to connect to. + * + * @see java.net.ConnectException + * @author Bill Shannon + * @since JavaMail 1.5.0 + */ + +public class SocketConnectException extends IOException { + /** + * The socket host name. + */ + private String host; + /** + * The socket port. + */ + private int port; + /** + * The connection timeout. + */ + private int cto; + /** + * The generated serial id. + */ + private static final long serialVersionUID = 3997871560538755463L; + + /** + * Constructs a SocketConnectException. + * + * @param msg error message detail + * @param cause the underlying exception that indicates the failure + * @param host the host we were trying to connect to + * @param port the port we were trying to connect to + * @param cto the timeout for the connection attempt + */ + public SocketConnectException(String msg, Exception cause, + String host, int port, int cto) { + super(msg); + initCause(cause); + this.host = host; + this.port = port; + this.cto = cto; + } + + /** + * The exception that caused the failure. + * + * @return the exception + */ + public Exception getException() { + // the "cause" is always an Exception; see constructor above + Throwable t = getCause(); + assert t == null || t instanceof Exception; + return (Exception) t; + } + + /** + * The host we were trying to connect to. + * + * @return the host + */ + public String getHost() { + return host; + } + + /** + * The port we were trying to connect to. + * + * @return the port + */ + public int getPort() { + return port; + } + + /** + * The timeout used for the connection attempt. + * + * @return the connection timeout + */ + public int getConnectionTimeout() { + return cto; + } +}
diff --git a/mail/src/main/java/com/sun/mail/util/SocketFetcher.java b/mail/src/main/java/com/sun/mail/util/SocketFetcher.java new file mode 100644 index 0000000..d6af220 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/util/SocketFetcher.java
@@ -0,0 +1,895 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.security.*; +import java.net.*; +import java.io.*; +import java.nio.channels.SocketChannel; +import java.nio.charset.StandardCharsets; +import java.lang.reflect.*; +import java.util.*; +import java.util.regex.*; +import java.util.logging.Level; +import java.security.cert.*; +import javax.net.*; +import javax.net.ssl.*; + +/** + * This class is used to get Sockets. Depending on the arguments passed + * it will either return a plain java.net.Socket or dynamically load + * the SocketFactory class specified in the classname param and return + * a socket created by that SocketFactory. + * + * @author Max Spivak + * @author Bill Shannon + */ +public class SocketFetcher { + + private static MailLogger logger = new MailLogger( + SocketFetcher.class, + "socket", + "DEBUG SocketFetcher", + PropUtil.getBooleanSystemProperty("mail.socket.debug", false), + System.out); + + // No one should instantiate this class. + private SocketFetcher() { + } + + /** + * This method returns a Socket. Properties control the use of + * socket factories and other socket characteristics. The properties + * used are: + * <ul> + * <li> <i>prefix</i>.socketFactory + * <li> <i>prefix</i>.socketFactory.class + * <li> <i>prefix</i>.socketFactory.fallback + * <li> <i>prefix</i>.socketFactory.port + * <li> <i>prefix</i>.ssl.socketFactory + * <li> <i>prefix</i>.ssl.socketFactory.class + * <li> <i>prefix</i>.ssl.socketFactory.port + * <li> <i>prefix</i>.timeout + * <li> <i>prefix</i>.connectiontimeout + * <li> <i>prefix</i>.localaddress + * <li> <i>prefix</i>.localport + * <li> <i>prefix</i>.usesocketchannels + * </ul> <p> + * If we're making an SSL connection, the ssl.socketFactory + * properties are used first, if set. <p> + * + * If the socketFactory property is set, the value is an + * instance of a SocketFactory class, not a string. The + * instance is used directly. If the socketFactory property + * is not set, the socketFactory.class property is considered. + * (Note that the SocketFactory property must be set using the + * <code>put</code> method, not the <code>setProperty</code> + * method.) <p> + * + * If the socketFactory.class property isn't set, the socket + * returned is an instance of java.net.Socket connected to the + * given host and port. If the socketFactory.class property is set, + * it is expected to contain a fully qualified classname of a + * javax.net.SocketFactory subclass. In this case, the class is + * dynamically instantiated and a socket created by that + * SocketFactory is returned. <p> + * + * If the socketFactory.fallback property is set to false, don't + * fall back to using regular sockets if the socket factory fails. <p> + * + * The socketFactory.port specifies a port to use when connecting + * through the socket factory. If unset, the port argument will be + * used. <p> + * + * If the connectiontimeout property is set, the timeout is passed + * to the socket connect method. <p> + * + * If the timeout property is set, it is used to set the socket timeout. + * <p> + * + * If the localaddress property is set, it's used as the local address + * to bind to. If the localport property is also set, it's used as the + * local port number to bind to. <p> + * + * If the usesocketchannels property is set, and we create the Socket + * ourself, and the selection of other properties allows, create a + * SocketChannel and get the Socket from it. This allows us to later + * retrieve the SocketChannel from the Socket and use it with Select. + * + * @param host The host to connect to + * @param port The port to connect to at the host + * @param props Properties object containing socket properties + * @param prefix Property name prefix, e.g., "mail.imap" + * @param useSSL use the SSL socket factory as the default + * @return the Socket + * @exception IOException for I/O errors + */ + public static Socket getSocket(String host, int port, Properties props, + String prefix, boolean useSSL) + throws IOException { + + if (logger.isLoggable(Level.FINER)) + logger.finer("getSocket" + ", host " + host + ", port " + port + + ", prefix " + prefix + ", useSSL " + useSSL); + if (prefix == null) + prefix = "socket"; + if (props == null) + props = new Properties(); // empty + int cto = PropUtil.getIntProperty(props, + prefix + ".connectiontimeout", -1); + Socket socket = null; + String localaddrstr = props.getProperty(prefix + ".localaddress", null); + InetAddress localaddr = null; + if (localaddrstr != null) + localaddr = InetAddress.getByName(localaddrstr); + int localport = PropUtil.getIntProperty(props, + prefix + ".localport", 0); + + boolean fb = PropUtil.getBooleanProperty(props, + prefix + ".socketFactory.fallback", true); + + int sfPort = -1; + String sfErr = "unknown socket factory"; + int to = PropUtil.getIntProperty(props, prefix + ".timeout", -1); + try { + /* + * If using SSL, first look for SSL-specific class name or + * factory instance. + */ + SocketFactory sf = null; + String sfPortName = null; + if (useSSL) { + Object sfo = props.get(prefix + ".ssl.socketFactory"); + if (sfo instanceof SocketFactory) { + sf = (SocketFactory)sfo; + sfErr = "SSL socket factory instance " + sf; + } + if (sf == null) { + String sfClass = + props.getProperty(prefix + ".ssl.socketFactory.class"); + sf = getSocketFactory(sfClass); + sfErr = "SSL socket factory class " + sfClass; + } + sfPortName = ".ssl.socketFactory.port"; + } + + if (sf == null) { + Object sfo = props.get(prefix + ".socketFactory"); + if (sfo instanceof SocketFactory) { + sf = (SocketFactory)sfo; + sfErr = "socket factory instance " + sf; + } + if (sf == null) { + String sfClass = + props.getProperty(prefix + ".socketFactory.class"); + sf = getSocketFactory(sfClass); + sfErr = "socket factory class " + sfClass; + } + sfPortName = ".socketFactory.port"; + } + + // if we now have a socket factory, use it + if (sf != null) { + sfPort = PropUtil.getIntProperty(props, + prefix + sfPortName, -1); + + // if port passed in via property isn't valid, use param + if (sfPort == -1) + sfPort = port; + socket = createSocket(localaddr, localport, + host, sfPort, cto, to, props, prefix, sf, useSSL); + } + } catch (SocketTimeoutException sex) { + throw sex; + } catch (Exception ex) { + if (!fb) { + if (ex instanceof InvocationTargetException) { + Throwable t = + ((InvocationTargetException)ex).getTargetException(); + if (t instanceof Exception) + ex = (Exception)t; + } + if (ex instanceof IOException) + throw (IOException)ex; + throw new SocketConnectException("Using " + sfErr, ex, + host, sfPort, cto); + } + } + + if (socket == null) { + socket = createSocket(localaddr, localport, + host, port, cto, to, props, prefix, null, useSSL); + + } else { + if (to >= 0) { + if (logger.isLoggable(Level.FINEST)) + logger.finest("set socket read timeout " + to); + socket.setSoTimeout(to); + } + } + + return socket; + } + + public static Socket getSocket(String host, int port, Properties props, + String prefix) throws IOException { + return getSocket(host, port, props, prefix, false); + } + + /** + * Create a socket with the given local address and connected to + * the given host and port. Use the specified connection timeout + * and read timeout. + * If a socket factory is specified, use it. Otherwise, use the + * SSLSocketFactory if useSSL is true. + */ + private static Socket createSocket(InetAddress localaddr, int localport, + String host, int port, int cto, int to, + Properties props, String prefix, + SocketFactory sf, boolean useSSL) + throws IOException { + Socket socket = null; + + if (logger.isLoggable(Level.FINEST)) + logger.finest("create socket: prefix " + prefix + + ", localaddr " + localaddr + ", localport " + localport + + ", host " + host + ", port " + port + + ", connection timeout " + cto + ", timeout " + to + + ", socket factory " + sf + ", useSSL " + useSSL); + + String proxyHost = props.getProperty(prefix + ".proxy.host", null); + String proxyUser = props.getProperty(prefix + ".proxy.user", null); + String proxyPassword = props.getProperty(prefix + ".proxy.password", null); + int proxyPort = 80; + String socksHost = null; + int socksPort = 1080; + String err = null; + + if (proxyHost != null) { + int i = proxyHost.indexOf(':'); + if (i >= 0) { + try { + proxyPort = Integer.parseInt(proxyHost.substring(i + 1)); + } catch (NumberFormatException ex) { + // ignore it + } + proxyHost = proxyHost.substring(0, i); + } + proxyPort = PropUtil.getIntProperty(props, + prefix + ".proxy.port", proxyPort); + err = "Using web proxy host, port: " + proxyHost + ", " + proxyPort; + if (logger.isLoggable(Level.FINER)) { + logger.finer("web proxy host " + proxyHost + ", port " + proxyPort); + if (proxyUser != null) + logger.finer("web proxy user " + proxyUser + ", password " + + (proxyPassword == null ? "<null>" : "<non-null>")); + } + } else if ((socksHost = + props.getProperty(prefix + ".socks.host", null)) != null) { + int i = socksHost.indexOf(':'); + if (i >= 0) { + try { + socksPort = Integer.parseInt(socksHost.substring(i + 1)); + } catch (NumberFormatException ex) { + // ignore it + } + socksHost = socksHost.substring(0, i); + } + socksPort = PropUtil.getIntProperty(props, + prefix + ".socks.port", socksPort); + err = "Using SOCKS host, port: " + socksHost + ", " + socksPort; + if (logger.isLoggable(Level.FINER)) + logger.finer("socks host " + socksHost + ", port " + socksPort); + } + + if (sf != null && !(sf instanceof SSLSocketFactory)) + socket = sf.createSocket(); + if (socket == null) { + if (socksHost != null) { + socket = new Socket( + new java.net.Proxy(java.net.Proxy.Type.SOCKS, + new InetSocketAddress(socksHost, socksPort))); + } else if (PropUtil.getBooleanProperty(props, + prefix + ".usesocketchannels", false)) { + logger.finer("using SocketChannels"); + socket = SocketChannel.open().socket(); + } else + socket = new Socket(); + } + if (to >= 0) { + if (logger.isLoggable(Level.FINEST)) + logger.finest("set socket read timeout " + to); + socket.setSoTimeout(to); + } + int writeTimeout = PropUtil.getIntProperty(props, + prefix + ".writetimeout", -1); + if (writeTimeout != -1) { // wrap original + if (logger.isLoggable(Level.FINEST)) + logger.finest("set socket write timeout " + writeTimeout); + socket = new WriteTimeoutSocket(socket, writeTimeout); + } + if (localaddr != null) + socket.bind(new InetSocketAddress(localaddr, localport)); + try { + logger.finest("connecting..."); + if (proxyHost != null) + proxyConnect(socket, proxyHost, proxyPort, + proxyUser, proxyPassword, host, port, cto); + else if (cto >= 0) + socket.connect(new InetSocketAddress(host, port), cto); + else + socket.connect(new InetSocketAddress(host, port)); + logger.finest("success!"); + } catch (IOException ex) { + logger.log(Level.FINEST, "connection failed", ex); + throw new SocketConnectException(err, ex, host, port, cto); + } + + /* + * If we want an SSL connection and we didn't get an SSLSocket, + * wrap our plain Socket with an SSLSocket. + */ + if ((useSSL || sf instanceof SSLSocketFactory) && + !(socket instanceof SSLSocket)) { + String trusted; + SSLSocketFactory ssf; + if ((trusted = props.getProperty(prefix + ".ssl.trust")) != null) { + try { + MailSSLSocketFactory msf = new MailSSLSocketFactory(); + if (trusted.equals("*")) + msf.setTrustAllHosts(true); + else + msf.setTrustedHosts(trusted.split("\\s+")); + ssf = msf; + } catch (GeneralSecurityException gex) { + IOException ioex = new IOException( + "Can't create MailSSLSocketFactory"); + ioex.initCause(gex); + throw ioex; + } + } else if (sf instanceof SSLSocketFactory) + ssf = (SSLSocketFactory)sf; + else + ssf = (SSLSocketFactory)SSLSocketFactory.getDefault(); + socket = ssf.createSocket(socket, host, port, true); + sf = ssf; + } + + /* + * No matter how we created the socket, if it turns out to be an + * SSLSocket, configure it. + */ + configureSSLSocket(socket, host, props, prefix, sf); + + return socket; + } + + /** + * Return a socket factory of the specified class. + */ + private static SocketFactory getSocketFactory(String sfClass) + throws ClassNotFoundException, + NoSuchMethodException, + IllegalAccessException, + InvocationTargetException { + if (sfClass == null || sfClass.length() == 0) + return null; + + // dynamically load the class + + ClassLoader cl = getContextClassLoader(); + Class<?> clsSockFact = null; + if (cl != null) { + try { + clsSockFact = Class.forName(sfClass, false, cl); + } catch (ClassNotFoundException cex) { } + } + if (clsSockFact == null) + clsSockFact = Class.forName(sfClass); + // get & invoke the getDefault() method + Method mthGetDefault = clsSockFact.getMethod("getDefault", + new Class<?>[]{}); + SocketFactory sf = (SocketFactory) + mthGetDefault.invoke(new Object(), new Object[]{}); + return sf; + } + + /** + * Start TLS on an existing socket. + * Supports the "STARTTLS" command in many protocols. + * This version for compatibility with possible third party code + * that might've used this API even though it shouldn't. + * + * @param socket the existing socket + * @return the wrapped Socket + * @exception IOException for I/O errors + * @deprecated + */ + @Deprecated + public static Socket startTLS(Socket socket) throws IOException { + return startTLS(socket, new Properties(), "socket"); + } + + /** + * Start TLS on an existing socket. + * Supports the "STARTTLS" command in many protocols. + * This version for compatibility with possible third party code + * that might've used this API even though it shouldn't. + * + * @param socket the existing socket + * @param props the properties + * @param prefix the property prefix + * @return the wrapped Socket + * @exception IOException for I/O errors + * @deprecated + */ + @Deprecated + public static Socket startTLS(Socket socket, Properties props, + String prefix) throws IOException { + InetAddress a = socket.getInetAddress(); + String host = a.getHostName(); + return startTLS(socket, host, props, prefix); + } + + /** + * Start TLS on an existing socket. + * Supports the "STARTTLS" command in many protocols. + * + * @param socket the existing socket + * @param host the host the socket is connected to + * @param props the properties + * @param prefix the property prefix + * @return the wrapped Socket + * @exception IOException for I/O errors + */ + public static Socket startTLS(Socket socket, String host, Properties props, + String prefix) throws IOException { + int port = socket.getPort(); + if (logger.isLoggable(Level.FINER)) + logger.finer("startTLS host " + host + ", port " + port); + + String sfErr = "unknown socket factory"; + try { + SSLSocketFactory ssf = null; + SocketFactory sf = null; + + // first, look for an SSL socket factory + Object sfo = props.get(prefix + ".ssl.socketFactory"); + if (sfo instanceof SocketFactory) { + sf = (SocketFactory)sfo; + sfErr = "SSL socket factory instance " + sf; + } + if (sf == null) { + String sfClass = + props.getProperty(prefix + ".ssl.socketFactory.class"); + sf = getSocketFactory(sfClass); + sfErr = "SSL socket factory class " + sfClass; + } + if (sf != null && sf instanceof SSLSocketFactory) + ssf = (SSLSocketFactory)sf; + + // next, look for a regular socket factory that happens to be + // an SSL socket factory + if (ssf == null) { + sfo = props.get(prefix + ".socketFactory"); + if (sfo instanceof SocketFactory) { + sf = (SocketFactory)sfo; + sfErr = "socket factory instance " + sf; + } + if (sf == null) { + String sfClass = + props.getProperty(prefix + ".socketFactory.class"); + sf = getSocketFactory(sfClass); + sfErr = "socket factory class " + sfClass; + } + if (sf != null && sf instanceof SSLSocketFactory) + ssf = (SSLSocketFactory)sf; + } + + // finally, use the default SSL socket factory + if (ssf == null) { + String trusted; + if ((trusted = props.getProperty(prefix + ".ssl.trust")) != + null) { + try { + MailSSLSocketFactory msf = new MailSSLSocketFactory(); + if (trusted.equals("*")) + msf.setTrustAllHosts(true); + else + msf.setTrustedHosts(trusted.split("\\s+")); + ssf = msf; + sfErr = "mail SSL socket factory"; + } catch (GeneralSecurityException gex) { + IOException ioex = new IOException( + "Can't create MailSSLSocketFactory"); + ioex.initCause(gex); + throw ioex; + } + } else { + ssf = (SSLSocketFactory)SSLSocketFactory.getDefault(); + sfErr = "default SSL socket factory"; + } + } + + socket = ssf.createSocket(socket, host, port, true); + configureSSLSocket(socket, host, props, prefix, ssf); + } catch (Exception ex) { + if (ex instanceof InvocationTargetException) { + Throwable t = + ((InvocationTargetException)ex).getTargetException(); + if (t instanceof Exception) + ex = (Exception)t; + } + if (ex instanceof IOException) + throw (IOException)ex; + // wrap anything else before sending it on + IOException ioex = new IOException( + "Exception in startTLS using " + sfErr + + ": host, port: " + + host + ", " + port + + "; Exception: " + ex); + ioex.initCause(ex); + throw ioex; + } + return socket; + } + + /** + * Configure the SSL options for the socket (if it's an SSL socket), + * based on the mail.<protocol>.ssl.protocols and + * mail.<protocol>.ssl.ciphersuites properties. + * Check the identity of the server as specified by the + * mail.<protocol>.ssl.checkserveridentity property. + */ + private static void configureSSLSocket(Socket socket, String host, + Properties props, String prefix, SocketFactory sf) + throws IOException { + if (!(socket instanceof SSLSocket)) + return; + SSLSocket sslsocket = (SSLSocket)socket; + + String protocols = props.getProperty(prefix + ".ssl.protocols", null); + if (protocols != null) + sslsocket.setEnabledProtocols(stringArray(protocols)); + else { + /* + * The UW IMAP server insists on at least the TLSv1 + * protocol for STARTTLS, and won't accept the old SSLv2 + * or SSLv3 protocols. Here we enable only the non-SSL + * protocols. XXX - this should probably be parameterized. + */ + String[] prots = sslsocket.getEnabledProtocols(); + if (logger.isLoggable(Level.FINER)) + logger.finer("SSL enabled protocols before " + + Arrays.asList(prots)); + List<String> eprots = new ArrayList<>(); + for (int i = 0; i < prots.length; i++) { + if (prots[i] != null && !prots[i].startsWith("SSL")) + eprots.add(prots[i]); + } + sslsocket.setEnabledProtocols( + eprots.toArray(new String[eprots.size()])); + } + String ciphers = props.getProperty(prefix + ".ssl.ciphersuites", null); + if (ciphers != null) + sslsocket.setEnabledCipherSuites(stringArray(ciphers)); + if (logger.isLoggable(Level.FINER)) { + logger.finer("SSL enabled protocols after " + + Arrays.asList(sslsocket.getEnabledProtocols())); + logger.finer("SSL enabled ciphers after " + + Arrays.asList(sslsocket.getEnabledCipherSuites())); + } + + /* + * Force the handshake to be done now so that we can report any + * errors (e.g., certificate errors) to the caller of the startTLS + * method. + */ + sslsocket.startHandshake(); + + /* + * Check server identity and trust. + */ + boolean idCheck = PropUtil.getBooleanProperty(props, + prefix + ".ssl.checkserveridentity", false); + if (idCheck) + checkServerIdentity(host, sslsocket); + if (sf instanceof MailSSLSocketFactory) { + MailSSLSocketFactory msf = (MailSSLSocketFactory)sf; + if (!msf.isServerTrusted(host, sslsocket)) { + throw cleanupAndThrow(sslsocket, + new IOException("Server is not trusted: " + host)); + } + } + } + + private static IOException cleanupAndThrow(Socket socket, IOException ife) { + try { + socket.close(); + } catch (Throwable thr) { + if (isRecoverable(thr)) { + ife.addSuppressed(thr); + } else { + thr.addSuppressed(ife); + if (thr instanceof Error) { + throw (Error) thr; + } + if (thr instanceof RuntimeException) { + throw (RuntimeException) thr; + } + throw new RuntimeException("unexpected exception", thr); + } + } + return ife; + } + + private static boolean isRecoverable(Throwable t) { + return (t instanceof Exception) || (t instanceof LinkageError); + } + + /** + * Check the server from the Socket connection against the server name(s) + * as expressed in the server certificate (RFC 2595 check). + * + * @param server name of the server expected + * @param sslSocket SSLSocket connected to the server + * @exception IOException if we can't verify identity of server + */ + private static void checkServerIdentity(String server, SSLSocket sslSocket) + throws IOException { + + // Check against the server name(s) as expressed in server certificate + try { + java.security.cert.Certificate[] certChain = + sslSocket.getSession().getPeerCertificates(); + if (certChain != null && certChain.length > 0 && + certChain[0] instanceof X509Certificate && + matchCert(server, (X509Certificate)certChain[0])) + return; + } catch (SSLPeerUnverifiedException e) { + sslSocket.close(); + IOException ioex = new IOException( + "Can't verify identity of server: " + server); + ioex.initCause(e); + throw ioex; + } + + // If we get here, there is nothing to consider the server as trusted. + sslSocket.close(); + throw new IOException("Can't verify identity of server: " + server); + } + + /** + * Do any of the names in the cert match the server name? + * + * @param server name of the server expected + * @param cert X509Certificate to get the subject's name from + * @return true if it matches + */ + private static boolean matchCert(String server, X509Certificate cert) { + if (logger.isLoggable(Level.FINER)) + logger.finer("matchCert server " + + server + ", cert " + cert); + + /* + * First, try to use sun.security.util.HostnameChecker, + * which exists in Sun's JDK starting with 1.4.1. + * We use reflection to access it in case it's not available + * in the JDK we're running on. + */ + try { + Class<?> hnc = Class.forName("sun.security.util.HostnameChecker"); + // invoke HostnameChecker.getInstance(HostnameChecker.TYPE_LDAP) + // HostnameChecker.TYPE_LDAP == 2 + // LDAP requires the same regex handling as we need + Method getInstance = hnc.getMethod("getInstance", + new Class<?>[] { byte.class }); + Object hostnameChecker = getInstance.invoke(new Object(), + new Object[] { Byte.valueOf((byte)2) }); + + // invoke hostnameChecker.match( server, cert) + if (logger.isLoggable(Level.FINER)) + logger.finer("using sun.security.util.HostnameChecker"); + Method match = hnc.getMethod("match", + new Class<?>[] { String.class, X509Certificate.class }); + try { + match.invoke(hostnameChecker, new Object[] { server, cert }); + return true; + } catch (InvocationTargetException cex) { + logger.log(Level.FINER, "HostnameChecker FAIL", cex); + return false; + } + } catch (Exception ex) { + logger.log(Level.FINER, "NO sun.security.util.HostnameChecker", ex); + // ignore failure and continue below + } + + /* + * Lacking HostnameChecker, we implement a crude version of + * the same checks ourselves. + */ + try { + /* + * Check each of the subjectAltNames. + * XXX - only checks DNS names, should also handle + * case where server name is a literal IP address + */ + Collection<List<?>> names = cert.getSubjectAlternativeNames(); + if (names != null) { + boolean foundName = false; + for (Iterator<List<?>> it = names.iterator(); it.hasNext(); ) { + List<?> nameEnt = it.next(); + Integer type = (Integer)nameEnt.get(0); + if (type.intValue() == 2) { // 2 == dNSName + foundName = true; + String name = (String)nameEnt.get(1); + if (logger.isLoggable(Level.FINER)) + logger.finer("found name: " + name); + if (matchServer(server, name)) + return true; + } + } + if (foundName) // found a name, but no match + return false; + } + } catch (CertificateParsingException ex) { + // ignore it + } + + // XXX - following is a *very* crude parse of the name and ignores + // all sorts of important issues such as quoting + Pattern p = Pattern.compile("CN=([^,]*)"); + Matcher m = p.matcher(cert.getSubjectX500Principal().getName()); + if (m.find() && matchServer(server, m.group(1).trim())) + return true; + + return false; + } + + /** + * Does the server we're expecting to connect to match the + * given name from the server's certificate? + * + * @param server name of the server expected + * @param name name from the server's certificate + */ + private static boolean matchServer(String server, String name) { + if (logger.isLoggable(Level.FINER)) + logger.finer("match server " + server + " with " + name); + if (name.startsWith("*.")) { + // match "foo.example.com" with "*.example.com" + String tail = name.substring(2); + if (tail.length() == 0) + return false; + int off = server.length() - tail.length(); + if (off < 1) + return false; + // if tail matches and is preceeded by "." + return server.charAt(off - 1) == '.' && + server.regionMatches(true, off, tail, 0, tail.length()); + } else + return server.equalsIgnoreCase(name); + } + + /** + * Use the HTTP CONNECT protocol to connect to a + * site through an HTTP proxy server. <p> + * + * Protocol is roughly: + * <pre> + * CONNECT <host>:<port> HTTP/1.1 + * Host: <host>:<port> + * <blank line> + * + * HTTP/1.1 200 Connect established + * <headers> + * <blank line> + * </pre> + */ + private static void proxyConnect(Socket socket, + String proxyHost, int proxyPort, + String proxyUser, String proxyPassword, + String host, int port, int cto) + throws IOException { + if (logger.isLoggable(Level.FINE)) + logger.fine("connecting through proxy " + + proxyHost + ":" + proxyPort + " to " + + host + ":" + port); + if (cto >= 0) + socket.connect(new InetSocketAddress(proxyHost, proxyPort), cto); + else + socket.connect(new InetSocketAddress(proxyHost, proxyPort)); + PrintStream os = new PrintStream(socket.getOutputStream(), false, + StandardCharsets.UTF_8.name()); + StringBuilder requestBuilder = new StringBuilder(); + requestBuilder.append("CONNECT ").append(host).append(":").append(port). + append(" HTTP/1.1\r\n"); + requestBuilder.append("Host: ").append(host).append(":").append(port). + append("\r\n"); + if (proxyUser != null && proxyPassword != null) { + byte[] upbytes = (proxyUser + ':' + proxyPassword). + getBytes(StandardCharsets.UTF_8); + String proxyHeaderValue = new String( + BASE64EncoderStream.encode(upbytes), + StandardCharsets.US_ASCII); + requestBuilder.append("Proxy-Authorization: Basic "). + append(proxyHeaderValue).append("\r\n"); + } + requestBuilder.append("Proxy-Connection: keep-alive\r\n\r\n"); + os.print(requestBuilder.toString()); + os.flush(); + BufferedReader r = new BufferedReader(new InputStreamReader( + socket.getInputStream(), StandardCharsets.UTF_8)); + String line; + boolean first = true; + while ((line = r.readLine()) != null) { + if (line.length() == 0) + break; + logger.finest(line); + if (first) { + StringTokenizer st = new StringTokenizer(line); + String http = st.nextToken(); + String code = st.nextToken(); + if (!code.equals("200")) { + try { + socket.close(); + } catch (IOException ioex) { + // ignored + } + ConnectException ex = new ConnectException( + "connection through proxy " + + proxyHost + ":" + proxyPort + " to " + + host + ":" + port + " failed: " + line); + logger.log(Level.FINE, "connect failed", ex); + throw ex; + } + first = false; + } + } + } + + /** + * Parse a string into whitespace separated tokens + * and return the tokens in an array. + */ + private static String[] stringArray(String s) { + StringTokenizer st = new StringTokenizer(s); + List<String> tokens = new ArrayList<>(); + while (st.hasMoreTokens()) + tokens.add(st.nextToken()); + return tokens.toArray(new String[tokens.size()]); + } + + /** + * Convenience method to get our context class loader. + * Assert any privileges we might have and then call the + * Thread.getContextClassLoader method. + */ + private static ClassLoader getContextClassLoader() { + return + AccessController.doPrivileged(new PrivilegedAction<ClassLoader>() { + @Override + public ClassLoader run() { + ClassLoader cl = null; + try { + cl = Thread.currentThread().getContextClassLoader(); + } catch (SecurityException ex) { } + return cl; + } + }); + } +}
diff --git a/mail/src/main/java/com/sun/mail/util/TraceInputStream.java b/mail/src/main/java/com/sun/mail/util/TraceInputStream.java new file mode 100644 index 0000000..fceb7ac --- /dev/null +++ b/mail/src/main/java/com/sun/mail/util/TraceInputStream.java
@@ -0,0 +1,140 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.*; +import java.util.logging.Level; + +/** + * This class is a FilterInputStream that writes the bytes + * being read from the given input stream into the given output + * stream. This class is typically used to provide a trace of + * the data that is being retrieved from an input stream. + * + * @author John Mani + */ + +public class TraceInputStream extends FilterInputStream { + private boolean trace = false; + private boolean quote = false; + private OutputStream traceOut; + + /** + * Creates an input stream filter built on top of the specified + * input stream. + * + * @param in the underlying input stream. + * @param logger log trace here + */ + public TraceInputStream(InputStream in, MailLogger logger) { + super(in); + this.trace = logger.isLoggable(Level.FINEST); + this.traceOut = new LogOutputStream(logger); + } + + /** + * Creates an input stream filter built on top of the specified + * input stream. + * + * @param in the underlying input stream. + * @param traceOut the trace stream. + */ + public TraceInputStream(InputStream in, OutputStream traceOut) { + super(in); + this.traceOut = traceOut; + } + + /** + * Set trace mode. + * @param trace the trace mode + */ + public void setTrace(boolean trace) { + this.trace = trace; + } + + /** + * Set quote mode. + * @param quote the quote mode + */ + public void setQuote(boolean quote) { + this.quote = quote; + } + + /** + * Reads the next byte of data from this input stream. Returns + * <code>-1</code> if no data is available. Writes out the read + * byte into the trace stream, if trace mode is <code>true</code> + */ + @Override + public int read() throws IOException { + int b = in.read(); + if (trace && b != -1) { + if (quote) + writeByte(b); + else + traceOut.write(b); + } + return b; + } + + /** + * Reads up to <code>len</code> bytes of data from this input stream + * into an array of bytes. Returns <code>-1</code> if no more data + * is available. Writes out the read bytes into the trace stream, if + * trace mode is <code>true</code> + */ + @Override + public int read(byte b[], int off, int len) throws IOException { + int count = in.read(b, off, len); + if (trace && count != -1) { + if (quote) { + for (int i = 0; i < count; i++) + writeByte(b[off + i]); + } else + traceOut.write(b, off, count); + } + return count; + } + + /** + * Write a byte in a way that every byte value is printable ASCII. + */ + private final void writeByte(int b) throws IOException { + b &= 0xff; + if (b > 0x7f) { + traceOut.write('M'); + traceOut.write('-'); + b &= 0x7f; + } + if (b == '\r') { + traceOut.write('\\'); + traceOut.write('r'); + } else if (b == '\n') { + traceOut.write('\\'); + traceOut.write('n'); + traceOut.write('\n'); + } else if (b == '\t') { + traceOut.write('\\'); + traceOut.write('t'); + } else if (b < ' ') { + traceOut.write('^'); + traceOut.write('@' + b); + } else { + traceOut.write(b); + } + } +}
diff --git a/mail/src/main/java/com/sun/mail/util/TraceOutputStream.java b/mail/src/main/java/com/sun/mail/util/TraceOutputStream.java new file mode 100644 index 0000000..28f2820 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/util/TraceOutputStream.java
@@ -0,0 +1,146 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.*; +import java.util.logging.Level; + +/** + * This class is a subclass of DataOutputStream that copies the + * data being written into the DataOutputStream into another output + * stream. This class is used here to provide a debug trace of the + * stuff thats being written out into the DataOutputStream. + * + * @author John Mani + */ + +public class TraceOutputStream extends FilterOutputStream { + private boolean trace = false; + private boolean quote = false; + private OutputStream traceOut; + + /** + * Creates an output stream filter built on top of the specified + * underlying output stream. + * + * @param out the underlying output stream. + * @param logger log trace here + */ + public TraceOutputStream(OutputStream out, MailLogger logger) { + super(out); + this.trace = logger.isLoggable(Level.FINEST); + this.traceOut = new LogOutputStream(logger);; + } + + /** + * Creates an output stream filter built on top of the specified + * underlying output stream. + * + * @param out the underlying output stream. + * @param traceOut the trace stream. + */ + public TraceOutputStream(OutputStream out, OutputStream traceOut) { + super(out); + this.traceOut = traceOut; + } + + /** + * Set the trace mode. + * + * @param trace the trace mode + */ + public void setTrace(boolean trace) { + this.trace = trace; + } + + /** + * Set quote mode. + * @param quote the quote mode + */ + public void setQuote(boolean quote) { + this.quote = quote; + } + + /** + * Writes the specified <code>byte</code> to this output stream. + * Writes out the byte into the trace stream if the trace mode + * is <code>true</code> + * + * @param b the byte to write + * @exception IOException for I/O errors + */ + @Override + public void write(int b) throws IOException { + if (trace) { + if (quote) + writeByte(b); + else + traceOut.write(b); + } + out.write(b); + } + + /** + * Writes <code>b.length</code> bytes to this output stream. + * Writes out the bytes into the trace stream if the trace + * mode is <code>true</code> + * + * @param b bytes to write + * @param off offset in array + * @param len number of bytes to write + * @exception IOException for I/O errors + */ + @Override + public void write(byte b[], int off, int len) throws IOException { + if (trace) { + if (quote) { + for (int i = 0; i < len; i++) + writeByte(b[off + i]); + } else + traceOut.write(b, off, len); + } + out.write(b, off, len); + } + + /** + * Write a byte in a way that every byte value is printable ASCII. + */ + private final void writeByte(int b) throws IOException { + b &= 0xff; + if (b > 0x7f) { + traceOut.write('M'); + traceOut.write('-'); + b &= 0x7f; + } + if (b == '\r') { + traceOut.write('\\'); + traceOut.write('r'); + } else if (b == '\n') { + traceOut.write('\\'); + traceOut.write('n'); + traceOut.write('\n'); + } else if (b == '\t') { + traceOut.write('\\'); + traceOut.write('t'); + } else if (b < ' ') { + traceOut.write('^'); + traceOut.write('@' + b); + } else { + traceOut.write(b); + } + } +}
diff --git a/mail/src/main/java/com/sun/mail/util/UUDecoderStream.java b/mail/src/main/java/com/sun/mail/util/UUDecoderStream.java new file mode 100644 index 0000000..82f957c --- /dev/null +++ b/mail/src/main/java/com/sun/mail/util/UUDecoderStream.java
@@ -0,0 +1,334 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.*; + +/** + * This class implements a UUDecoder. It is implemented as + * a FilterInputStream, so one can just wrap this class around + * any input stream and read bytes from this filter. The decoding + * is done as the bytes are read out. + * + * @author John Mani + * @author Bill Shannon + */ + +public class UUDecoderStream extends FilterInputStream { + private String name; + private int mode; + + private byte[] buffer = new byte[45]; // max decoded chars in a line = 45 + private int bufsize = 0; // size of the cache + private int index = 0; // index into the cache + private boolean gotPrefix = false; + private boolean gotEnd = false; + private LineInputStream lin; + private boolean ignoreErrors; + private boolean ignoreMissingBeginEnd; + private String readAhead; + + /** + * Create a UUdecoder that decodes the specified input stream. + * The System property <code>mail.mime.uudecode.ignoreerrors</code> + * controls whether errors in the encoded data cause an exception + * or are ignored. The default is false (errors cause exception). + * The System property <code>mail.mime.uudecode.ignoremissingbeginend</code> + * controls whether a missing begin or end line cause an exception + * or are ignored. The default is false (errors cause exception). + * @param in the input stream + */ + public UUDecoderStream(InputStream in) { + super(in); + lin = new LineInputStream(in); + // default to false + ignoreErrors = PropUtil.getBooleanSystemProperty( + "mail.mime.uudecode.ignoreerrors", false); + // default to false + ignoreMissingBeginEnd = PropUtil.getBooleanSystemProperty( + "mail.mime.uudecode.ignoremissingbeginend", false); + } + + /** + * Create a UUdecoder that decodes the specified input stream. + * @param in the input stream + * @param ignoreErrors ignore errors? + * @param ignoreMissingBeginEnd ignore missing begin or end? + */ + public UUDecoderStream(InputStream in, boolean ignoreErrors, + boolean ignoreMissingBeginEnd) { + super(in); + lin = new LineInputStream(in); + this.ignoreErrors = ignoreErrors; + this.ignoreMissingBeginEnd = ignoreMissingBeginEnd; + } + + /** + * Read the next decoded byte from this input stream. The byte + * is returned as an <code>int</code> in the range <code>0</code> + * to <code>255</code>. If no byte is available because the end of + * the stream has been reached, the value <code>-1</code> is returned. + * This method blocks until input data is available, the end of the + * stream is detected, or an exception is thrown. + * + * @return next byte of data, or <code>-1</code> if the end of + * stream is reached. + * @exception IOException if an I/O error occurs. + * @see java.io.FilterInputStream#in + */ + @Override + public int read() throws IOException { + if (index >= bufsize) { + readPrefix(); + if (!decode()) + return -1; + index = 0; // reset index into buffer + } + return buffer[index++] & 0xff; // return lower byte + } + + @Override + public int read(byte[] buf, int off, int len) throws IOException { + int i, c; + for (i = 0; i < len; i++) { + if ((c = read()) == -1) { + if (i == 0) // At end of stream, so we should + i = -1; // return -1, NOT 0. + break; + } + buf[off+i] = (byte)c; + } + return i; + } + + @Override + public boolean markSupported() { + return false; + } + + @Override + public int available() throws IOException { + // This is only an estimate, since in.available() + // might include CRLFs too .. + return ((in.available() * 3)/4 + (bufsize-index)); + } + + /** + * Get the "name" field from the prefix. This is meant to + * be the pathname of the decoded file + * + * @return name of decoded file + * @exception IOException if an I/O error occurs. + */ + public String getName() throws IOException { + readPrefix(); + return name; + } + + /** + * Get the "mode" field from the prefix. This is the permission + * mode of the source file. + * + * @return permission mode of source file + * @exception IOException if an I/O error occurs. + */ + public int getMode() throws IOException { + readPrefix(); + return mode; + } + + /** + * UUencoded streams start off with the line: + * "begin <mode> <filename>" + * Search for this prefix and gobble it up. + */ + private void readPrefix() throws IOException { + if (gotPrefix) // got the prefix + return; + + mode = 0666; // defaults, overridden below + name = "encoder.buf"; // same default used by encoder + String line; + for (;;) { + // read till we get the prefix: "begin MODE FILENAME" + line = lin.readLine(); // NOTE: readLine consumes CRLF pairs too + if (line == null) { + if (!ignoreMissingBeginEnd) + throw new DecodingException("UUDecoder: Missing begin"); + // at EOF, fake it + gotPrefix = true; + gotEnd = true; + break; + } + if (line.regionMatches(false, 0, "begin", 0, 5)) { + try { + mode = Integer.parseInt(line.substring(6,9)); + } catch (NumberFormatException ex) { + if (!ignoreErrors) + throw new DecodingException( + "UUDecoder: Error in mode: " + ex.toString()); + } + if (line.length() > 10) { + name = line.substring(10); + } else { + if (!ignoreErrors) + throw new DecodingException( + "UUDecoder: Missing name: " + line); + } + gotPrefix = true; + break; + } else if (ignoreMissingBeginEnd && line.length() != 0) { + int count = line.charAt(0); + count = (count - ' ') & 0x3f; + int need = ((count * 8)+5)/6; + if (need == 0 || line.length() >= need + 1) { + /* + * Looks like a legitimate encoded line. + * Pretend we saw the "begin" line and + * save this line for later processing in + * decode(). + */ + readAhead = line; + gotPrefix = true; // fake it + break; + } + } + } + } + + private boolean decode() throws IOException { + + if (gotEnd) + return false; + bufsize = 0; + int count = 0; + String line; + for (;;) { + /* + * If we ignored a missing "begin", the first line + * will be saved in readAhead. + */ + if (readAhead != null) { + line = readAhead; + readAhead = null; + } else + line = lin.readLine(); + + /* + * Improperly encoded data sometimes omits the zero length + * line that starts with a space character, we detect the + * following "end" line here. + */ + if (line == null) { + if (!ignoreMissingBeginEnd) + throw new DecodingException( + "UUDecoder: Missing end at EOF"); + gotEnd = true; + return false; + } + if (line.equals("end")) { + gotEnd = true; + return false; + } + if (line.length() == 0) + continue; + count = line.charAt(0); + if (count < ' ') { + if (!ignoreErrors) + throw new DecodingException( + "UUDecoder: Buffer format error"); + continue; + } + + /* + * The first character in a line is the number of original (not + * the encoded atoms) characters in the line. Note that all the + * code below has to handle the <SPACE> character that indicates + * end of encoded stream. + */ + count = (count - ' ') & 0x3f; + + if (count == 0) { + line = lin.readLine(); + if (line == null || !line.equals("end")) { + if (!ignoreMissingBeginEnd) + throw new DecodingException( + "UUDecoder: Missing End after count 0 line"); + } + gotEnd = true; + return false; + } + + int need = ((count * 8)+5)/6; +//System.out.println("count " + count + ", need " + need + ", len " + line.length()); + if (line.length() < need + 1) { + if (!ignoreErrors) + throw new DecodingException( + "UUDecoder: Short buffer error"); + continue; + } + + // got a line we're committed to, break out and decode it + break; + } + + int i = 1; + byte a, b; + /* + * A correct uuencoder always encodes 3 characters at a time, even + * if there aren't 3 characters left. But since some people out + * there have broken uuencoders we handle the case where they + * don't include these "unnecessary" characters. + */ + while (bufsize < count) { + // continue decoding until we get 'count' decoded chars + a = (byte)((line.charAt(i++) - ' ') & 0x3f); + b = (byte)((line.charAt(i++) - ' ') & 0x3f); + buffer[bufsize++] = (byte)(((a << 2) & 0xfc) | ((b >>> 4) & 3)); + + if (bufsize < count) { + a = b; + b = (byte)((line.charAt(i++) - ' ') & 0x3f); + buffer[bufsize++] = + (byte)(((a << 4) & 0xf0) | ((b >>> 2) & 0xf)); + } + + if (bufsize < count) { + a = b; + b = (byte)((line.charAt(i++) - ' ') & 0x3f); + buffer[bufsize++] = (byte)(((a << 6) & 0xc0) | (b & 0x3f)); + } + } + return true; + } + + /*** begin TEST program ***** + public static void main(String argv[]) throws Exception { + FileInputStream infile = new FileInputStream(argv[0]); + UUDecoderStream decoder = new UUDecoderStream(infile); + int c; + + try { + while ((c = decoder.read()) != -1) + System.out.write(c); + System.out.flush(); + } catch (Exception e) { + e.printStackTrace(); + } + } + **** end TEST program ****/ +}
diff --git a/mail/src/main/java/com/sun/mail/util/UUEncoderStream.java b/mail/src/main/java/com/sun/mail/util/UUEncoderStream.java new file mode 100644 index 0000000..183cc34 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/util/UUEncoderStream.java
@@ -0,0 +1,205 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.*; + +/** + * This class implements a UUEncoder. It is implemented as + * a FilterOutputStream, so one can just wrap this class around + * any output stream and write bytes into this filter. The Encoding + * is done as the bytes are written out. + * + * @author John Mani + */ + +public class UUEncoderStream extends FilterOutputStream { + private byte[] buffer; // cache of bytes that are yet to be encoded + private int bufsize = 0; // size of the cache + private boolean wrotePrefix = false; + private boolean wroteSuffix = false; + + private String name; // name of file + private int mode; // permissions mode + + /** + * Create a UUencoder that encodes the specified input stream + * @param out the output stream + */ + public UUEncoderStream(OutputStream out) { + this(out, "encoder.buf", 0644); + } + + /** + * Create a UUencoder that encodes the specified input stream + * @param out the output stream + * @param name Specifies a name for the encoded buffer + */ + public UUEncoderStream(OutputStream out, String name) { + this(out, name, 0644); + } + + /** + * Create a UUencoder that encodes the specified input stream + * @param out the output stream + * @param name Specifies a name for the encoded buffer + * @param mode Specifies permission mode for the encoded buffer + */ + public UUEncoderStream(OutputStream out, String name, int mode) { + super(out); + this.name = name; + this.mode = mode; + buffer = new byte[45]; + } + + /** + * Set up the buffer name and permission mode. + * This method has any effect only if it is invoked before + * you start writing into the output stream + * + * @param name the buffer name + * @param mode the permission mode + */ + public void setNameMode(String name, int mode) { + this.name = name; + this.mode = mode; + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + for (int i = 0; i < len; i++) + write(b[off + i]); + } + + @Override + public void write(byte[] data) throws IOException { + write(data, 0, data.length); + } + + @Override + public void write(int c) throws IOException { + /* buffer up characters till we get a line's worth, then encode + * and write them out. Max number of characters allowed per + * line is 45. + */ + buffer[bufsize++] = (byte)c; + if (bufsize == 45) { + writePrefix(); + encode(); + bufsize = 0; + } + } + + @Override + public void flush() throws IOException { + if (bufsize > 0) { // If there's unencoded characters in the buffer + writePrefix(); + encode(); // .. encode them + bufsize = 0; + } + writeSuffix(); + out.flush(); + } + + @Override + public void close() throws IOException { + flush(); + out.close(); + } + + /** + * Write out the prefix: "begin <mode> <name>" + */ + private void writePrefix() throws IOException { + if (!wrotePrefix) { + // name should be ASCII, but who knows... + PrintStream ps = new PrintStream(out, false, "utf-8"); + ps.format("begin %o %s%n", mode, name); + ps.flush(); + wrotePrefix = true; + } + } + + /** + * Write a single line containing space and the suffix line + * containing the single word "end" (terminated by a newline) + */ + private void writeSuffix() throws IOException { + if (!wroteSuffix) { + PrintStream ps = new PrintStream(out, false, "us-ascii"); + ps.println(" \nend"); + ps.flush(); + wroteSuffix = true; + } + } + + /** + * Encode a line. + * Start off with the character count, followed by the encoded atoms + * and terminate with LF. (or is it CRLF or the local line-terminator ?) + * Take three bytes and encodes them into 4 characters + * If bufsize if not a multiple of 3, the remaining bytes are filled + * with '1'. This insures that the last line won't end in spaces + * and potentiallly be truncated. + */ + private void encode() throws IOException { + byte a, b, c; + int c1, c2, c3, c4; + int i = 0; + + // Start off with the count of characters in the line + out.write((bufsize & 0x3f) + ' '); + + while (i < bufsize) { + a = buffer[i++]; + if (i < bufsize) { + b = buffer[i++]; + if (i < bufsize) + c = buffer[i++]; + else // default c to 1 + c = 1; + } + else { // default b & c to 1 + b = 1; + c = 1; + } + + c1 = (a >>> 2) & 0x3f; + c2 = ((a << 4) & 0x30) | ((b >>> 4) & 0xf); + c3 = ((b << 2) & 0x3c) | ((c >>> 6) & 0x3); + c4 = c & 0x3f; + out.write(c1 + ' '); + out.write(c2 + ' '); + out.write(c3 + ' '); + out.write(c4 + ' '); + } + // Terminate with LF. (should it be CRLF or local line-terminator ?) + out.write('\n'); + } + + /**** begin TEST program ***** + public static void main(String argv[]) throws Exception { + FileInputStream infile = new FileInputStream(argv[0]); + UUEncoderStream encoder = new UUEncoderStream(System.out); + int c; + + while ((c = infile.read()) != -1) + encoder.write(c); + encoder.close(); + } + **** end TEST program *****/ +}
diff --git a/mail/src/main/java/com/sun/mail/util/WriteTimeoutSocket.java b/mail/src/main/java/com/sun/mail/util/WriteTimeoutSocket.java new file mode 100644 index 0000000..5cd01b2 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/util/WriteTimeoutSocket.java
@@ -0,0 +1,401 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.*; +import java.net.*; +import java.util.concurrent.*; +import java.util.Collections; +import java.util.Set; +import java.nio.channels.SocketChannel; +import java.lang.reflect.*; + +/** + * A special Socket that uses a ScheduledExecutorService to + * implement timeouts for writes. The write timeout is specified + * (in milliseconds) when the WriteTimeoutSocket is created. + * + * @author Bill Shannon + */ +public class WriteTimeoutSocket extends Socket { + + // delegate all operations to this socket + private final Socket socket; + // to schedule task to cancel write after timeout + private final ScheduledExecutorService ses; + // the timeout, in milliseconds + private final int timeout; + + public WriteTimeoutSocket(Socket socket, int timeout) throws IOException { + this.socket = socket; + // XXX - could share executor with all instances? + this.ses = Executors.newScheduledThreadPool(1); + this.timeout = timeout; + } + + public WriteTimeoutSocket(int timeout) throws IOException { + this(new Socket(), timeout); + } + + public WriteTimeoutSocket(InetAddress address, int port, int timeout) + throws IOException { + this(timeout); + socket.connect(new InetSocketAddress(address, port)); + } + + public WriteTimeoutSocket(InetAddress address, int port, + InetAddress localAddress, int localPort, int timeout) + throws IOException { + this(timeout); + socket.bind(new InetSocketAddress(localAddress, localPort)); + socket.connect(new InetSocketAddress(address, port)); + } + + public WriteTimeoutSocket(String host, int port, int timeout) + throws IOException { + this(timeout); + socket.connect(new InetSocketAddress(host, port)); + } + + public WriteTimeoutSocket(String host, int port, + InetAddress localAddress, int localPort, int timeout) + throws IOException { + this(timeout); + socket.bind(new InetSocketAddress(localAddress, localPort)); + socket.connect(new InetSocketAddress(host, port)); + } + + // override all Socket methods and delegate to underlying Socket + + @Override + public void connect(SocketAddress remote) throws IOException { + socket.connect(remote, 0); + } + + @Override + public void connect(SocketAddress remote, int timeout) throws IOException { + socket.connect(remote, timeout); + } + + @Override + public void bind(SocketAddress local) throws IOException { + socket.bind(local); + } + + @Override + public SocketAddress getRemoteSocketAddress() { + return socket.getRemoteSocketAddress(); + } + + @Override + public SocketAddress getLocalSocketAddress() { + return socket.getLocalSocketAddress(); + } + + @Override + public void setPerformancePreferences(int connectionTime, int latency, + int bandwidth) { + socket.setPerformancePreferences(connectionTime, latency, bandwidth); + } + + @Override + public SocketChannel getChannel() { + return socket.getChannel(); + } + + @Override + public InetAddress getInetAddress() { + return socket.getInetAddress(); + } + + @Override + public InetAddress getLocalAddress() { + return socket.getLocalAddress(); + } + + @Override + public int getPort() { + return socket.getPort(); + } + + @Override + public int getLocalPort() { + return socket.getLocalPort(); + } + + @Override + public InputStream getInputStream() throws IOException { + return socket.getInputStream(); + } + + @Override + public synchronized OutputStream getOutputStream() throws IOException { + // wrap the returned stream to implement write timeout + return new TimeoutOutputStream(socket.getOutputStream(), ses, timeout); + } + + @Override + public void setTcpNoDelay(boolean on) throws SocketException { + socket.setTcpNoDelay(on); + } + + @Override + public boolean getTcpNoDelay() throws SocketException { + return socket.getTcpNoDelay(); + } + + @Override + public void setSoLinger(boolean on, int linger) throws SocketException { + socket.setSoLinger(on, linger); + } + + @Override + public int getSoLinger() throws SocketException { + return socket.getSoLinger(); + } + + @Override + public void sendUrgentData(int data) throws IOException { + socket.sendUrgentData(data); + } + + @Override + public void setOOBInline(boolean on) throws SocketException { + socket.setOOBInline(on); + } + + @Override + public boolean getOOBInline() throws SocketException { + return socket.getOOBInline(); + } + + @Override + public void setSoTimeout(int timeout) throws SocketException { + socket.setSoTimeout(timeout); + } + + @Override + public int getSoTimeout() throws SocketException { + return socket.getSoTimeout(); + } + + @Override + public void setSendBufferSize(int size) throws SocketException { + socket.setSendBufferSize(size); + } + + @Override + public int getSendBufferSize() throws SocketException { + return socket.getSendBufferSize(); + } + + @Override + public void setReceiveBufferSize(int size) throws SocketException { + socket.setReceiveBufferSize(size); + } + + @Override + public int getReceiveBufferSize() throws SocketException { + return socket.getReceiveBufferSize(); + } + + @Override + public void setKeepAlive(boolean on) throws SocketException { + socket.setKeepAlive(on); + } + + @Override + public boolean getKeepAlive() throws SocketException { + return socket.getKeepAlive(); + } + + @Override + public void setTrafficClass(int tc) throws SocketException { + socket.setTrafficClass(tc); + } + + @Override + public int getTrafficClass() throws SocketException { + return socket.getTrafficClass(); + } + + @Override + public void setReuseAddress(boolean on) throws SocketException { + socket.setReuseAddress(on); + } + + @Override + public boolean getReuseAddress() throws SocketException { + return socket.getReuseAddress(); + } + + @Override + public void close() throws IOException { + try { + socket.close(); + } finally { + ses.shutdownNow(); + } + } + + @Override + public void shutdownInput() throws IOException { + socket.shutdownInput(); + } + + @Override + public void shutdownOutput() throws IOException { + socket.shutdownOutput(); + } + + @Override + public String toString() { + return socket.toString(); + } + + @Override + public boolean isConnected() { + return socket.isConnected(); + } + + @Override + public boolean isBound() { + return socket.isBound(); + } + + @Override + public boolean isClosed() { + return socket.isClosed(); + } + + @Override + public boolean isInputShutdown() { + return socket.isInputShutdown(); + } + + @Override + public boolean isOutputShutdown() { + return socket.isOutputShutdown(); + } + + /* + * The following three methods were added to java.net.Socket in Java SE 9. + * Since they're not supported on Android, and since we know that we + * never use them in JavaMail, we just stub them out here. + */ + //@Override + public <T> Socket setOption(SocketOption<T> so, T val) throws IOException { + // socket.setOption(so, val); + // return this; + throw new UnsupportedOperationException("WriteTimeoutSocket.setOption"); + } + + //@Override + public <T> T getOption(SocketOption<T> so) throws IOException { + // return socket.getOption(so); + throw new UnsupportedOperationException("WriteTimeoutSocket.getOption"); + } + + //@Override + public Set<SocketOption<?>> supportedOptions() { + // return socket.supportedOptions(); + return Collections.emptySet(); + } + + /** + * KLUDGE for Android, which has this illegal non-Java Compatible method. + * + * @return the FileDescriptor object + */ + public FileDescriptor getFileDescriptor$() { + try { + Method m = Socket.class.getDeclaredMethod("getFileDescriptor$"); + return (FileDescriptor)m.invoke(socket); + } catch (Exception ex) { + return null; + } + } +} + + +/** + * An OutputStream that wraps the Socket's OutputStream and uses + * the ScheduledExecutorService to schedule a task to close the + * socket (aborting the write) if the timeout expires. + */ +class TimeoutOutputStream extends OutputStream { + private final OutputStream os; + private final ScheduledExecutorService ses; + private final Callable<Object> timeoutTask; + private final int timeout; + private byte[] b1; + + public TimeoutOutputStream(OutputStream os0, ScheduledExecutorService ses, + int timeout) throws IOException { + this.os = os0; + this.ses = ses; + this.timeout = timeout; + timeoutTask = new Callable<Object>() { + @Override + public Object call() throws Exception { + os.close(); // close the stream to abort the write + return null; + } + }; + } + + @Override + public synchronized void write(int b) throws IOException { + if (b1 == null) + b1 = new byte[1]; + b1[0] = (byte)b; + this.write(b1); + } + + @Override + public synchronized void write(byte[] bs, int off, int len) + throws IOException { + if ((off < 0) || (off > bs.length) || (len < 0) || + ((off + len) > bs.length) || ((off + len) < 0)) { + throw new IndexOutOfBoundsException(); + } else if (len == 0) { + return; + } + + // Implement timeout with a scheduled task + ScheduledFuture<Object> sf = null; + try { + try { + if (timeout > 0) + sf = ses.schedule(timeoutTask, + timeout, TimeUnit.MILLISECONDS); + } catch (RejectedExecutionException ex) { + // ignore it; Executor was shut down by another thread, + // the following write should fail with IOException + } + os.write(bs, off, len); + } finally { + if (sf != null) + sf.cancel(true); + } + } + + @Override + public void close() throws IOException { + os.close(); + } +}
diff --git a/mail/src/main/java/com/sun/mail/util/logging/CollectorFormatter.java b/mail/src/main/java/com/sun/mail/util/logging/CollectorFormatter.java new file mode 100644 index 0000000..3b67ba1 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/util/logging/CollectorFormatter.java
@@ -0,0 +1,601 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2018 Jason Mehrens. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package com.sun.mail.util.logging; + +import static com.sun.mail.util.logging.LogManagerProperties.fromLogManager; +import java.lang.reflect.UndeclaredThrowableException; +import java.text.MessageFormat; +import java.util.Comparator; +import java.util.Locale; +import java.util.ResourceBundle; +import java.util.logging.Formatter; +import java.util.logging.Handler; +import java.util.logging.LogRecord; + +/** + * A LogRecord formatter that takes a sequence of LogRecords and combines them + * into a single summary result. Formating of the head, LogRecord, and tail are + * delegated to the wrapped formatter. + * + * <p> + * By default each <tt>CollectorFormatter</tt> is initialized using the + * following LogManager configuration properties where + * <tt><formatter-name></tt> refers to the fully qualified class name or + * the fully qualified derived class name of the formatter. If properties are + * not defined, or contain invalid values, then the specified default values are + * used. + * <ul> + * <li><formatter-name>.comparator name of a + * {@linkplain java.util.Comparator} class used to choose the collected + * <tt>LogRecord</tt>. If a comparator is specified then the max + * <tt>LogRecord</tt> is chosen. If comparator is set to the string literal + * null, then the last record is chosen. (defaults to + * {@linkplain SeverityComparator}) + * + * <li><formatter-name>.comparator.reverse a boolean + * <tt>true</tt> to collect the min <tt>LogRecord</tt> or <tt>false</tt> to + * collect the max <tt>LogRecord</tt>. (defaults to <tt>false</tt>) + * + * <li><formatter-name>.format the + * {@linkplain java.text.MessageFormat MessageFormat} string used to format the + * collected summary statistics. The arguments are explained in detail in the + * {@linkplain #getTail(java.util.logging.Handler) getTail} documentation. + * (defaults to <tt>{0}{1}{2}{4,choice,-1#|0#|0<... {4,number,integer} + * more}\n</tt>) + * + * <li><formatter-name>.formatter name of a <tt>Formatter</tt> class used + * to format the collected LogRecord. (defaults to {@linkplain CompactFormatter}) + * + * </ul> + * + * @author Jason Mehrens + * @since JavaMail 1.5.2 + */ +public class CollectorFormatter extends Formatter { + /** + * Avoid depending on JMX runtime bean to get the start time. + */ + private static final long INIT_TIME = System.currentTimeMillis(); + /** + * The message format string used as the formatted output. + */ + private final String fmt; + /** + * The formatter used to format the chosen log record. + */ + private final Formatter formatter; + /** + * The comparator used to pick the log record to format. + */ + private final Comparator<? super LogRecord> comparator; + /** + * The last accepted record. Synchronized access is preferred over volatile + * for this class. + */ + private LogRecord last; + /** + * The number of log records that have been formatted. + */ + private long count; + /** + * The number of log produced containing at least one log record. + * Only incremented when this formatter is reset. + */ + private long generation = 1L; + /** + * The number of log records that have been formatted with a thrown object. + */ + private long thrown; + /** + * The eldest log record time or eldest time possible for this instance. + */ + private long minMillis = INIT_TIME; + /** + * The newest log record time. + */ + private long maxMillis = Long.MIN_VALUE; + + /** + * Creates the formatter using the LogManager defaults. + * + * @throws SecurityException if a security manager exists and the caller + * does not have <tt>LoggingPermission("control")</tt>. + * @throws UndeclaredThrowableException if there are problems loading from + * the LogManager. + */ + public CollectorFormatter() { + final String p = getClass().getName(); + this.fmt = initFormat(p); + this.formatter = initFormatter(p); + this.comparator = initComparator(p); + } + + /** + * Creates the formatter using the given format. + * + * @param format the message format or null to use the LogManager default. + * @throws SecurityException if a security manager exists and the caller + * does not have <tt>LoggingPermission("control")</tt>. + * @throws UndeclaredThrowableException if there are problems loading from + * the LogManager. + */ + public CollectorFormatter(String format) { + final String p = getClass().getName(); + this.fmt = format == null ? initFormat(p) : format; + this.formatter = initFormatter(p); + this.comparator = initComparator(p); + } + + /** + * Creates the formatter using the given values. + * + * @param format the format string or null to use the LogManager default. + * @param f the formatter used on the collected log record or null to + * specify no formatter. + * @param c the comparator used to determine which log record to format or + * null to specify no comparator. + * @throws SecurityException if a security manager exists and the caller + * does not have <tt>LoggingPermission("control")</tt>. + * @throws UndeclaredThrowableException if there are problems loading from + * the LogManager. + */ + public CollectorFormatter(String format, Formatter f, + Comparator<? super LogRecord> c) { + final String p = getClass().getName(); + this.fmt = format == null ? initFormat(p) : format; + this.formatter = f; + this.comparator = c; + } + + /** + * Accumulates log records which will be used to produce the final output. + * The output is generated using the {@link #getTail} method which also + * resets this formatter back to its original state. + * + * @param record the record to store. + * @return an empty string. + * @throws NullPointerException if the given record is null. + */ + @Override + public String format(final LogRecord record) { + if (record == null) { + throw new NullPointerException(); + } + + boolean accepted; + do { + final LogRecord peek = peek(); + //The self compare of the first record acts like a type check. + LogRecord update = apply(peek != null ? peek : record, record); + if (peek != update) { //Not identical. + update.getSourceMethodName(); //Infer caller, null check. + accepted = acceptAndUpdate(peek, update); + } else { + accepted = accept(peek, record); + } + } while (!accepted); + return ""; + } + + /** + * Formats the collected LogRecord and summary statistics. The collected + * results are reset after calling this method. The + * {@linkplain java.text.MessageFormat java.text} argument indexes are assigned + * to the following properties: + * + * <ol start='0'> + * <li>{@code head} the + * {@linkplain Formatter#getHead(java.util.logging.Handler) head} string + * returned from the target formatter and + * {@linkplain #finish(java.lang.String) finished} by this formatter. + * <li>{@code formatted} the current log record + * {@linkplain Formatter#format(java.util.logging.LogRecord) formatted} by + * the target formatter and {@linkplain #finish(java.lang.String) finished} + * by this formatter. If the formatter is null then record is formatted by + * this {@linkplain #formatMessage(java.util.logging.LogRecord) formatter}. + * <li>{@code tail} the + * {@linkplain Formatter#getTail(java.util.logging.Handler) tail} string + * returned from the target formatter and + * {@linkplain #finish(java.lang.String) finished} by this formatter. + * <li>{@code count} the total number of log records + * {@linkplain #format consumed} by this formatter. + * <li>{@code remaining} the count minus one. + * <li>{@code thrown} the total number of log records + * {@linkplain #format consumed} by this formatter with an assigned + * {@linkplain java.util.logging.LogRecord#getThrown throwable}. + * <li>{@code normal messages} the count minus the thrown. + * <li>{@code minMillis} the eldest log record + * {@linkplain java.util.logging.LogRecord#getMillis event time} + * {@linkplain #format consumed} by this formatter. If the count is zero + * then this is set to the previous max or approximate start time if there + * was no previous max. By default this parameter is defined as a number. + * The format type and format style rules from the + * {@linkplain java.text.MessageFormat} should be used to convert this from + * milliseconds to a date or time. + * <li>{@code maxMillis} the most recent log record + * {@linkplain java.util.logging.LogRecord#getMillis event time} + * {@linkplain #format consumed} by this formatter. If the count is zero + * then this is set to the {@linkplain System#currentTimeMillis() current time}. + * By default this parameter is defined as a number. The format type and + * format style rules from the {@linkplain java.text.MessageFormat} should be + * used to convert this from milliseconds to a date or time. + * <li>{@code elapsed} the elapsed time in milliseconds between the + * {@code maxMillis} and {@code minMillis}. + * <li>{@code startTime} the approximate start time in milliseconds. By + * default this parameter is defined as a number. The format type and format + * style rules from the {@linkplain java.text.MessageFormat} should be used to + * convert this from milliseconds to a date or time. + * <li>{@code currentTime} the + * {@linkplain System#currentTimeMillis() current time} in milliseconds. By + * default this parameter is defined as a number. The format type and format + * style rules from the {@linkplain java.text.MessageFormat} should be used to + * convert this from milliseconds to a date or time. + * <li>{@code uptime} the elapsed time in milliseconds between the + * {@code currentTime} and {@code startTime}. + * <li>{@code generation} the number times this method produced output with + * at least one {@linkplain #format consumed} log record. This can be used + * to track the number of complete reports this formatter has produced. + * </ol> + * + * <p> + * Some example formats:<br> + * <ul> + * <li>{@code com.sun.mail.util.logging.CollectorFormatter.format={0}{1}{2}{4,choice,-1#|0#|0<... {4,number,integer} more}\n} + * <p> + * This prints the head ({@code {0}}), format ({@code {1}}), and tail + * ({@code {2}}) from the target formatter followed by the number of + * remaining ({@code {4}}) log records consumed by this formatter if there + * are any remaining records. + * <pre> + * Encoding failed.|NullPointerException: null String.getBytes(:913)... 3 more + * </pre> + * <li>{@code com.sun.mail.util.logging.CollectorFormatter.format=These {3} messages occurred between\n{7,date,EEE, MMM dd HH:mm:ss:S ZZZ yyyy} and {8,time,EEE, MMM dd HH:mm:ss:S ZZZ yyyy}\n} + * <p> + * This prints the count ({@code {3}}) followed by the date and time of the + * eldest log record ({@code {7}}) and the date and time of the most recent + * log record ({@code {8}}). + * <pre> + * These 292 messages occurred between + * Tue, Jul 21 14:11:42:449 -0500 2009 and Fri, Nov 20 07:29:24:0 -0600 2009 + * </pre> + * <li>{@code com.sun.mail.util.logging.CollectorFormatter.format=These {3} messages occurred between {9,choice,86400000#{7,date} {7,time} and {8,time}|86400000<{7,date} and {8,date}}\n} + * <p> + * This prints the count ({@code {3}}) and then chooses the format based on + * the elapsed time ({@code {9}}). If the elapsed time is less than one day + * then the eldest log record ({@code {7}}) date and time is formatted + * followed by just the time of the most recent log record ({@code {8}}. + * Otherwise, the just the date of the eldest log record ({@code {7}}) and + * just the date of most recent log record ({@code {8}} is formatted. + * <pre> + * These 73 messages occurred between Jul 21, 2009 2:11:42 PM and 2:13:32 PM + * + * These 116 messages occurred between Jul 21, 2009 and Aug 20, 2009 + * </pre> + * <li>{@code com.sun.mail.util.logging.CollectorFormatter.format={13} alert reports since {10,date}.\n} + * <p> + * This prints the generation ({@code {13}}) followed by the start time + * ({@code {10}}) formatted as a date. + * <pre> + * 4,320 alert reports since Jul 21, 2012. + * </pre> + * </ul> + * + * @param h the handler or null. + * @return the output string. + */ + @Override + public String getTail(final Handler h) { + super.getTail(h); //Be forward compatible with super.getHead. + return formatRecord(h, true); + } + + /** + * Formats the collected LogRecord and summary statistics. The LogRecord and + * summary statistics are not changed by calling this method. + * + * @return the current record formatted or the default toString. + * @see #getTail(java.util.logging.Handler) + */ + @Override + public String toString() { + String result; + try { + result = formatRecord((Handler) null, false); + } catch (final RuntimeException ignore) { + result = super.toString(); + } + return result; + } + + /** + * Used to choose the collected LogRecord. This implementation returns the + * greater of two LogRecords. + * + * @param t the current record. + * @param u the record that could replace the current. + * @return the greater of the given log records. + * @throws NullPointerException may occur if either record is null. + */ + protected LogRecord apply(final LogRecord t, final LogRecord u) { + if (t == null || u == null) { + throw new NullPointerException(); + } + + if (comparator != null) { + return comparator.compare(t, u) >= 0 ? t : u; + } else { + return u; + } + } + + /** + * Updates the summary statistics only if the expected record matches the + * last record. The update record is not stored. + * + * @param e the LogRecord that is expected. + * @param u the LogRecord used to collect statistics. + * @return true if the last record was the expected record. + * @throws NullPointerException if the update record is null. + */ + private synchronized boolean accept(final LogRecord e, final LogRecord u) { + /** + * LogRecord methods must be called before the check of the last stored + * record to guard against subclasses of LogRecord that might attempt to + * reset the state by triggering a call to getTail. + */ + final long millis = u.getMillis(); //Null check. + final Throwable ex = u.getThrown(); + if (last == e) { //Only if the exact same reference. + if (++count != 1L) { + minMillis = Math.min(minMillis, millis); + } else { //Show single records as instant and not a time period. + minMillis = millis; + } + maxMillis = Math.max(maxMillis, millis); + + if (ex != null) { + ++thrown; + } + return true; + } else { + return false; + } + } + + /** + * Resets all of the collected summary statistics including the LogRecord. + * @param min the current min milliseconds. + */ + private synchronized void reset(final long min) { + if (last != null) { + last = null; + ++generation; + } + + count = 0L; + thrown = 0L; + minMillis = min; + maxMillis = Long.MIN_VALUE; + } + + /** + * Formats the given record with the head and tail. + * + * @param h the Handler or null. + * @param reset true if the summary statistics and LogRecord should be reset + * back to initial values. + * @return the formatted string. + * @see #getTail(java.util.logging.Handler) + */ + private String formatRecord(final Handler h, final boolean reset) { + final LogRecord record; + final long c; + final long t; + final long g; + long msl; + long msh; + long now; + synchronized (this) { + record = last; + c = count; + g = generation; + t = thrown; + msl = minMillis; + msh = maxMillis; + now = System.currentTimeMillis(); + if (c == 0L) { + msh = now; + } + + if (reset) { //BUG ID 6351685 + reset(msh); + } + } + + final String head; + final String msg; + final String tail; + final Formatter f = this.formatter; + if (f != null) { + synchronized (f) { + head = f.getHead(h); + msg = record != null ? f.format(record) : ""; + tail = f.getTail(h); + } + } else { + head = ""; + msg = record != null ? formatMessage(record) : ""; + tail = ""; + } + + Locale l = null; + if (record != null) { + ResourceBundle rb = record.getResourceBundle(); + l = rb == null ? null : rb.getLocale(); + } + + final MessageFormat mf; + if (l == null) { //BUG ID 8039165 + mf = new MessageFormat(fmt); + } else { + mf = new MessageFormat(fmt, l); + } + + /** + * These arguments are described in the getTail documentation. + */ + return mf.format(new Object[]{finish(head), finish(msg), finish(tail), + c, (c - 1L), t, (c - t), msl, msh, (msh - msl), INIT_TIME, now, + (now - INIT_TIME), g}); + } + + /** + * Applied to the head, format, and tail returned by the target formatter. + * This implementation trims all input strings. + * + * @param s the string to transform. + * @return the transformed string. + * @throws NullPointerException if the given string is null. + */ + protected String finish(String s) { + return s.trim(); + } + + /** + * Peek at the current log record. + * + * @return null or the current log record. + */ + private synchronized LogRecord peek() { + return this.last; + } + + /** + * Updates the summary statistics and stores given LogRecord if the expected + * record matches the current record. + * + * @param e the expected record. + * @param u the update record. + * @return true if the update was performed. + * @throws NullPointerException if the update record is null. + */ + private synchronized boolean acceptAndUpdate(LogRecord e, LogRecord u) { + if (accept(e, u)) { + this.last = u; + return true; + } else { + return false; + } + } + + /** + * Gets the message format string from the LogManager or creates the default + * message format string. + * + * @param p the class name prefix. + * @return the format string. + * @throws NullPointerException if the given argument is null. + */ + private String initFormat(final String p) { + String v = fromLogManager(p.concat(".format")); + if (v == null || v.length() == 0) { + v = "{0}{1}{2}{4,choice,-1#|0#|0<... {4,number,integer} more}\n"; + } + return v; + } + + /** + * Gets and creates the formatter from the LogManager or creates the default + * formatter. + * + * @param p the class name prefix. + * @return the formatter. + * @throws NullPointerException if the given argument is null. + * @throws UndeclaredThrowableException if the formatter can not be created. + */ + private Formatter initFormatter(final String p) { + Formatter f; + String v = fromLogManager(p.concat(".formatter")); + if (v != null && v.length() != 0) { + if (!"null".equalsIgnoreCase(v)) { + try { + f = LogManagerProperties.newFormatter(v); + } catch (final RuntimeException re) { + throw re; + } catch (final Exception e) { + throw new UndeclaredThrowableException(e); + } + } else { + f = null; + } + } else { + //Don't force the byte code verifier to load the formatter. + f = Formatter.class.cast(new CompactFormatter()); + } + return f; + } + + /** + * Gets and creates the comparator from the LogManager or returns the + * default comparator. + * + * @param p the class name prefix. + * @return the comparator or null. + * @throws IllegalArgumentException if it was specified that the comparator + * should be reversed but no initial comparator was specified. + * @throws NullPointerException if the given argument is null. + * @throws UndeclaredThrowableException if the comparator can not be + * created. + */ + @SuppressWarnings("unchecked") + private Comparator<? super LogRecord> initComparator(final String p) { + Comparator<? super LogRecord> c; + final String name = fromLogManager(p.concat(".comparator")); + final String reverse = fromLogManager(p.concat(".comparator.reverse")); + try { + if (name != null && name.length() != 0) { + if (!"null".equalsIgnoreCase(name)) { + c = LogManagerProperties.newComparator(name); + if (Boolean.parseBoolean(reverse)) { + assert c != null; + c = LogManagerProperties.reverseOrder(c); + } + } else { + if (reverse != null) { + throw new IllegalArgumentException( + "No comparator to reverse."); + } else { + c = null; //No ordering. + } + } + } else { + if (reverse != null) { + throw new IllegalArgumentException( + "No comparator to reverse."); + } else { + //Don't force the byte code verifier to load the comparator. + c = Comparator.class.cast(SeverityComparator.getInstance()); + } + } + } catch (final RuntimeException re) { + throw re; //Avoid catch all. + } catch (final Exception e) { + throw new UndeclaredThrowableException(e); + } + return c; + } +}
diff --git a/mail/src/main/java/com/sun/mail/util/logging/CompactFormatter.java b/mail/src/main/java/com/sun/mail/util/logging/CompactFormatter.java new file mode 100644 index 0000000..f359f18 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/util/logging/CompactFormatter.java
@@ -0,0 +1,865 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2018 Jason Mehrens. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package com.sun.mail.util.logging; + +import java.util.*; +import java.util.logging.LogRecord; + +/** + * A plain text formatter that can produce fixed width output. By default this + * formatter will produce output no greater than 160 characters wide plus the + * separator and newline characters. Only specified fields support an + * {@linkplain #toAlternate(java.lang.String) alternate} fixed width format. + * <p> + * By default each <tt>CompactFormatter</tt> is initialized using the following + * LogManager configuration properties where + * <tt><formatter-name></tt> refers to the fully qualified class name or + * the fully qualified derived class name of the formatter. If properties are + * not defined, or contain invalid values, then the specified default values are + * used. + * <ul> + * <li><formatter-name>.format - the {@linkplain java.util.Formatter + * format} string used to transform the output. The format string can be + * used to fix the output size. (defaults to <tt>%7$#.160s%n</tt>)</li> + * </ul> + * + * @author Jason Mehrens + * @since JavaMail 1.5.2 + */ +public class CompactFormatter extends java.util.logging.Formatter { + + /** + * Load any declared classes to workaround GLASSFISH-21258. + */ + static { + loadDeclaredClasses(); + } + + /** + * Used to load declared classes encase class loader doesn't allow loading + * during JVM termination. This method is used with unit testing. + * + * @return an array of classes never null. + */ + private static Class<?>[] loadDeclaredClasses() { + return new Class<?>[]{Alternate.class}; + } + + /** + * Holds the java.util.Formatter pattern. + */ + private final String fmt; + + /** + * Creates an instance with a default format pattern. + */ + public CompactFormatter() { + String p = getClass().getName(); + this.fmt = initFormat(p); + } + + /** + * Creates an instance with the given format pattern. + * + * @param format the {@linkplain java.util.Formatter pattern} or null to use + * the LogManager default. The arguments are described in the + * {@linkplain #format(java.util.logging.LogRecord) format} method. + */ + public CompactFormatter(final String format) { + String p = getClass().getName(); + this.fmt = format == null ? initFormat(p) : format; + } + + /** + * Format the given log record and returns the formatted string. The + * {@linkplain java.util.Formatter#format(java.lang.String, java.lang.Object...) + * java.util} argument indexes are assigned to the following properties: + * + * <ol start='0'> + * <li>{@code format} - the {@linkplain java.util.Formatter + * java.util.Formatter} format string specified in the + * <formatter-name>.format property or the format that was given when + * this formatter was created.</li> + * <li>{@code date} - if the log record supports nanoseconds then a + * ZonedDateTime object representing the event time of the log record in the + * system time zone. Otherwise, a {@linkplain Date} object representing + * {@linkplain LogRecord#getMillis event time} of the log record.</li> + * <li>{@code source} - a string representing the caller, if available; + * otherwise, the logger's name.</li> + * <li>{@code logger} - the logger's + * {@linkplain Class#getSimpleName() simple} + * {@linkplain LogRecord#getLoggerName() name}.</li> + * <li>{@code level} - the + * {@linkplain java.util.logging.Level#getLocalizedName log level}.</li> + * <li>{@code message} - the formatted log message returned from the + * {@linkplain #formatMessage(LogRecord)} method.</li> + * <li>{@code thrown} - a string representing the + * {@linkplain LogRecord#getThrown throwable} associated with the log record + * and a relevant stack trace element if available; otherwise, an empty + * string is used.</li> + * <li>{@code message|thrown} The message and the thrown properties joined + * as one parameter. This parameter supports + * {@linkplain #toAlternate(java.lang.String) alternate} form.</li> + * <li>{@code thrown|message} The thrown and message properties joined as + * one parameter. This parameter supports + * {@linkplain #toAlternate(java.lang.String) alternate} form.</li> + * <li>{@code sequence} the + * {@linkplain LogRecord#getSequenceNumber() sequence number} if the given + * log record.</li> + * <li>{@code thread id} the {@linkplain LogRecord#getThreadID() thread id} + * of the given log record. By default this is formatted as a {@code long} + * by an unsigned conversion.</li> + * <li>{@code error} the throwable + * {@linkplain Class#getSimpleName() simple class name} and + * {@linkplain #formatError(LogRecord) error message} without any stack + * trace.</li> + * <li>{@code message|error} The message and error properties joined as one + * parameter. This parameter supports + * {@linkplain #toAlternate(java.lang.String) alternate} form.</li> + * <li>{@code error|message} The error and message properties joined as one + * parameter. This parameter supports + * {@linkplain #toAlternate(java.lang.String) alternate} form.</li> + * <li>{@code backtrace} only the + * {@linkplain #formatBackTrace(LogRecord) stack trace} of the given + * throwable.</li> + * <li>{@code bundlename} the resource bundle + * {@linkplain LogRecord#getResourceBundleName() name} of the given log + * record.</li> + * <li>{@code key} the {@linkplain LogRecord#getMessage() raw message} + * before localization or formatting.</li> + * </ol> + * + * <p> + * Some example formats:<br> + * <ul> + * <li>{@code com.sun.mail.util.logging.CompactFormatter.format=%7$#.160s%n} + * <p> + * This prints only 160 characters of the message|thrown ({@code 7$}) using + * the {@linkplain #toAlternate(java.lang.String) alternate} form. The + * separator is not included as part of the total width. + * <pre> + * Encoding failed.|NullPointerException: null String.getBytes(:913) + * </pre> + * + * <li>{@code com.sun.mail.util.logging.CompactFormatter.format=%7$#.20s%n} + * <p> + * This prints only 20 characters of the message|thrown ({@code 7$}) using + * the {@linkplain #toAlternate(java.lang.String) alternate} form. This will + * perform a weighted truncation of both the message and thrown properties + * of the log record. The separator is not included as part of the total + * width. + * <pre> + * Encoding|NullPointerE + * </pre> + * + * <li>{@code com.sun.mail.util.logging.CompactFormatter.format=%1$tc %2$s%n%4$s: %5$s%6$s%n} + * <p> + * This prints the timestamp ({@code 1$}) and the source ({@code 2$}) on the + * first line. The second line is the log level ({@code 4$}), log message + * ({@code 5$}), and the throwable with a relevant stack trace element + * ({@code 6$}) if one is available. + * <pre> + * Fri Nov 20 07:29:24 CST 2009 MyClass fatal + * SEVERE: Encoding failed.NullPointerException: null String.getBytes(:913) + * </pre> + * + * <li>{@code com.sun.mail.util.logging.CompactFormatter.format=%4$s: %12$#.160s%n} + * <p> + * This prints the log level ({@code 4$}) and only 160 characters of the + * message|error ({@code 12$}) using the alternate form. + * <pre> + * SEVERE: Unable to send notification.|SocketException: Permission denied: connect + * </pre> + * + * <li>{@code com.sun.mail.util.logging.CompactFormatter.format=[%9$d][%1$tT][%10$d][%2$s] %5$s%n%6$s%n} + * <p> + * This prints the sequence ({@code 9$}), event time ({@code 1$}) as 24 hour + * time, thread id ({@code 10$}), source ({@code 2$}), log message + * ({@code 5$}), and the throwable with back trace ({@code 6$}). + * <pre> + * [125][14:11:42][38][MyClass fatal] Unable to send notification. + * SocketException: Permission denied: connect SMTPTransport.openServer(:1949) + * </pre> + * + * </ul> + * + * @param record the log record to format. + * @return the formatted record. + * @throws NullPointerException if the given record is null. + */ + @Override + public String format(final LogRecord record) { + //LogRecord is mutable so define local vars. + ResourceBundle rb = record.getResourceBundle(); + Locale l = rb == null ? null : rb.getLocale(); + + String msg = formatMessage(record); + String thrown = formatThrown(record); + String err = formatError(record); + Object[] params = { + formatZonedDateTime(record), + formatSource(record), + formatLoggerName(record), + formatLevel(record), + msg, + thrown, + new Alternate(msg, thrown), + new Alternate(thrown, msg), + record.getSequenceNumber(), + formatThreadID(record), + err, + new Alternate(msg, err), + new Alternate(err, msg), + formatBackTrace(record), + record.getResourceBundleName(), + record.getMessage()}; + + if (l == null) { //BUG ID 6282094 + return String.format(fmt, params); + } else { + return String.format(l, fmt, params); + } + } + + /** + * Formats message for the log record. This method removes any fully + * qualified throwable class names from the message. + * + * @param record the log record. + * @return the formatted message string. + */ + @Override + public String formatMessage(final LogRecord record) { + String msg = super.formatMessage(record); + msg = replaceClassName(msg, record.getThrown()); + msg = replaceClassName(msg, record.getParameters()); + return msg; + } + + /** + * Formats the message from the thrown property of the log record. This + * method replaces fully qualified throwable class names from the message + * cause chain with simple class names. + * + * @param t the throwable to format or null. + * @return the empty string if null was given or the formatted message + * string from the throwable which may be null. + */ + public String formatMessage(final Throwable t) { + String r; + if (t != null) { + final Throwable apply = apply(t); + final String m = apply.getLocalizedMessage(); + final String s = apply.toString(); + final String sn = simpleClassName(apply.getClass()); + if (!isNullOrSpaces(m)) { + if (s.contains(m)) { + if (s.startsWith(apply.getClass().getName()) + || s.startsWith(sn)) { + r = replaceClassName(m, t); + } else { + r = replaceClassName(simpleClassName(s), t); + } + } else { + r = replaceClassName(simpleClassName(s) + ": " + m, t); + } + } else { + r = replaceClassName(simpleClassName(s), t); + } + + if (!r.contains(sn)) { + r = sn + ": " + r; + } + } else { + r = ""; + } + return r; + } + + /** + * Formats the level property of the given log record. + * + * @param record the record. + * @return the formatted logger name. + * @throws NullPointerException if the given record is null. + */ + public String formatLevel(final LogRecord record) { + return record.getLevel().getLocalizedName(); + } + + /** + * Formats the source from the given log record. + * + * @param record the record. + * @return the formatted source of the log record. + * @throws NullPointerException if the given record is null. + */ + public String formatSource(final LogRecord record) { + String source = record.getSourceClassName(); + if (source != null) { + if (record.getSourceMethodName() != null) { + source = simpleClassName(source) + " " + + record.getSourceMethodName(); + } else { + source = simpleClassName(source); + } + } else { + source = simpleClassName(record.getLoggerName()); + } + return source; + } + + /** + * Formats the logger name property of the given log record. + * + * @param record the record. + * @return the formatted logger name. + * @throws NullPointerException if the given record is null. + */ + public String formatLoggerName(final LogRecord record) { + return simpleClassName(record.getLoggerName()); + } + + /** + * Formats the thread id property of the given log record. By default this + * is formatted as a {@code long} by an unsigned conversion. + * + * @param record the record. + * @return the formatted thread id as a number. + * @throws NullPointerException if the given record is null. + * @since JavaMail 1.5.4 + */ + public Number formatThreadID(final LogRecord record) { + /** + * Thread.getID is defined as long and LogRecord.getThreadID is defined + * as int. Convert to unsigned as a means to better map the two types of + * thread identifiers. + */ + return (((long) record.getThreadID()) & 0xffffffffL); + } + + /** + * Formats the thrown property of a LogRecord. The returned string will + * contain a throwable message with a back trace. + * + * @param record the record. + * @return empty string if nothing was thrown or formatted string. + * @throws NullPointerException if the given record is null. + * @see #apply(java.lang.Throwable) + * @see #formatBackTrace(java.util.logging.LogRecord) + */ + public String formatThrown(final LogRecord record) { + String msg; + final Throwable t = record.getThrown(); + if (t != null) { + String site = formatBackTrace(record); + msg = formatMessage(t) + (isNullOrSpaces(site) ? "" : ' ' + site); + } else { + msg = ""; + } + return msg; + } + + /** + * Formats the thrown property of a LogRecord as an error message. The + * returned string will not contain a back trace. + * + * @param record the record. + * @return empty string if nothing was thrown or formatted string. + * @throws NullPointerException if the given record is null. + * @see Throwable#toString() + * @see #apply(java.lang.Throwable) + * @see #formatMessage(java.lang.Throwable) + * @since JavaMail 1.5.4 + */ + public String formatError(final LogRecord record) { + return formatMessage(record.getThrown()); + } + + /** + * Formats the back trace for the given log record. + * + * @param record the log record to format. + * @return the formatted back trace. + * @throws NullPointerException if the given record is null. + * @see #apply(java.lang.Throwable) + * @see #formatThrown(java.util.logging.LogRecord) + * @see #ignore(java.lang.StackTraceElement) + */ + public String formatBackTrace(final LogRecord record) { + String site = ""; + final Throwable t = record.getThrown(); + if (t != null) { + final Throwable root = apply(t); + StackTraceElement[] trace = root.getStackTrace(); + site = findAndFormat(trace); + if (isNullOrSpaces(site)) { + int limit = 0; + for (Throwable c = t; c != null; c = c.getCause()) { + StackTraceElement[] ste = c.getStackTrace(); + site = findAndFormat(ste); + if (!isNullOrSpaces(site)) { + break; + } else { + if (trace.length == 0) { + trace = ste; + } + } + + //Deal with excessive cause chains + //and cyclic throwables. + if (++limit == (1 << 16)) { + break; //Give up. + } + } + + //Punt. + if (isNullOrSpaces(site) && trace.length != 0) { + site = formatStackTraceElement(trace[0]); + } + } + } + return site; + } + + /** + * Finds and formats the first stack frame of interest. + * + * @param trace the fill stack to examine. + * @return a String that best describes the call site. + * @throws NullPointerException if stack trace element array is null. + */ + private String findAndFormat(final StackTraceElement[] trace) { + String site = ""; + for (StackTraceElement s : trace) { + if (!ignore(s)) { + site = formatStackTraceElement(s); + break; + } + } + + //Check if all code was compiled with no debugging info. + if (isNullOrSpaces(site)) { + for (StackTraceElement s : trace) { + if (!defaultIgnore(s)) { + site = formatStackTraceElement(s); + break; + } + } + } + return site; + } + + /** + * Formats a stack trace element into a simple call site. + * + * @param s the stack trace element to format. + * @return the formatted stack trace element. + * @throws NullPointerException if stack trace element is null. + * @see #formatThrown(java.util.logging.LogRecord) + */ + private String formatStackTraceElement(final StackTraceElement s) { + String v = simpleClassName(s.getClassName()); + String result; + if (v != null) { + result = s.toString().replace(s.getClassName(), v); + } else { + result = s.toString(); + } + + //If the class name contains the simple file name then remove file name. + v = simpleFileName(s.getFileName()); + if (v != null && result.startsWith(v)) { + result = result.replace(s.getFileName(), ""); + } + return result; + } + + /** + * Chooses a single throwable from the cause chain that will be formatted. + * This implementation chooses the throwable that best describes the chain. + * Subclasses can override this method to choose an alternate throwable for + * formatting. + * + * @param t the throwable from the log record. + * @return the chosen throwable or null only if the given argument is null. + * @see #formatThrown(java.util.logging.LogRecord) + */ + protected Throwable apply(final Throwable t) { + return SeverityComparator.getInstance().apply(t); + } + + /** + * Determines if a stack frame should be ignored as the cause of an error. + * + * @param s the stack trace element. + * @return true if this frame should be ignored. + * @see #formatThrown(java.util.logging.LogRecord) + */ + protected boolean ignore(final StackTraceElement s) { + return isUnknown(s) || defaultIgnore(s); + } + + /** + * Defines the alternate format. This implementation removes all control + * characters from the given string. + * + * @param s any string or null. + * @return null if the argument was null otherwise, an alternate string. + */ + protected String toAlternate(final String s) { + return s != null ? s.replaceAll("[\\x00-\\x1F\\x7F]+", "") : null; + } + + /** + * Gets the zoned date time from the given log record. + * + * @param record the current log record. + * @return a zoned date time or a legacy date object. + * @throws NullPointerException if the given record is null. + * @since JavaMail 1.5.6 + */ + private Comparable<?> formatZonedDateTime(final LogRecord record) { + Comparable<?> zdt = LogManagerProperties.getZonedDateTime(record); + if (zdt == null) { + zdt = new java.util.Date(record.getMillis()); + } + return zdt; + } + + /** + * Determines if a stack frame should be ignored as the cause of an error. + * This does not check for unknown line numbers because code can be compiled + * without debugging info. + * + * @param s the stack trace element. + * @return true if this frame should be ignored. + */ + private boolean defaultIgnore(final StackTraceElement s) { + return isSynthetic(s) || isStaticUtility(s) || isReflection(s); + } + + /** + * Determines if a stack frame is for a static utility class. + * + * @param s the stack trace element. + * @return true if this frame should be ignored. + */ + private boolean isStaticUtility(final StackTraceElement s) { + try { + return LogManagerProperties.isStaticUtilityClass(s.getClassName()); + } catch (RuntimeException ignore) { + } catch (Exception | LinkageError ignore) { + } + final String cn = s.getClassName(); + return (cn.endsWith("s") && !cn.endsWith("es")) + || cn.contains("Util") || cn.endsWith("Throwables"); + } + + /** + * Determines if a stack trace element is for a synthetic method. + * + * @param s the stack trace element. + * @return true if synthetic. + * @throws NullPointerException if stack trace element is null. + */ + private boolean isSynthetic(final StackTraceElement s) { + return s.getMethodName().indexOf('$') > -1; + } + + /** + * Determines if a stack trace element has an unknown line number or a + * native line number. + * + * @param s the stack trace element. + * @return true if the line number is unknown. + * @throws NullPointerException if stack trace element is null. + */ + private boolean isUnknown(final StackTraceElement s) { + return s.getLineNumber() < 0; + } + + /** + * Determines if a stack trace element represents a reflection frame. + * + * @param s the stack trace element. + * @return true if the line number is unknown. + * @throws NullPointerException if stack trace element is null. + */ + private boolean isReflection(final StackTraceElement s) { + try { + return LogManagerProperties.isReflectionClass(s.getClassName()); + } catch (RuntimeException ignore) { + } catch (Exception | LinkageError ignore) { + } + return s.getClassName().startsWith("java.lang.reflect.") + || s.getClassName().startsWith("sun.reflect."); + } + + /** + * Creates the format pattern for this formatter. + * + * @param p the class name prefix. + * @return the java.util.Formatter format string. + * @throws NullPointerException if the given class name is null. + */ + private String initFormat(final String p) { + String v = LogManagerProperties.fromLogManager(p.concat(".format")); + if (isNullOrSpaces(v)) { + v = "%7$#.160s%n"; //160 chars split between message and thrown. + } + return v; + } + + /** + * Searches the given message for all instances fully qualified class name + * with simple class name based off of the types contained in the given + * parameter array. + * + * @param msg the message. + * @param t the throwable cause chain to search or null. + * @return the modified message string. + */ + private static String replaceClassName(String msg, Throwable t) { + if (!isNullOrSpaces(msg)) { + int limit = 0; + for (Throwable c = t; c != null; c = c.getCause()) { + final Class<?> k = c.getClass(); + msg = msg.replace(k.getName(), simpleClassName(k)); + + //Deal with excessive cause chains and cyclic throwables. + if (++limit == (1 << 16)) { + break; //Give up. + } + } + } + return msg; + } + + /** + * Searches the given message for all instances fully qualified class name + * with simple class name based off of the types contained in the given + * parameter array. + * + * @param msg the message or null. + * @param p the parameter array or null. + * @return the modified message string. + */ + private static String replaceClassName(String msg, Object[] p) { + if (!isNullOrSpaces(msg) && p != null) { + for (Object o : p) { + if (o != null) { + final Class<?> k = o.getClass(); + msg = msg.replace(k.getName(), simpleClassName(k)); + } + } + } + return msg; + } + + /** + * Gets the simple class name from the given class. This is a workaround for + * BUG ID JDK-8057919. + * + * @param k the class object. + * @return the simple class name or null. + * @since JavaMail 1.5.3 + */ + private static String simpleClassName(final Class<?> k) { + try { + return k.getSimpleName(); + } catch (final InternalError JDK8057919) { + } + return simpleClassName(k.getName()); + } + + /** + * Converts a fully qualified class name to a simple class name. If the + * leading part of the given string is not a legal class name then the given + * string is returned. + * + * @param name the fully qualified class name prefix or null. + * @return the simple class name or given input. + */ + private static String simpleClassName(String name) { + if (name != null) { + int cursor = 0; + int sign = -1; + int dot = -1; + for (int c, prev = dot; cursor < name.length(); + cursor += Character.charCount(c)) { + c = name.codePointAt(cursor); + if (!Character.isJavaIdentifierPart(c)) { + if (c == ((int) '.')) { + if ((dot + 1) != cursor && (dot + 1) != sign) { + prev = dot; + dot = cursor; + } else { + return name; + } + } else { + if ((dot + 1) == cursor) { + dot = prev; + } + break; + } + } else { + if (c == ((int) '$')) { + sign = cursor; + } + } + } + + if (dot > -1 && ++dot < cursor && ++sign < cursor) { + name = name.substring(sign > dot ? sign : dot); + } + } + return name; + } + + /** + * Converts a file name with an extension to a file name without an + * extension. + * + * @param name the full file name or null. + * @return the simple file name or null. + */ + private static String simpleFileName(String name) { + if (name != null) { + final int index = name.lastIndexOf('.'); + name = index > -1 ? name.substring(0, index) : name; + } + return name; + } + + /** + * Determines is the given string is null or spaces. + * + * @param s the string or null. + * @return true if null or spaces. + */ + private static boolean isNullOrSpaces(final String s) { + return s == null || s.trim().length() == 0; + } + + /** + * Used to format two arguments as fixed length message. + */ + private class Alternate implements java.util.Formattable { + + /** + * The left side of the output. + */ + private final String left; + /** + * The right side of the output. + */ + private final String right; + + /** + * Creates an alternate output. + * + * @param left the left side or null. + * @param right the right side or null. + */ + Alternate(final String left, final String right) { + this.left = String.valueOf(left); + this.right = String.valueOf(right); + } + + @SuppressWarnings("override") //JDK-6954234 + public void formatTo(java.util.Formatter formatter, int flags, + int width, int precision) { + + String l = left; + String r = right; + if ((flags & java.util.FormattableFlags.UPPERCASE) + == java.util.FormattableFlags.UPPERCASE) { + l = l.toUpperCase(formatter.locale()); + r = r.toUpperCase(formatter.locale()); + } + + if ((flags & java.util.FormattableFlags.ALTERNATE) + == java.util.FormattableFlags.ALTERNATE) { + l = toAlternate(l); + r = toAlternate(r); + } + + if (precision <= 0) { + precision = Integer.MAX_VALUE; + } + + int fence = Math.min(l.length(), precision); + if (fence > (precision >> 1)) { + fence = Math.max(fence - r.length(), fence >> 1); + } + + if (fence > 0) { + if (fence > l.length() + && Character.isHighSurrogate(l.charAt(fence - 1))) { + --fence; + } + l = l.substring(0, fence); + } + r = r.substring(0, Math.min(precision - fence, r.length())); + + if (width > 0) { + final int half = width >> 1; + if (l.length() < half) { + l = pad(flags, l, half); + } + + if (r.length() < half) { + r = pad(flags, r, half); + } + } + + Object[] empty = Collections.emptySet().toArray(); + formatter.format(l, empty); + if (l.length() != 0 && r.length() != 0) { + formatter.format("|", empty); + } + formatter.format(r, empty); + } + + /** + * Pad the given input string. + * + * @param flags the formatter flags. + * @param s the string to pad. + * @param length the final string length. + * @return the padded string. + */ + private String pad(int flags, String s, int length) { + final int padding = length - s.length(); + final StringBuilder b = new StringBuilder(length); + if ((flags & java.util.FormattableFlags.LEFT_JUSTIFY) + == java.util.FormattableFlags.LEFT_JUSTIFY) { + for (int i = 0; i < padding; ++i) { + b.append('\u0020'); + } + b.append(s); + } else { + b.append(s); + for (int i = 0; i < padding; ++i) { + b.append('\u0020'); + } + } + return b.toString(); + } + } +}
diff --git a/mail/src/main/java/com/sun/mail/util/logging/DurationFilter.java b/mail/src/main/java/com/sun/mail/util/logging/DurationFilter.java new file mode 100644 index 0000000..d868229 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/util/logging/DurationFilter.java
@@ -0,0 +1,444 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2018 Jason Mehrens. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package com.sun.mail.util.logging; + +import static com.sun.mail.util.logging.LogManagerProperties.fromLogManager; +import java.util.logging.*; + +/** + * A filter used to limit log records based on a maximum generation rate. + * + * The duration specified is used to compute the record rate and the amount of + * time the filter will reject records once the rate has been exceeded. Once the + * rate is exceeded records are not allowed until the duration has elapsed. + * + * <p> + * By default each {@code DurationFilter} is initialized using the following + * LogManager configuration properties where {@code <filter-name>} refers to the + * fully qualified class name of the handler. If properties are not defined, or + * contain invalid values, then the specified default values are used. + * + * <ul> + * <li>{@literal <filter-name>}.records the max number of records per duration. + * A numeric long integer or a multiplication expression can be used as the + * value. (defaults to {@code 1000}) + * + * <li>{@literal <filter-name>}.duration the number of milliseconds to suppress + * log records from being published. This is also used as duration to determine + * the log record rate. A numeric long integer or a multiplication expression + * can be used as the value. If the {@code java.time} package is available then + * an ISO-8601 duration format of {@code PnDTnHnMn.nS} can be used as the value. + * The suffixes of "D", "H", "M" and "S" are for days, hours, minutes and + * seconds. The suffixes must occur in order. The seconds can be specified with + * a fractional component to declare milliseconds. (defaults to {@code PT15M}) + * </ul> + * + * <p> + * For example, the settings to limit {@code MailHandler} with a default + * capacity to only send a maximum of two email messages every six minutes would + * be as follows: + * <pre> + * {@code + * com.sun.mail.util.logging.MailHandler.filter = com.sun.mail.util.logging.DurationFilter + * com.sun.mail.util.logging.MailHandler.capacity = 1000 + * com.sun.mail.util.logging.DurationFilter.records = 2L * 1000L + * com.sun.mail.util.logging.DurationFilter.duration = PT6M + * } + * </pre> + * + * + * @author Jason Mehrens + * @since JavaMail 1.5.5 + */ +public class DurationFilter implements Filter { + + /** + * The number of expected records per duration. + */ + private final long records; + /** + * The duration in milliseconds used to determine the rate. The duration is + * also used as the amount of time that the filter will not allow records + * when saturated. + */ + private final long duration; + /** + * The number of records seen for the current duration. This value negative + * if saturated. Zero is considered saturated but is reserved for recording + * the first duration. + */ + private long count; + /** + * The most recent record time seen for the current duration. + */ + private long peak; + /** + * The start time for the current duration. + */ + private long start; + + /** + * Creates the filter using the default properties. + */ + public DurationFilter() { + this.records = checkRecords(initLong(".records")); + this.duration = checkDuration(initLong(".duration")); + } + + /** + * Creates the filter using the given properties. Default values are used if + * any of the given values are outside the allowed range. + * + * @param records the number of records per duration. + * @param duration the number of milliseconds to suppress log records from + * being published. + */ + public DurationFilter(final long records, final long duration) { + this.records = checkRecords(records); + this.duration = checkDuration(duration); + } + + /** + * Determines if this filter is equal to another filter. + * + * @param obj the given object. + * @return true if equal otherwise false. + */ + @Override + public boolean equals(final Object obj) { + if (this == obj) { //Avoid locks and deal with rapid state changes. + return true; + } + + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + final DurationFilter other = (DurationFilter) obj; + if (this.records != other.records) { + return false; + } + + if (this.duration != other.duration) { + return false; + } + + final long c; + final long p; + final long s; + synchronized (this) { + c = this.count; + p = this.peak; + s = this.start; + } + + synchronized (other) { + if (c != other.count || p != other.peak || s != other.start) { + return false; + } + } + return true; + } + + /** + * Determines if this filter is able to accept the maximum number of log + * records for this instant in time. The result is a best-effort estimate + * and should be considered out of date as soon as it is produced. This + * method is designed for use in monitoring the state of this filter. + * + * @return true if the filter is idle; false otherwise. + */ + public boolean isIdle() { + return test(0L, System.currentTimeMillis()); + } + + /** + * Returns a hash code value for this filter. + * + * @return hash code for this filter. + */ + @Override + public int hashCode() { + int hash = 3; + hash = 89 * hash + (int) (this.records ^ (this.records >>> 32)); + hash = 89 * hash + (int) (this.duration ^ (this.duration >>> 32)); + return hash; + } + + /** + * Check if the given log record should be published. This method will + * modify the internal state of this filter. + * + * @param record the log record to check. + * @return true if allowed; false otherwise. + * @throws NullPointerException if given record is null. + */ + @SuppressWarnings("override") //JDK-6954234 + public boolean isLoggable(final LogRecord record) { + return accept(record.getMillis()); + } + + /** + * Determines if this filter will accept log records for this instant in + * time. The result is a best-effort estimate and should be considered out + * of date as soon as it is produced. This method is designed for use in + * monitoring the state of this filter. + * + * @return true if the filter is not saturated; false otherwise. + */ + public boolean isLoggable() { + return test(records, System.currentTimeMillis()); + } + + /** + * Returns a string representation of this filter. The result is a + * best-effort estimate and should be considered out of date as soon as it + * is produced. + * + * @return a string representation of this filter. + */ + @Override + public String toString() { + boolean idle; + boolean loggable; + synchronized (this) { + final long millis = System.currentTimeMillis(); + idle = test(0L, millis); + loggable = test(records, millis); + } + + return getClass().getName() + "{records=" + records + + ", duration=" + duration + + ", idle=" + idle + + ", loggable=" + loggable + '}'; + } + + /** + * Creates a copy of this filter that retains the filter settings but does + * not include the current filter state. The newly create clone acts as if + * it has never seen any records. + * + * @return a copy of this filter. + * @throws CloneNotSupportedException if this filter is not allowed to be + * cloned. + */ + @Override + protected DurationFilter clone() throws CloneNotSupportedException { + final DurationFilter clone = (DurationFilter) super.clone(); + clone.count = 0L; //Reset the filter state. + clone.peak = 0L; + clone.start = 0L; + return clone; + } + + /** + * Checks if this filter is not saturated or bellow a maximum rate. + * + * @param limit the number of records allowed to be under the rate. + * @param millis the current time in milliseconds. + * @return true if not saturated or bellow the rate. + */ + private boolean test(final long limit, final long millis) { + assert limit >= 0L : limit; + final long c; + final long s; + synchronized (this) { + c = count; + s = start; + } + + if (c > 0L) { //If not saturated. + if ((millis - s) >= duration || c < limit) { + return true; + } + } else { //Subtraction is used to deal with numeric overflow. + if ((millis - s) >= 0L || c == 0L) { + return true; + } + } + return false; + } + + /** + * Determines if the record is loggable by time. + * + * @param millis the log record milliseconds. + * @return true if accepted false otherwise. + */ + private synchronized boolean accept(final long millis) { + //Subtraction is used to deal with numeric overflow of millis. + boolean allow; + if (count > 0L) { //If not saturated. + if ((millis - peak) > 0L) { + peak = millis; //Record the new peak. + } + + //Under the rate if the count has not been reached. + if (count != records) { + ++count; + allow = true; + } else { + if ((peak - start) >= duration) { + count = 1L; //Start a new duration. + start = peak; + allow = true; + } else { + count = -1L; //Saturate for the duration. + start = peak + duration; + allow = false; + } + } + } else { + //If the saturation period has expired or this is the first record + //then start a new duration and allow records. + if ((millis - start) >= 0L || count == 0L) { + count = 1L; + start = millis; + peak = millis; + allow = true; + } else { + allow = false; //Remain in a saturated state. + } + } + return allow; + } + + /** + * Reads a long value or multiplication expression from the LogManager. If + * the value can not be parsed or is not defined then Long.MIN_VALUE is + * returned. + * + * @param suffix a dot character followed by the key name. + * @return a long value or Long.MIN_VALUE if unable to parse or undefined. + * @throws NullPointerException if suffix is null. + */ + private long initLong(final String suffix) { + long result = 0L; + final String p = getClass().getName(); + String value = fromLogManager(p.concat(suffix)); + if (value != null && value.length() != 0) { + value = value.trim(); + if (isTimeEntry(suffix, value)) { + try { + result = LogManagerProperties.parseDurationToMillis(value); + } catch (final RuntimeException ignore) { + } catch (final Exception ignore) { + } catch (final LinkageError ignore) { + } + } + + if (result == 0L) { //Zero is invalid. + try { + result = 1L; + for (String s : tokenizeLongs(value)) { + if (s.endsWith("L") || s.endsWith("l")) { + s = s.substring(0, s.length() - 1); + } + result = multiplyExact(result, Long.parseLong(s)); + } + } catch (final RuntimeException ignore) { + result = Long.MIN_VALUE; + } + } + } else { + result = Long.MIN_VALUE; + } + return result; + } + + /** + * Determines if the given suffix can be a time unit and the value is + * encoded as an ISO ISO-8601 duration format. + * + * @param suffix the suffix property. + * @param value the value of the property. + * @return true if the entry is a time entry. + * @throws IndexOutOfBoundsException if value is empty. + * @throws NullPointerException if either argument is null. + */ + private boolean isTimeEntry(final String suffix, final String value) { + return (value.charAt(0) == 'P' || value.charAt(0) == 'p') + && suffix.equals(".duration"); + } + + /** + * Parse any long value or multiplication expressions into tokens. + * + * @param value the expression or value. + * @return an array of long tokens, never empty. + * @throws NullPointerException if the given value is null. + * @throws NumberFormatException if the expression is invalid. + */ + private static String[] tokenizeLongs(final String value) { + String[] e; + final int i = value.indexOf('*'); + if (i > -1 && (e = value.split("\\s*\\*\\s*")).length != 0) { + if (i == 0 || value.charAt(value.length() - 1) == '*') { + throw new NumberFormatException(value); + } + + if (e.length == 1) { + throw new NumberFormatException(e[0]); + } + } else { + e = new String[]{value}; + } + return e; + } + + /** + * Multiply and check for overflow. This can be replaced with + * {@code java.lang.Math.multiplyExact} when JavaMail requires JDK 8. + * + * @param x the first value. + * @param y the second value. + * @return x times y. + * @throws ArithmeticException if overflow is detected. + */ + private static long multiplyExact(final long x, final long y) { + long r = x * y; + if (((Math.abs(x) | Math.abs(y)) >>> 31L != 0L)) { + if (((y != 0L) && (r / y != x)) + || (x == Long.MIN_VALUE && y == -1L)) { + throw new ArithmeticException(); + } + } + return r; + } + + /** + * Converts record count to a valid record count. If the value is out of + * bounds then the default record count is used. + * + * @param records the record count. + * @return a valid number of record count. + */ + private static long checkRecords(final long records) { + return records > 0L ? records : 1000L; + } + + /** + * Converts the duration to a valid duration. If the value is out of bounds + * then the default duration is used. + * + * @param duration the duration to check. + * @return a valid duration. + */ + private static long checkDuration(final long duration) { + return duration > 0L ? duration : 15L * 60L * 1000L; + } +}
diff --git a/mail/src/main/java/com/sun/mail/util/logging/LogManagerProperties.java b/mail/src/main/java/com/sun/mail/util/logging/LogManagerProperties.java new file mode 100644 index 0000000..b20a8f2 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/util/logging/LogManagerProperties.java
@@ -0,0 +1,1090 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2009, 2018 Jason Mehrens. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package com.sun.mail.util.logging; + +import java.io.*; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.UndeclaredThrowableException; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.*; +import java.util.logging.*; +import java.util.logging.Formatter; + +/** + * An adapter class to allow the Mail API to access the LogManager properties. + * The LogManager properties are treated as the root of all properties. First, + * the parent properties are searched. If no value is found, then, the + * LogManager is searched with prefix value. If not found, then, just the key + * itself is searched in the LogManager. If a value is found in the LogManager + * it is then copied to this properties object with no key prefix. If no value + * is found in the LogManager or the parent properties, then this properties + * object is searched only by passing the key value. + * + * <p> + * This class also emulates the LogManager functions for creating new objects + * from string class names. This is to support initial setup of objects such as + * log filters, formatters, error managers, etc. + * + * <p> + * This class should never be exposed outside of this package. Keep this class + * package private (default access). + * + * @author Jason Mehrens + * @since JavaMail 1.4.3 + */ +final class LogManagerProperties extends Properties { + + /** + * Generated serial id. + */ + private static final long serialVersionUID = -2239983349056806252L; + /** + * Holds the method used to get the LogRecord instant if running on JDK 9 or + * later. + */ + private static final Method LR_GET_INSTANT; + + /** + * Holds the method used to get the default time zone if running on JDK 9 or + * later. + */ + private static final Method ZI_SYSTEM_DEFAULT; + + /** + * Holds the method used to convert and instant to a zoned date time if + * running on JDK 9 later. + */ + private static final Method ZDT_OF_INSTANT; + + static { + Method lrgi = null; + Method zisd = null; + Method zdtoi = null; + try { + lrgi = LogRecord.class.getMethod("getInstant"); + assert Comparable.class + .isAssignableFrom(lrgi.getReturnType()) : lrgi; + zisd = findClass("java.time.ZoneId") + .getMethod("systemDefault"); + if (!Modifier.isStatic(zisd.getModifiers())) { + throw new NoSuchMethodException(zisd.toString()); + } + + zdtoi = findClass("java.time.ZonedDateTime") + .getMethod("ofInstant", findClass("java.time.Instant"), + findClass("java.time.ZoneId")); + if (!Modifier.isStatic(zdtoi.getModifiers())) { + throw new NoSuchMethodException(zdtoi.toString()); + } + + if (!Comparable.class.isAssignableFrom(zdtoi.getReturnType())) { + throw new NoSuchMethodException(zdtoi.toString()); + } + } catch (final RuntimeException ignore) { + } catch (final Exception ignore) { //No need for specific catch. + } catch (final LinkageError ignore) { + } finally { + if (lrgi == null || zisd == null || zdtoi == null) { + lrgi = null; //If any are null then clear all. + zisd = null; + zdtoi = null; + } + } + + LR_GET_INSTANT = lrgi; + ZI_SYSTEM_DEFAULT = zisd; + ZDT_OF_INSTANT = zdtoi; + } + /** + * Caches the read only reflection class names string array. Declared + * volatile for safe publishing only. The VO_VOLATILE_REFERENCE_TO_ARRAY + * warning is a false positive. + */ + @SuppressWarnings("VolatileArrayField") + private static volatile String[] REFLECT_NAMES; + /** + * Caches the LogManager or Properties so we only read the configuration + * once. + */ + private static final Object LOG_MANAGER = loadLogManager(); + + /** + * Get the LogManager or loads a Properties object to use as the LogManager. + * + * @return the LogManager or a loaded Properties object. + * @since JavaMail 1.5.3 + */ + private static Object loadLogManager() { + Object m; + try { + m = LogManager.getLogManager(); + } catch (final LinkageError restricted) { + m = readConfiguration(); + } catch (final RuntimeException unexpected) { + m = readConfiguration(); + } + return m; + } + + /** + * Create a properties object from the default logging configuration file. + * Since the LogManager is not available in restricted environments, only + * the default configuration is applicable. + * + * @return a properties object loaded with the default configuration. + * @since JavaMail 1.5.3 + */ + private static Properties readConfiguration() { + /** + * Load the properties file so the default settings are available when + * user code creates a logging object. The class loader for the + * restricted LogManager can't access these classes to attach them to a + * logger or handler on startup. Creating logging objects at this point + * is both useless and risky. + */ + final Properties props = new Properties(); + try { + String n = System.getProperty("java.util.logging.config.file"); + if (n != null) { + final File f = new File(n).getCanonicalFile(); + final InputStream in = new FileInputStream(f); + try { + props.load(in); + } finally { + in.close(); + } + } + } catch (final RuntimeException permissionsOrMalformed) { + } catch (final Exception ioe) { + } catch (final LinkageError unexpected) { + } + return props; + } + + /** + * Gets LogManger property for the running JVM. If the LogManager doesn't + * exist then the default LogManger properties are used. + * + * @param name the property name. + * @return the LogManager. + * @throws NullPointerException if the given name is null. + * @since JavaMail 1.5.3 + */ + static String fromLogManager(final String name) { + if (name == null) { + throw new NullPointerException(); + } + + final Object m = LOG_MANAGER; + try { + if (m instanceof Properties) { + return ((Properties) m).getProperty(name); + } + } catch (final RuntimeException unexpected) { + } + + if (m != null) { + try { + if (m instanceof LogManager) { + return ((LogManager) m).getProperty(name); + } + } catch (final LinkageError restricted) { + } catch (final RuntimeException unexpected) { + } + } + return null; + } + + /** + * Check that the current context is trusted to modify the logging + * configuration. This requires LoggingPermission("control"). + * @throws SecurityException if a security manager exists and the caller + * does not have {@code LoggingPermission("control")}. + * @since JavaMail 1.5.3 + */ + static void checkLogManagerAccess() { + boolean checked = false; + final Object m = LOG_MANAGER; + if (m != null) { + try { + if (m instanceof LogManager) { + checked = true; + ((LogManager) m).checkAccess(); + } + } catch (final SecurityException notAllowed) { + if (checked) { + throw notAllowed; + } + } catch (final LinkageError restricted) { + } catch (final RuntimeException unexpected) { + } + } + + if (!checked) { + checkLoggingAccess(); + } + } + + /** + * Check that the current context is trusted to modify the logging + * configuration when the LogManager is not present. This requires + * LoggingPermission("control"). + * @throws SecurityException if a security manager exists and the caller + * does not have {@code LoggingPermission("control")}. + * @since JavaMail 1.5.3 + */ + private static void checkLoggingAccess() { + /** + * Some environments selectively enforce logging permissions by allowing + * access to loggers but not allowing access to handlers. This is an + * indirect way of checking for LoggingPermission when the LogManager is + * not present. The root logger will lazy create handlers so the global + * logger is used instead as it is a known named logger with well + * defined behavior. If the global logger is a subclass then fallback to + * using the SecurityManager. + */ + boolean checked = false; + final Logger global = Logger.getLogger("global"); + try { + if (Logger.class == global.getClass()) { + global.removeHandler((Handler) null); + checked = true; + } + } catch (final NullPointerException unexpected) { + } + + if (!checked) { + final SecurityManager sm = System.getSecurityManager(); + if (sm != null) { + sm.checkPermission(new LoggingPermission("control", null)); + } + } + } + + /** + * Determines if access to the {@code java.util.logging.LogManager} class is + * restricted by the class loader. + * + * @return true if a LogManager is present. + * @since JavaMail 1.5.3 + */ + static boolean hasLogManager() { + final Object m = LOG_MANAGER; + return m != null && !(m instanceof Properties); + } + + /** + * Gets the ZonedDateTime from the given log record. + * + * @param record used to generate the zoned date time. + * @return null if LogRecord doesn't support nanoseconds otherwise a new + * zoned date time is returned. + * @throws NullPointerException if record is null. + * @since JavaMail 1.5.6 + */ + @SuppressWarnings("UseSpecificCatch") + static Comparable<?> getZonedDateTime(LogRecord record) { + if (record == null) { + throw new NullPointerException(); + } + final Method m = ZDT_OF_INSTANT; + if (m != null) { + try { + return (Comparable<?>) m.invoke((Object) null, + LR_GET_INSTANT.invoke(record), + ZI_SYSTEM_DEFAULT.invoke((Object) null)); + } catch (final RuntimeException ignore) { + assert LR_GET_INSTANT != null + && ZI_SYSTEM_DEFAULT != null : ignore; + } catch (final InvocationTargetException ite) { + final Throwable cause = ite.getCause(); + if (cause instanceof Error) { + throw (Error) cause; + } else if (cause instanceof RuntimeException) { + throw (RuntimeException) cause; + } else { //Should never happen. + throw new UndeclaredThrowableException(ite); + } + } catch (final Exception ignore) { + } + } + return null; + } + + /** + * Gets the local host name from the given service. + * + * @param s the service to examine. + * @return the local host name or null. + * @throws IllegalAccessException if the method is inaccessible. + * @throws InvocationTargetException if the method throws an exception. + * @throws LinkageError if the linkage fails. + * @throws NullPointerException if the given service is null. + * @throws ExceptionInInitializerError if the static initializer fails. + * @throws Exception if there is a problem. + * @throws NoSuchMethodException if the given service does not have a method + * to get the local host name as a string. + * @throws SecurityException if unable to inspect properties of object. + * @since JavaMail 1.5.3 + */ + static String getLocalHost(final Object s) throws Exception { + try { + final Method m = s.getClass().getMethod("getLocalHost"); + if (!Modifier.isStatic(m.getModifiers()) + && m.getReturnType() == String.class) { + return (String) m.invoke(s); + } else { + throw new NoSuchMethodException(m.toString()); + } + } catch (final ExceptionInInitializerError EIIE) { + throw wrapOrThrow(EIIE); + } catch (final InvocationTargetException ite) { + throw paramOrError(ite); + } + } + + /** + * Used to parse an ISO-8601 duration format of {@code PnDTnHnMn.nS}. + * + * @param value an ISO-8601 duration character sequence. + * @return the number of milliseconds parsed from the duration. + * @throws ClassNotFoundException if the java.time classes are not present. + * @throws IllegalAccessException if the method is inaccessible. + * @throws InvocationTargetException if the method throws an exception. + * @throws LinkageError if the linkage fails. + * @throws NullPointerException if the given duration is null. + * @throws ExceptionInInitializerError if the static initializer fails. + * @throws Exception if there is a problem. + * @throws NoSuchMethodException if the correct time methods are missing. + * @throws SecurityException if reflective access to the java.time classes + * are not allowed. + * @since JavaMail 1.5.5 + */ + static long parseDurationToMillis(final CharSequence value) throws Exception { + try { + final Class<?> k = findClass("java.time.Duration"); + final Method parse = k.getMethod("parse", CharSequence.class); + if (!k.isAssignableFrom(parse.getReturnType()) + || !Modifier.isStatic(parse.getModifiers())) { + throw new NoSuchMethodException(parse.toString()); + } + + final Method toMillis = k.getMethod("toMillis"); + if (!Long.TYPE.isAssignableFrom(toMillis.getReturnType()) + || Modifier.isStatic(toMillis.getModifiers())) { + throw new NoSuchMethodException(toMillis.toString()); + } + return (Long) toMillis.invoke(parse.invoke(null, value)); + } catch (final ExceptionInInitializerError EIIE) { + throw wrapOrThrow(EIIE); + } catch (final InvocationTargetException ite) { + throw paramOrError(ite); + } + } + + /** + * Converts a locale to a language tag. + * + * @param locale the locale to convert. + * @return the language tag. + * @throws NullPointerException if the given locale is null. + * @since JavaMail 1.4.5 + */ + static String toLanguageTag(final Locale locale) { + final String l = locale.getLanguage(); + final String c = locale.getCountry(); + final String v = locale.getVariant(); + final char[] b = new char[l.length() + c.length() + v.length() + 2]; + int count = l.length(); + l.getChars(0, count, b, 0); + if (c.length() != 0 || (l.length() != 0 && v.length() != 0)) { + b[count] = '-'; + ++count; //be nice to the client compiler. + c.getChars(0, c.length(), b, count); + count += c.length(); + } + + if (v.length() != 0 && (l.length() != 0 || c.length() != 0)) { + b[count] = '-'; + ++count; //be nice to the client compiler. + v.getChars(0, v.length(), b, count); + count += v.length(); + } + return String.valueOf(b, 0, count); + } + + /** + * Creates a new filter from the given class name. + * + * @param name the fully qualified class name. + * @return a new filter. + * @throws ClassCastException if class name does not match the type. + * @throws ClassNotFoundException if the class name was not found. + * @throws IllegalAccessException if the constructor is inaccessible. + * @throws InstantiationException if the given class name is abstract. + * @throws InvocationTargetException if the constructor throws an exception. + * @throws LinkageError if the linkage fails. + * @throws ExceptionInInitializerError if the static initializer fails. + * @throws Exception to match the error method of the ErrorManager. + * @throws NoSuchMethodException if the class name does not have a no + * argument constructor. + * @since JavaMail 1.4.5 + */ + static Filter newFilter(String name) throws Exception { + return newObjectFrom(name, Filter.class); + } + + /** + * Creates a new formatter from the given class name. + * + * @param name the fully qualified class name. + * @return a new formatter. + * @throws ClassCastException if class name does not match the type. + * @throws ClassNotFoundException if the class name was not found. + * @throws IllegalAccessException if the constructor is inaccessible. + * @throws InstantiationException if the given class name is abstract. + * @throws InvocationTargetException if the constructor throws an exception. + * @throws LinkageError if the linkage fails. + * @throws ExceptionInInitializerError if the static initializer fails. + * @throws Exception to match the error method of the ErrorManager. + * @throws NoSuchMethodException if the class name does not have a no + * argument constructor. + * @since JavaMail 1.4.5 + */ + static Formatter newFormatter(String name) throws Exception { + return newObjectFrom(name, Formatter.class); + } + + /** + * Creates a new log record comparator from the given class name. + * + * @param name the fully qualified class name. + * @return a new comparator. + * @throws ClassCastException if class name does not match the type. + * @throws ClassNotFoundException if the class name was not found. + * @throws IllegalAccessException if the constructor is inaccessible. + * @throws InstantiationException if the given class name is abstract. + * @throws InvocationTargetException if the constructor throws an exception. + * @throws LinkageError if the linkage fails. + * @throws ExceptionInInitializerError if the static initializer fails. + * @throws Exception to match the error method of the ErrorManager. + * @throws NoSuchMethodException if the class name does not have a no + * argument constructor. + * @since JavaMail 1.4.5 + * @see java.util.logging.LogRecord + */ + @SuppressWarnings("unchecked") + static Comparator<? super LogRecord> newComparator(String name) throws Exception { + return newObjectFrom(name, Comparator.class); + } + + /** + * Returns a comparator that imposes the reverse ordering of the specified + * {@link Comparator}. If the given comparator declares a public + * reverseOrder method that method is called first and the return value is + * used. If that method is not declared or the caller does not have access + * then a comparator wrapping the given comparator is returned. + * + * @param <T> the element type to be compared + * @param c a comparator whose ordering is to be reversed by the returned + * comparator + * @return A comparator that imposes the reverse ordering of the specified + * comparator. + * @throws NullPointerException if the given comparator is null. + * @since JavaMail 1.5.0 + */ + @SuppressWarnings({"unchecked", "ThrowableResultIgnored"}) + static <T> Comparator<T> reverseOrder(final Comparator<T> c) { + if (c == null) { + throw new NullPointerException(); + } + + Comparator<T> reverse = null; + //Comparator in Java 1.8 has 'reversed' as a default method. + //This code calls that method first to allow custom + //code to define what reverse order means. + try { + //assert Modifier.isPublic(c.getClass().getModifiers()) : + // Modifier.toString(c.getClass().getModifiers()); + final Method m = c.getClass().getMethod("reversed"); + if (!Modifier.isStatic(m.getModifiers()) + && Comparator.class.isAssignableFrom(m.getReturnType())) { + try { + reverse = (Comparator<T>) m.invoke(c); + } catch (final ExceptionInInitializerError eiie) { + throw wrapOrThrow(eiie); + } + } + } catch (final NoSuchMethodException ignore) { + } catch (final IllegalAccessException ignore) { + } catch (final RuntimeException ignore) { + } catch (final InvocationTargetException ite) { + paramOrError(ite); //Ignore invocation bugs (returned values). + } + + if (reverse == null) { + reverse = Collections.reverseOrder(c); + } + return reverse; + } + + /** + * Creates a new error manager from the given class name. + * + * @param name the fully qualified class name. + * @return a new error manager. + * @throws ClassCastException if class name does not match the type. + * @throws ClassNotFoundException if the class name was not found. + * @throws IllegalAccessException if the constructor is inaccessible. + * @throws InstantiationException if the given class name is abstract. + * @throws InvocationTargetException if the constructor throws an exception. + * @throws LinkageError if the linkage fails. + * @throws ExceptionInInitializerError if the static initializer fails. + * @throws Exception to match the error method of the ErrorManager. + * @throws NoSuchMethodException if the class name does not have a no + * argument constructor. + * @since JavaMail 1.4.5 + */ + static ErrorManager newErrorManager(String name) throws Exception { + return newObjectFrom(name, ErrorManager.class); + } + + /** + * Determines if the given class name identifies a utility class. + * + * @param name the fully qualified class name. + * @return true if the given class name + * @throws ClassNotFoundException if the class name was not found. + * @throws IllegalAccessException if the constructor is inaccessible. + * @throws LinkageError if the linkage fails. + * @throws ExceptionInInitializerError if the static initializer fails. + * @throws Exception to match the error method of the ErrorManager. + * @throws SecurityException if unable to inspect properties of class. + * @since JavaMail 1.5.2 + */ + static boolean isStaticUtilityClass(String name) throws Exception { + final Class<?> c = findClass(name); + final Class<?> obj = Object.class; + Method[] methods; + boolean util; + if (c != obj && (methods = c.getMethods()).length != 0) { + util = true; + for (Method m : methods) { + if (m.getDeclaringClass() != obj + && !Modifier.isStatic(m.getModifiers())) { + util = false; + break; + } + } + } else { + util = false; + } + return util; + } + + /** + * Determines if the given class name is a reflection class name responsible + * for invoking methods and or constructors. + * + * @param name the fully qualified class name. + * @return true if the given class name + * @throws ClassNotFoundException if the class name was not found. + * @throws IllegalAccessException if the constructor is inaccessible. + * @throws LinkageError if the linkage fails. + * @throws ExceptionInInitializerError if the static initializer fails. + * @throws Exception to match the error method of the ErrorManager. + * @throws SecurityException if unable to inspect properties of class. + * @since JavaMail 1.5.2 + */ + static boolean isReflectionClass(String name) throws Exception { + String[] names = REFLECT_NAMES; + if (names == null) { //Benign data race. + REFLECT_NAMES = names = reflectionClassNames(); + } + + for (String rf : names) { //The set of names is small. + if (name.equals(rf)) { + return true; + } + } + + findClass(name); //Fail late instead of normal return. + return false; + } + + /** + * Determines all of the reflection class names used to invoke methods. + * + * This method performs indirect and direct calls on a throwable to capture + * the standard class names and the implementation class names. + * + * @return a string array containing the fully qualified class names. + * @throws Exception if there is a problem. + */ + private static String[] reflectionClassNames() throws Exception { + final Class<?> thisClass = LogManagerProperties.class; + assert Modifier.isFinal(thisClass.getModifiers()) : thisClass; + try { + final HashSet<String> traces = new HashSet<>(); + Throwable t = Throwable.class.getConstructor().newInstance(); + for (StackTraceElement ste : t.getStackTrace()) { + if (!thisClass.getName().equals(ste.getClassName())) { + traces.add(ste.getClassName()); + } else { + break; + } + } + + Throwable.class.getMethod("fillInStackTrace").invoke(t); + for (StackTraceElement ste : t.getStackTrace()) { + if (!thisClass.getName().equals(ste.getClassName())) { + traces.add(ste.getClassName()); + } else { + break; + } + } + return traces.toArray(new String[traces.size()]); + } catch (final InvocationTargetException ITE) { + throw paramOrError(ITE); + } + } + + /** + * Creates a new object from the given class name. + * + * @param <T> The generic class type. + * @param name the fully qualified class name. + * @param type the assignable type for the given name. + * @return a new object assignable to the given type. + * @throws ClassCastException if class name does not match the type. + * @throws ClassNotFoundException if the class name was not found. + * @throws IllegalAccessException if the constructor is inaccessible. + * @throws InstantiationException if the given class name is abstract. + * @throws InvocationTargetException if the constructor throws an exception. + * @throws LinkageError if the linkage fails. + * @throws ExceptionInInitializerError if the static initializer fails. + * @throws Exception to match the error method of the ErrorManager. + * @throws NoSuchMethodException if the class name does not have a no + * argument constructor. + * @since JavaMail 1.4.5 + */ + static <T> T newObjectFrom(String name, Class<T> type) throws Exception { + try { + final Class<?> clazz = LogManagerProperties.findClass(name); + //This check avoids additional side effects when the name parameter + //is a literal name and not a class name. + if (type.isAssignableFrom(clazz)) { + try { + return type.cast(clazz.getConstructor().newInstance()); + } catch (final InvocationTargetException ITE) { + throw paramOrError(ITE); + } + } else { + throw new ClassCastException(clazz.getName() + + " cannot be cast to " + type.getName()); + } + } catch (final NoClassDefFoundError NCDFE) { + //No class def found can occur on filesystems that are + //case insensitive (BUG ID 6196068). In some cases, we allow class + //names or literal names, this code guards against the case where a + //literal name happens to match a class name in a different case. + //This is also a nice way to adapt this error for the error manager. + throw new ClassNotFoundException(NCDFE.toString(), NCDFE); + } catch (final ExceptionInInitializerError EIIE) { + throw wrapOrThrow(EIIE); + } + } + + /** + * Returns the given exception or throws the escaping cause. + * + * @param ite any invocation target. + * @return the exception. + * @throws VirtualMachineError if present as cause. + * @throws ThreadDeath if present as cause. + * @since JavaMail 1.4.5 + */ + private static Exception paramOrError(InvocationTargetException ite) { + final Throwable cause = ite.getCause(); + if (cause != null) { + //Bitwise inclusive OR produces tighter bytecode for instanceof + //and matches with multicatch syntax. + if (cause instanceof VirtualMachineError + | cause instanceof ThreadDeath) { + throw (Error) cause; + } + } + return ite; + } + + /** + * Throws the given error if the cause is an error otherwise the given error + * is wrapped. + * + * @param eiie the error. + * @return an InvocationTargetException. + * @since JavaMail 1.5.0 + */ + private static InvocationTargetException wrapOrThrow( + ExceptionInInitializerError eiie) { + //This linkage error will escape the constructor new instance call. + //If the cause is an error, rethrow to skip any error manager. + if (eiie.getCause() instanceof Error) { + throw eiie; + } else { + //Considered a bug in the code, wrap the error so it can be + //reported to the error manager. + return new InvocationTargetException(eiie); + } + } + + /** + * This code is modified from the LogManager, which explictly states + * searching the system class loader first, then the context class loader. + * There is resistance (compatibility) to change this behavior to simply + * searching the context class loader. + * + * @param name full class name + * @return the class. + * @throws LinkageError if the linkage fails. + * @throws ClassNotFoundException if the class name was not found. + * @throws ExceptionInInitializerError if static initializer fails. + */ + private static Class<?> findClass(String name) throws ClassNotFoundException { + ClassLoader[] loaders = getClassLoaders(); + assert loaders.length == 2 : loaders.length; + Class<?> clazz; + if (loaders[0] != null) { + try { + clazz = Class.forName(name, false, loaders[0]); + } catch (ClassNotFoundException tryContext) { + clazz = tryLoad(name, loaders[1]); + } + } else { + clazz = tryLoad(name, loaders[1]); + } + return clazz; + } + + /** + * Loads a class using the given loader or the class loader of this class. + * + * @param name the class name. + * @param l any class loader or null. + * @return the raw class. + * @throws ClassNotFoundException if not found. + */ + private static Class<?> tryLoad(String name, ClassLoader l) throws ClassNotFoundException { + if (l != null) { + return Class.forName(name, false, l); + } else { + return Class.forName(name); + } + } + + /** + * Gets the class loaders using elevated privileges. + * + * @return any array of class loaders. Indexes may be null. + */ + private static ClassLoader[] getClassLoaders() { + return AccessController.doPrivileged(new PrivilegedAction<ClassLoader[]>() { + + @SuppressWarnings("override") //JDK-6954234 + public ClassLoader[] run() { + final ClassLoader[] loaders = new ClassLoader[2]; + try { + loaders[0] = ClassLoader.getSystemClassLoader(); + } catch (SecurityException ignore) { + loaders[0] = null; + } + + try { + loaders[1] = Thread.currentThread().getContextClassLoader(); + } catch (SecurityException ignore) { + loaders[1] = null; + } + return loaders; + } + }); + } + /** + * The namespace prefix to search LogManager and defaults. + */ + private final String prefix; + + /** + * Creates a log manager properties object. + * + * @param parent the parent properties. + * @param prefix the namespace prefix. + * @throws NullPointerException if <tt>prefix</tt> or <tt>parent</tt> is + * <tt>null</tt>. + */ + LogManagerProperties(final Properties parent, final String prefix) { + super(parent); + if (parent == null || prefix == null) { + throw new NullPointerException(); + } + this.prefix = prefix; + } + + /** + * Returns a properties object that contains a snapshot of the current + * state. This method violates the clone contract so that no instances of + * LogManagerProperties is exported for public use. + * + * @return the snapshot. + * @since JavaMail 1.4.4 + */ + @Override + @SuppressWarnings("CloneDoesntCallSuperClone") + public synchronized Object clone() { + return exportCopy(defaults); + } + + /** + * Searches defaults, then searches the log manager if available or the + * system properties by the prefix property, and then by the key itself. + * + * @param key a non null key. + * @return the value for that key. + */ + @Override + public synchronized String getProperty(final String key) { + String value = defaults.getProperty(key); + if (value == null) { + if (key.length() > 0) { + value = fromLogManager(prefix + '.' + key); + } + + if (value == null) { + value = fromLogManager(key); + } + + /** + * Copy the log manager properties as we read them. If a value is no + * longer present in the LogManager read it from here. The reason + * this works is because LogManager.reset() closes all attached + * handlers therefore, stale values only exist in closed handlers. + */ + if (value != null) { + super.put(key, value); + } else { + Object v = super.get(key); //defaults are not used. + value = v instanceof String ? (String) v : null; + } + } + return value; + } + + /** + * Calls getProperty directly. If getProperty returns null the default value + * is returned. + * + * @param key a key to search for. + * @param def the default value to use if not found. + * @return the value for the key. + * @since JavaMail 1.4.4 + */ + @Override + public String getProperty(final String key, final String def) { + final String value = this.getProperty(key); + return value == null ? def : value; + } + + /** + * Required to work with PropUtil. Calls getProperty directly if the given + * key is a string. Otherwise, performs a get operation on the defaults + * followed by the normal hash table get. + * + * @param key any key. + * @return the value for the key or null. + * @since JavaMail 1.4.5 + */ + @Override + public synchronized Object get(final Object key) { + Object value; + if (key instanceof String) { + value = getProperty((String) key); + } else { + value = null; + } + + //Search for non-string value. + if (value == null) { + value = defaults.get(key); + if (value == null && !defaults.containsKey(key)) { + value = super.get(key); + } + } + return value; + } + + /** + * Required to work with PropUtil. An updated copy of the key is fetched + * from the log manager if the key doesn't exist in this properties. + * + * @param key any key. + * @return the value for the key or the default value for the key. + * @since JavaMail 1.4.5 + */ + @Override + public synchronized Object put(final Object key, final Object value) { + if (key instanceof String && value instanceof String) { + final Object def = preWrite(key); + final Object man = super.put(key, value); + return man == null ? def : man; + } else { + return super.put(key, value); + } + } + + /** + * Calls the put method directly. + * + * @param key any key. + * @return the value for the key or the default value for the key. + * @since JavaMail 1.4.5 + */ + @Override + public Object setProperty(String key, String value) { + return this.put(key, value); + } + + /** + * Required to work with PropUtil. An updated copy of the key is fetched + * from the log manager prior to returning. + * + * @param key any key. + * @return the value for the key or null. + * @since JavaMail 1.4.5 + */ + @Override + public synchronized boolean containsKey(final Object key) { + boolean found = key instanceof String + && getProperty((String) key) != null; + if (!found) { + found = defaults.containsKey(key) || super.containsKey(key); + } + return found; + } + + /** + * Required to work with PropUtil. An updated copy of the key is fetched + * from the log manager if the key doesn't exist in this properties. + * + * @param key any key. + * @return the value for the key or the default value for the key. + * @since JavaMail 1.4.5 + */ + @Override + public synchronized Object remove(final Object key) { + final Object def = preWrite(key); + final Object man = super.remove(key); + return man == null ? def : man; + } + + /** + * It is assumed that this method will never be called. No way to get the + * property names from LogManager. + * + * @return the property names + */ + @Override + public Enumeration<?> propertyNames() { + assert false; + return super.propertyNames(); + } + + /** + * It is assumed that this method will never be called. The prefix value is + * not used for the equals method. + * + * @param o any object or null. + * @return true if equal, otherwise false. + */ + @Override + public boolean equals(final Object o) { + if (o == null) { + return false; + } + if (o == this) { + return true; + } + if (o instanceof Properties == false) { + return false; + } + assert false : prefix; + return super.equals(o); + } + + /** + * It is assumed that this method will never be called. See equals. + * + * @return the hash code. + */ + @Override + public int hashCode() { + assert false : prefix.hashCode(); + return super.hashCode(); + } + + /** + * Called before a write operation of a key. Caches a key read from the log + * manager in this properties object. The key is only cached if it is an + * instance of a String and this properties doesn't contain a copy of the + * key. + * + * @param key the key to search. + * @return the default value for the key. + */ + private Object preWrite(final Object key) { + assert Thread.holdsLock(this); + return get(key); + } + + /** + * Creates a public snapshot of this properties object using the given + * parent properties. + * + * @param parent the defaults to use with the snapshot. + * @return the safe snapshot. + */ + private Properties exportCopy(final Properties parent) { + Thread.holdsLock(this); + final Properties child = new Properties(parent); + child.putAll(this); + return child; + } + + /** + * It is assumed that this method will never be called. We return a safe + * copy for export to avoid locking this properties object or the defaults + * during write. + * + * @return the parent properties. + * @throws ObjectStreamException if there is a problem. + */ + private synchronized Object writeReplace() throws ObjectStreamException { + assert false; + return exportCopy((Properties) defaults.clone()); + } +}
diff --git a/mail/src/main/java/com/sun/mail/util/logging/MailHandler.java b/mail/src/main/java/com/sun/mail/util/logging/MailHandler.java new file mode 100644 index 0000000..891e921 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/util/logging/MailHandler.java
@@ -0,0 +1,4396 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2009, 2018 Jason Mehrens. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util.logging; + +import static com.sun.mail.util.logging.LogManagerProperties.fromLogManager; +import java.io.*; +import java.lang.reflect.InvocationTargetException; +import java.net.InetAddress; +import java.net.URLConnection; +import java.net.UnknownHostException; +import java.nio.charset.Charset; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.*; +import java.util.logging.*; +import java.util.logging.Formatter; +import javax.activation.*; +import javax.mail.*; +import javax.mail.internet.*; +import javax.mail.util.ByteArrayDataSource; + +/** + * <tt>Handler</tt> that formats log records as an email message. + * + * <p> + * This <tt>Handler</tt> will store a fixed number of log records used to + * generate a single email message. When the internal buffer reaches capacity, + * all log records are formatted and placed in an email which is sent to an + * email server. The code to manually setup this handler can be as simple as + * the following: + * + * <pre> + * Properties props = new Properties(); + * props.put("mail.smtp.host", "my-mail-server"); + * props.put("mail.to", "me@example.com"); + * props.put("verify", "local"); + * MailHandler h = new MailHandler(props); + * h.setLevel(Level.WARNING); + * </pre> + * + * <p> + * <b>Configuration:</b> + * The LogManager should define at least one or more recipient addresses and a + * mail host for outgoing email. The code to setup this handler via the + * logging properties can be as simple as the following: + * + * <pre> + * #Default MailHandler settings. + * com.sun.mail.util.logging.MailHandler.mail.smtp.host = my-mail-server + * com.sun.mail.util.logging.MailHandler.mail.to = me@example.com + * com.sun.mail.util.logging.MailHandler.level = WARNING + * com.sun.mail.util.logging.MailHandler.verify = local + * </pre> + * + * For a custom handler, e.g. <tt>com.foo.MyHandler</tt>, the properties would + * be: + * + * <pre> + * #Subclass com.foo.MyHandler settings. + * com.foo.MyHandler.mail.smtp.host = my-mail-server + * com.foo.MyHandler.mail.to = me@example.com + * com.foo.MyHandler.level = WARNING + * com.foo.MyHandler.verify = local + * </pre> + * + * All mail properties documented in the <tt>Java Mail API</tt> cascade to the + * LogManager by prefixing a key using the fully qualified class name of this + * <tt>MailHandler</tt> or the fully qualified derived class name dot mail + * property. If the prefixed property is not found, then the mail property + * itself is searched in the LogManager. By default each <tt>MailHandler</tt> is + * initialized using the following LogManager configuration properties where + * <tt><handler-name></tt> refers to the fully qualified class name of the + * handler. If properties are not defined, or contain invalid values, then the + * specified default values are used. + * + * <ul> + * <li><handler-name>.attachment.filters a comma + * separated list of <tt>Filter</tt> class names used to create each attachment. + * The literal <tt>null</tt> is reserved for attachments that do not require + * filtering. (defaults to the + * {@linkplain java.util.logging.Handler#getFilter() body} filter) + * + * <li><handler-name>.attachment.formatters a comma + * separated list of <tt>Formatter</tt> class names used to create each + * attachment. (default is no attachments) + * + * <li><handler-name>.attachment.names a comma separated + * list of names or <tt>Formatter</tt> class names of each attachment. All + * control characters are removed from the attachment names. + * (default is {@linkplain java.util.logging.Formatter#toString() toString} + * of the attachment formatter) + * + * <li><handler-name>.authenticator name of an + * {@linkplain javax.mail.Authenticator} class used to provide login credentials + * to the email server or string literal that is the password used with the + * {@linkplain Authenticator#getDefaultUserName() default} user name. + * (default is <tt>null</tt>) + * + * <li><handler-name>.capacity the max number of + * <tt>LogRecord</tt> objects include in each email message. + * (defaults to <tt>1000</tt>) + * + * <li><handler-name>.comparator name of a + * {@linkplain java.util.Comparator} class used to sort the published + * <tt>LogRecord</tt> objects prior to all formatting. + * (defaults to <tt>null</tt> meaning records are unsorted). + * + * <li><handler-name>.comparator.reverse a boolean + * <tt>true</tt> to reverse the order of the specified comparator or + * <tt>false</tt> to retain the original order. (defaults to <tt>false</tt>) + * + * <li><handler-name>.encoding the name of the Java + * {@linkplain java.nio.charset.Charset#name() character set} to use for the + * email message. (defaults to <tt>null</tt>, the + * {@linkplain javax.mail.internet.MimeUtility#getDefaultJavaCharset() default} + * platform encoding). + * + * <li><handler-name>.errorManager name of an + * <tt>ErrorManager</tt> class used to handle any configuration or mail + * transport problems. (defaults to <tt>java.util.logging.ErrorManager</tt>) + * + * <li><handler-name>.filter name of a <tt>Filter</tt> + * class used for the body of the message. (defaults to <tt>null</tt>, + * allow all records) + * + * <li><handler-name>.formatter name of a + * <tt>Formatter</tt> class used to format the body of this message. + * (defaults to <tt>java.util.logging.SimpleFormatter</tt>) + * + * <li><handler-name>.level specifies the default level + * for this <tt>Handler</tt> (defaults to <tt>Level.WARNING</tt>). + * + * <li><handler-name>.mail.bcc a comma separated list of + * addresses which will be blind carbon copied. Typically, this is set to the + * recipients that may need to be privately notified of a log message or + * notified that a log message was sent to a third party such as a support team. + * The empty string can be used to specify no blind carbon copied address. + * (defaults to <tt>null</tt>, none) + * + * <li><handler-name>.mail.cc a comma separated list of + * addresses which will be carbon copied. Typically, this is set to the + * recipients that may need to be notified of a log message but, are not + * required to provide direct support. The empty string can be used to specify + * no carbon copied address. (defaults to <tt>null</tt>, none) + * + * <li><handler-name>.mail.from a comma separated list of + * addresses which will be from addresses. Typically, this is set to the email + * address identifying the user running the application. The empty string can + * be used to override the default behavior and specify no from address. + * (defaults to the {@linkplain javax.mail.Message#setFrom() local address}) + * + * <li><handler-name>.mail.host the host name or IP + * address of the email server. (defaults to <tt>null</tt>, use + * {@linkplain Transport#protocolConnect default} + * <tt>Java Mail</tt> behavior) + * + * <li><handler-name>.mail.reply.to a comma separated + * list of addresses which will be reply-to addresses. Typically, this is set + * to the recipients that provide support for the application itself. The empty + * string can be used to specify no reply-to address. + * (defaults to <tt>null</tt>, none) + * + * <li><handler-name>.mail.to a comma separated list of + * addresses which will be send-to addresses. Typically, this is set to the + * recipients that provide support for the application, system, and/or + * supporting infrastructure. The empty string can be used to specify no + * send-to address which overrides the default behavior. (defaults to + * {@linkplain javax.mail.internet.InternetAddress#getLocalAddress + * local address}.) + * + * <li><handler-name>.mail.sender a single address + * identifying sender of the email; never equal to the from address. Typically, + * this is set to the email address identifying the application itself. The + * empty string can be used to specify no sender address. + * (defaults to <tt>null</tt>, none) + * + * <li><handler-name>.subject the name of a + * <tt>Formatter</tt> class or string literal used to create the subject line. + * The empty string can be used to specify no subject. All control characters + * are removed from the subject line. (defaults to {@linkplain + * com.sun.mail.util.logging.CollectorFormatter CollectorFormatter}.) + * + * <li><handler-name>.pushFilter the name of a + * <tt>Filter</tt> class used to trigger an early push. + * (defaults to <tt>null</tt>, no early push) + * + * <li><handler-name>.pushLevel the level which will + * trigger an early push. (defaults to <tt>Level.OFF</tt>, only push when full) + * + * <li><handler-name>.verify <a name="verify">used</a> to + * verify the <tt>Handler</tt> configuration prior to a push. + * <ul> + * <li>If the value is not set, equal to an empty string, or equal to the + * literal <tt>null</tt> then no settings are verified prior to a push. + * <li>If set to a value of <tt>limited</tt> then the <tt>Handler</tt> will + * verify minimal local machine settings. + * <li>If set to a value of <tt>local</tt> the <tt>Handler</tt> will verify + * all of settings of the local machine. + * <li>If set to a value of <tt>resolve</tt>, the <tt>Handler</tt> will + * verify all local settings and try to resolve the remote host name with + * the domain name server. + * <li>If set to a value of <tt>login</tt>, the <tt>Handler</tt> will + * verify all local settings and try to establish a connection with + * the email server. + * <li>If set to a value of <tt>remote</tt>, the <tt>Handler</tt> will + * verify all local settings, try to establish a connection with the + * email server, and try to verify the envelope of the email message. + * </ul> + * If this <tt>Handler</tt> is only implicitly closed by the + * <tt>LogManager</tt>, then verification should be turned on. + * (defaults to <tt>null</tt>, no verify). + * </ul> + * + * <p> + * <b>Normalization:</b> + * The error manager, filters, and formatters when loaded from the LogManager + * are converted into canonical form inside the MailHandler. The pool of + * interned values is limited to each MailHandler object such that no two + * MailHandler objects created by the LogManager will be created sharing + * identical error managers, filters, or formatters. If a filter or formatter + * should <b>not</b> be interned then it is recommended to retain the identity + * equals and identity hashCode methods as the implementation. For a filter or + * formatter to be interned the class must implement the + * {@linkplain java.lang.Object#equals(java.lang.Object) equals} + * and {@linkplain java.lang.Object#hashCode() hashCode} methods. + * The recommended code to use for stateless filters and formatters is: + * <pre> + * public boolean equals(Object obj) { + * return obj == null ? false : obj.getClass() == getClass(); + * } + * + * public int hashCode() { + * return 31 * getClass().hashCode(); + * } + * </pre> + * + * <p> + * <b>Sorting:</b> + * All <tt>LogRecord</tt> objects are ordered prior to formatting if this + * <tt>Handler</tt> has a non null comparator. Developers might be interested + * in sorting the formatted email by thread id, time, and sequence properties + * of a <tt>LogRecord</tt>. Where as system administrators might be interested + * in sorting the formatted email by thrown, level, time, and sequence + * properties of a <tt>LogRecord</tt>. If comparator for this handler is + * <tt>null</tt> then the order is unspecified. + * + * <p> + * <b>Formatting:</b> + * The main message body is formatted using the <tt>Formatter</tt> returned by + * <tt>getFormatter()</tt>. Only records that pass the filter returned by + * <tt>getFilter()</tt> will be included in the message body. The subject + * <tt>Formatter</tt> will see all <tt>LogRecord</tt> objects that were + * published regardless of the current <tt>Filter</tt>. The MIME type of the + * message body can be {@linkplain FileTypeMap#setDefaultFileTypeMap overridden} + * by adding a MIME {@linkplain MimetypesFileTypeMap entry} using the simple + * class name of the body formatter as the file extension. The MIME type of the + * attachments can be overridden by changing the attachment file name extension + * or by editing the default MIME entry for a specific file name extension. + * + * <p> + * <b>Attachments:</b> + * This <tt>Handler</tt> allows multiple attachments per each email message. + * The presence of an attachment formatter will change the content type of the + * email message to a multi-part message. The attachment order maps directly to + * the array index order in this <tt>Handler</tt> with zero index being the + * first attachment. The number of attachment formatters controls the number of + * attachments per email and the content type of each attachment. The + * attachment filters determine if a <tt>LogRecord</tt> will be included in an + * attachment. If an attachment filter is <tt>null</tt> then all records are + * included for that attachment. Attachments without content will be omitted + * from email message. The attachment name formatters create the file name for + * an attachment. Custom attachment name formatters can be used to generate an + * attachment name based on the contents of the attachment. + * + * <p> + * <b>Push Level and Push Filter:</b> + * The push method, push level, and optional push filter can be used to + * conditionally trigger a push at or prior to full capacity. When a push + * occurs, the current buffer is formatted into an email and is sent to the + * email server. If the push method, push level, or push filter trigger a push + * then the outgoing email is flagged as high importance with urgent priority. + * + * <p> + * <b>Buffering:</b> + * Log records that are published are stored in an internal buffer. When this + * buffer reaches capacity the existing records are formatted and sent in an + * email. Any published records can be sent before reaching capacity by + * explictly calling the <tt>flush</tt>, <tt>push</tt>, or <tt>close</tt> + * methods. If a circular buffer is required then this handler can be wrapped + * with a {@linkplain java.util.logging.MemoryHandler} typically with an + * equivalent capacity, level, and push level. + * + * <p> + * <b>Error Handling:</b> + * If the transport of an email message fails, the email is converted to + * a {@linkplain javax.mail.internet.MimeMessage#writeTo raw} + * {@linkplain java.io.ByteArrayOutputStream#toString(java.lang.String) string} + * and is then passed as the <tt>msg</tt> parameter to + * {@linkplain Handler#reportError reportError} along with the exception + * describing the cause of the failure. This allows custom error managers to + * store, {@linkplain javax.mail.internet.MimeMessage#MimeMessage( + * javax.mail.Session, java.io.InputStream) reconstruct}, and resend the + * original MimeMessage. The message parameter string is <b>not</b> a raw email + * if it starts with value returned from <tt>Level.SEVERE.getName()</tt>. + * Custom error managers can use the following test to determine if the + * <tt>msg</tt> parameter from this handler is a raw email: + * + * <pre> + * public void error(String msg, Exception ex, int code) { + * if (msg == null || msg.length() == 0 || msg.startsWith(Level.SEVERE.getName())) { + * super.error(msg, ex, code); + * } else { + * //The 'msg' parameter is a raw email. + * } + * } + * </pre> + * + * @author Jason Mehrens + * @since JavaMail 1.4.3 + */ +public class MailHandler extends Handler { + /** + * Use the emptyFilterArray method. + */ + private static final Filter[] EMPTY_FILTERS = new Filter[0]; + /** + * Use the emptyFormatterArray method. + */ + private static final Formatter[] EMPTY_FORMATTERS = new Formatter[0]; + /** + * Min byte size for header data. Used for initial arrays sizing. + */ + private static final int MIN_HEADER_SIZE = 1024; + /** + * Cache the off value. + */ + private static final int offValue = Level.OFF.intValue(); + /** + * The action to set the context class loader for use with the JavaMail API. + * Load and pin this before it is loaded in the close method. The field is + * declared as java.security.PrivilegedAction so + * WebappClassLoader.clearReferencesStaticFinal() method will ignore this + * field. + */ + private static final PrivilegedAction<Object> MAILHANDLER_LOADER + = new GetAndSetContext(MailHandler.class); + /** + * A thread local mutex used to prevent logging loops. This code has to be + * prepared to deal with unexpected null values since the + * WebappClassLoader.clearReferencesThreadLocals() and + * InnocuousThread.eraseThreadLocals() can remove thread local values. + * The MUTEX has 5 states: + * 1. A null value meaning default state of not publishing. + * 2. MUTEX_PUBLISH on first entry of a push or publish. + * 3. The index of the first filter to accept a log record. + * 4. MUTEX_REPORT when cycle of records is detected. + * 5. MUTEXT_LINKAGE when a linkage error is reported. + */ + private static final ThreadLocal<Integer> MUTEX = new ThreadLocal<>(); + /** + * The marker object used to report a publishing state. + * This must be less than the body filter index (-1). + */ + private static final Integer MUTEX_PUBLISH = -2; + /** + * The used for the error reporting state. + * This must be less than the PUBLISH state. + */ + private static final Integer MUTEX_REPORT = -4; + /** + * The used for linkage error reporting. + * This must be less than the REPORT state. + */ + private static final Integer MUTEX_LINKAGE = -8; + /** + * Used to turn off security checks. + */ + private volatile boolean sealed; + /** + * Determines if we are inside of a push. + * Makes the handler properties read-only during a push. + */ + private boolean isWriting; + /** + * Holds all of the email server properties. + */ + private Properties mailProps; + /** + * Holds the authenticator required to login to the email server. + */ + private Authenticator auth; + /** + * Holds the session object used to generate emails. + * Sessions can be shared by multiple threads. + * See JDK-6228391 and K 6278. + */ + private Session session; + /** + * A mapping of log record to matching filter index. Negative one is used + * to track the body filter. Zero and greater is used to track the + * attachment parts. All indexes less than or equal to the matched value + * have already seen the given log record. + */ + private int[] matched; + /** + * Holds all of the log records that will be used to create the email. + */ + private LogRecord[] data; + /** + * The number of log records in the buffer. + */ + private int size; + /** + * The maximum number of log records to format per email. + * Used to roughly bound the size of an email. + * Every time the capacity is reached, the handler will push. + * The capacity will be negative if this handler is closed. + * Negative values are used to ensure all records are pushed. + */ + private int capacity; + /** + * Used to order all log records prior to formatting. The main email body + * and all attachments use the order determined by this comparator. If no + * comparator is present the log records will be in no specified order. + */ + private Comparator<? super LogRecord> comparator; + /** + * Holds the formatter used to create the subject line of the email. + * A subject formatter is not required for the email message. + * All published records pass through the subject formatter. + */ + private Formatter subjectFormatter; + /** + * Holds the push level for this handler. + * This is only required if an email must be sent prior to shutdown + * or before the buffer is full. + */ + private Level pushLevel; + /** + * Holds the push filter for trigger conditions requiring an early push. + * Only gets called if the given log record is greater than or equal + * to the push level and the push level is not Level.OFF. + */ + private Filter pushFilter; + /** + * Holds the entry and body filter for this handler. + * There is no way to un-seal the super handler. + */ + private volatile Filter filter; + /** + * Holds the level for this handler. + * There is no way to un-seal the super handler. + */ + private volatile Level logLevel = Level.ALL; + /** + * Holds the filters for each attachment. Filters are optional for + * each attachment. This is declared volatile because this is treated as + * copy-on-write. The VO_VOLATILE_REFERENCE_TO_ARRAY warning is a false + * positive. + */ + @SuppressWarnings("VolatileArrayField") + private volatile Filter[] attachmentFilters; + /** + * Holds the encoding name for this handler. + * There is no way to un-seal the super handler. + */ + private String encoding; + /** + * Holds the entry and body filter for this handler. + * There is no way to un-seal the super handler. + */ + private Formatter formatter; + /** + * Holds the formatters that create the content for each attachment. + * Each formatter maps directly to an attachment. The formatters + * getHead, format, and getTail methods are only called if one or more + * log records pass through the attachment filters. + */ + private Formatter[] attachmentFormatters; + /** + * Holds the formatters that create the file name for each attachment. + * Each formatter must produce a non null and non empty name. + * The final file name will be the concatenation of one getHead call, plus + * all of the format calls, plus one getTail call. + */ + private Formatter[] attachmentNames; + /** + * Used to override the content type for the body and set the content type + * for each attachment. + */ + private FileTypeMap contentTypes; + /** + * Holds the error manager for this handler. + * There is no way to un-seal the super handler. + */ + private volatile ErrorManager errorManager = defaultErrorManager(); + + /** + * Creates a <tt>MailHandler</tt> that is configured by the + * <tt>LogManager</tt> configuration properties. + * @throws SecurityException if a security manager exists and the + * caller does not have <tt>LoggingPermission("control")</tt>. + */ + public MailHandler() { + init((Properties) null); + sealed = true; + checkAccess(); + } + + /** + * Creates a <tt>MailHandler</tt> that is configured by the + * <tt>LogManager</tt> configuration properties but overrides the + * <tt>LogManager</tt> capacity with the given capacity. + * @param capacity of the internal buffer. + * @throws IllegalArgumentException if <tt>capacity</tt> less than one. + * @throws SecurityException if a security manager exists and the + * caller does not have <tt>LoggingPermission("control")</tt>. + */ + public MailHandler(final int capacity) { + init((Properties) null); + sealed = true; + setCapacity0(capacity); + } + + /** + * Creates a mail handler with the given mail properties. + * The key/value pairs are defined in the <tt>Java Mail API</tt> + * documentation. This <tt>Handler</tt> will also search the + * <tt>LogManager</tt> for defaults if needed. + * @param props a non <tt>null</tt> properties object. + * @throws NullPointerException if <tt>props</tt> is <tt>null</tt>. + * @throws SecurityException if a security manager exists and the + * caller does not have <tt>LoggingPermission("control")</tt>. + */ + public MailHandler(final Properties props) { + if (props == null) { + throw new NullPointerException(); + } + init(props); + sealed = true; + setMailProperties0(props); + } + + /** + * Check if this <tt>Handler</tt> would actually log a given + * <tt>LogRecord</tt> into its internal buffer. + * <p> + * This method checks if the <tt>LogRecord</tt> has an appropriate level and + * whether it satisfies any <tt>Filter</tt> including any attachment filters. + * However it does <b>not</b> check whether the <tt>LogRecord</tt> would + * result in a "push" of the buffer contents. + * <p> + * @param record a <tt>LogRecord</tt> + * @return true if the <tt>LogRecord</tt> would be logged. + */ + @Override + public boolean isLoggable(final LogRecord record) { + int levelValue = getLevel().intValue(); + if (record.getLevel().intValue() < levelValue || levelValue == offValue) { + return false; + } + + Filter body = getFilter(); + if (body == null || body.isLoggable(record)) { + setMatchedPart(-1); + return true; + } + + return isAttachmentLoggable(record); + } + + /** + * Stores a <tt>LogRecord</tt> in the internal buffer. + * <p> + * The <tt>isLoggable</tt> method is called to check if the given log record + * is loggable. If the given record is loggable, it is copied into + * an internal buffer. Then the record's level property is compared with + * the push level. If the given level of the <tt>LogRecord</tt> + * is greater than or equal to the push level then the push filter is + * called. If no push filter exists, the push filter returns true, + * or the capacity of the internal buffer has been reached then all buffered + * records are formatted into one email and sent to the server. + * + * @param record description of the log event. + */ + @Override + public void publish(final LogRecord record) { + /** + * It is possible for the handler to be closed after the + * call to isLoggable. In that case, the current thread + * will push to ensure that all published records are sent. + * See close(). + */ + + if (tryMutex()) { + try { + if (isLoggable(record)) { + record.getSourceMethodName(); //Infer caller. + publish0(record); + } + } catch (final LinkageError JDK8152515) { + reportLinkageError(JDK8152515, ErrorManager.WRITE_FAILURE); + } finally { + releaseMutex(); + } + } else { + reportUnPublishedError(record); + } + } + + /** + * Performs the publish after the record has been filtered. + * @param record the record. + * @since JavaMail 1.4.5 + */ + private void publish0(final LogRecord record) { + Message msg; + boolean priority; + synchronized (this) { + if (size == data.length && size < capacity) { + grow(); + } + + if (size < data.length) { + //assert data.length == matched.length; + matched[size] = getMatchedPart(); + data[size] = record; + ++size; //Be nice to client compiler. + priority = isPushable(record); + if (priority || size >= capacity) { + msg = writeLogRecords(ErrorManager.WRITE_FAILURE); + } else { + msg = null; + } + } else { + priority = false; + msg = null; + } + } + + if (msg != null) { + send(msg, priority, ErrorManager.WRITE_FAILURE); + } + } + + /** + * Report to the error manager that a logging loop was detected and + * we are going to break the cycle of messages. It is possible that + * a custom error manager could continue the cycle in which case + * we will stop trying to report errors. + * @param record the record or null. + * @since JavaMail 1.4.6 + */ + private void reportUnPublishedError(LogRecord record) { + final Integer idx = MUTEX.get(); + if (idx == null || idx > MUTEX_REPORT) { + MUTEX.set(MUTEX_REPORT); + try { + final String msg; + if (record != null) { + final Formatter f = createSimpleFormatter(); + msg = "Log record " + record.getSequenceNumber() + + " was not published. " + + head(f) + format(f, record) + tail(f, ""); + } else { + msg = null; + } + Exception e = new IllegalStateException( + "Recursive publish detected by thread " + + Thread.currentThread()); + reportError(msg, e, ErrorManager.WRITE_FAILURE); + } finally { + if (idx != null) { + MUTEX.set(idx); + } else { + MUTEX.remove(); + } + } + } + } + + /** + * Used to detect reentrance by the current thread to the publish method. + * This mutex is thread local scope and will not block other threads. + * The state is advanced on if the current thread is in a reset state. + * @return true if the mutex was acquired. + * @since JavaMail 1.4.6 + */ + private boolean tryMutex() { + if (MUTEX.get() == null) { + MUTEX.set(MUTEX_PUBLISH); + return true; + } else { + return false; + } + } + + /** + * Releases the mutex held by the current thread. + * This mutex is thread local scope and will not block other threads. + * @since JavaMail 1.4.6 + */ + private void releaseMutex() { + MUTEX.remove(); + } + + /** + * This is used to get the filter index from when {@code isLoggable} and + * {@code isAttachmentLoggable} was invoked by {@code publish} method. + * + * @return the filter index or MUTEX_PUBLISH if unknown. + * @since JavaMail 1.5.5 + * @throws NullPointerException if tryMutex was not called. + */ + private int getMatchedPart() { + //assert Thread.holdsLock(this); + Integer idx = MUTEX.get(); + if (idx == null || idx >= readOnlyAttachmentFilters().length) { + idx = MUTEX_PUBLISH; + } + return idx; + } + + /** + * This is used to record the filter index when {@code isLoggable} and + * {@code isAttachmentLoggable} was invoked by {@code publish} method. + * + * @param index the filter index. + * @since JavaMail 1.5.5 + */ + private void setMatchedPart(int index) { + if (MUTEX_PUBLISH.equals(MUTEX.get())) { + MUTEX.set(index); + } + } + + /** + * Clear previous matches when the filters are modified and there are + * existing log records that were matched. + * @param index the lowest filter index to clear. + * @since JavaMail 1.5.5 + */ + private void clearMatches(int index) { + assert Thread.holdsLock(this); + for (int r = 0; r < size; ++r) { + if (matched[r] >= index) { + matched[r] = MUTEX_PUBLISH; + } + } + } + + /** + * A callback method for when this object is about to be placed into + * commission. This contract is defined by the + * {@code org.glassfish.hk2.api.PostConstruct} interface. If this class is + * loaded via a lifecycle managed environment other than HK2 then it is + * recommended that this method is called either directly or through + * extending this class to signal that this object is ready for use. + * + * @since JavaMail 1.5.3 + */ + //@javax.annotation.PostConstruct + public void postConstruct() { + } + + /** + * A callback method for when this object is about to be decommissioned. + * This contract is defined by the {@code org.glassfish.hk2.api.PreDestory} + * interface. If this class is loaded via a lifecycle managed environment + * other than HK2 then it is recommended that this method is called either + * directly or through extending this class to signal that this object will + * be destroyed. + * + * @since JavaMail 1.5.3 + */ + //@javax.annotation.PreDestroy + public void preDestroy() { + /** + * Close can require permissions so just trigger a push. + */ + push(false, ErrorManager.CLOSE_FAILURE); + } + + /** + * Pushes any buffered records to the email server as high importance with + * urgent priority. The internal buffer is then cleared. Does nothing if + * called from inside a push. + * @see #flush() + */ + public void push() { + push(true, ErrorManager.FLUSH_FAILURE); + } + + /** + * Pushes any buffered records to the email server as normal priority. + * The internal buffer is then cleared. Does nothing if called from inside + * a push. + * @see #push() + */ + @Override + public void flush() { + push(false, ErrorManager.FLUSH_FAILURE); + } + + /** + * Prevents any other records from being published. + * Pushes any buffered records to the email server as normal priority. + * The internal buffer is then cleared. Once this handler is closed it + * will remain closed. + * <p> + * If this <tt>Handler</tt> is only implicitly closed by the + * <tt>LogManager</tt>, then <a href="#verify">verification</a> should be + * turned on. + * @throws SecurityException if a security manager exists and the + * caller does not have <tt>LoggingPermission("control")</tt>. + * @see #flush() + */ + @Override + public void close() { + try { + checkAccess(); //Ensure setLevel works before clearing the buffer. + Message msg = null; + synchronized (this) { + try { + msg = writeLogRecords(ErrorManager.CLOSE_FAILURE); + } finally { //Change level after formatting. + this.logLevel = Level.OFF; + /** + * The sign bit of the capacity is set to ensure that + * records that have passed isLoggable, but have yet to be + * added to the internal buffer, are immediately pushed as + * an email. + */ + if (this.capacity > 0) { + this.capacity = -this.capacity; + } + + //Ensure not inside a push. + if (size == 0 && data.length != 1) { + this.data = new LogRecord[1]; + this.matched = new int[this.data.length]; + } + } + } + + if (msg != null) { + send(msg, false, ErrorManager.CLOSE_FAILURE); + } + } catch (final LinkageError JDK8152515) { + reportLinkageError(JDK8152515, ErrorManager.CLOSE_FAILURE); + } + } + + /** + * Set the log level specifying which message levels will be + * logged by this <tt>Handler</tt>. Message levels lower than this + * value will be discarded. + * @param newLevel the new value for the log level + * @throws NullPointerException if <tt>newLevel</tt> is <tt>null</tt>. + * @throws SecurityException if a security manager exists and + * the caller does not have <tt>LoggingPermission("control")</tt>. + */ + @Override + public void setLevel(final Level newLevel) { + if (newLevel == null) { + throw new NullPointerException(); + } + checkAccess(); + + //Don't allow a closed handler to be opened (half way). + synchronized (this) { //Wait for writeLogRecords. + if (this.capacity > 0) { + this.logLevel = newLevel; + } + } + } + + /** + * Get the log level specifying which messages will be logged by this + * <tt>Handler</tt>. Message levels lower than this level will be + * discarded. + * + * @return the level of messages being logged. + */ + @Override + public Level getLevel() { + return logLevel; //Volatile access. + } + + /** + * Retrieves the ErrorManager for this Handler. + * + * @return the ErrorManager for this Handler + * @throws SecurityException if a security manager exists and if the caller + * does not have <tt>LoggingPermission("control")</tt>. + */ + @Override + public ErrorManager getErrorManager() { + checkAccess(); + return this.errorManager; //Volatile access. + } + + /** + * Define an ErrorManager for this Handler. + * <p> + * The ErrorManager's "error" method will be invoked if any errors occur + * while using this Handler. + * + * @param em the new ErrorManager + * @throws SecurityException if a security manager exists and if the + * caller does not have <tt>LoggingPermission("control")</tt>. + * @throws NullPointerException if the given error manager is null. + */ + @Override + public void setErrorManager(final ErrorManager em) { + checkAccess(); + setErrorManager0(em); + } + + /** + * Sets the error manager on this handler and the super handler. In secure + * environments the super call may not be allowed which is not a failure + * condition as it is an attempt to free the unused handler error manager. + * + * @param em a non null error manager. + * @throws NullPointerException if the given error manager is null. + * @since JavaMail 1.5.6 + */ + private void setErrorManager0(final ErrorManager em) { + if (em == null) { + throw new NullPointerException(); + } + try { + synchronized (this) { //Wait for writeLogRecords. + this.errorManager = em; + super.setErrorManager(em); //Try to free super error manager. + } + } catch (RuntimeException | LinkageError ignore) { + } + } + + /** + * Get the current <tt>Filter</tt> for this <tt>Handler</tt>. + * + * @return a <tt>Filter</tt> object (may be null) + */ + @Override + public Filter getFilter() { + return this.filter; //Volatile access. + } + + /** + * Set a <tt>Filter</tt> to control output on this <tt>Handler</tt>. + * <P> + * For each call of <tt>publish</tt> the <tt>Handler</tt> will call this + * <tt>Filter</tt> (if it is non-null) to check if the <tt>LogRecord</tt> + * should be published or discarded. + * + * @param newFilter a <tt>Filter</tt> object (may be null) + * @throws SecurityException if a security manager exists and if the caller + * does not have <tt>LoggingPermission("control")</tt>. + */ + @Override + public void setFilter(final Filter newFilter) { + checkAccess(); + synchronized (this) { //Wait for writeLogRecords. + if (newFilter != filter) { + clearMatches(-1); + } + this.filter = newFilter; //Volatile access. + } + } + + /** + * Return the character encoding for this <tt>Handler</tt>. + * + * @return The encoding name. May be null, which indicates the default + * encoding should be used. + */ + @Override + public synchronized String getEncoding() { + return this.encoding; + } + + /** + * Set the character encoding used by this <tt>Handler</tt>. + * <p> + * The encoding should be set before any <tt>LogRecords</tt> are written + * to the <tt>Handler</tt>. + * + * @param encoding The name of a supported character encoding. May be + * null, to indicate the default platform encoding. + * @throws SecurityException if a security manager exists and if the caller + * does not have <tt>LoggingPermission("control")</tt>. + * @throws UnsupportedEncodingException if the named encoding is not + * supported. + */ + @Override + public void setEncoding(String encoding) throws UnsupportedEncodingException { + checkAccess(); + setEncoding0(encoding); + } + + /** + * Set the character encoding used by this handler. This method does not + * check permissions of the caller. + * + * @param e any encoding name or null for the default. + * @throws UnsupportedEncodingException if the given encoding is not supported. + */ + private void setEncoding0(String e) throws UnsupportedEncodingException { + if (e != null) { + try { + if (!java.nio.charset.Charset.isSupported(e)) { + throw new UnsupportedEncodingException(e); + } + } catch (java.nio.charset.IllegalCharsetNameException icne) { + throw new UnsupportedEncodingException(e); + } + } + + synchronized (this) { //Wait for writeLogRecords. + this.encoding = e; + } + } + + /** + * Return the <tt>Formatter</tt> for this <tt>Handler</tt>. + * + * @return the <tt>Formatter</tt> (may be null). + */ + @Override + public synchronized Formatter getFormatter() { + return this.formatter; + } + + /** + * Set a <tt>Formatter</tt>. This <tt>Formatter</tt> will be used to format + * <tt>LogRecords</tt> for this <tt>Handler</tt>. + * <p> + * Some <tt>Handlers</tt> may not use <tt>Formatters</tt>, in which case the + * <tt>Formatter</tt> will be remembered, but not used. + * <p> + * @param newFormatter the <tt>Formatter</tt> to use (may not be null) + * @throws SecurityException if a security manager exists and if the caller + * does not have <tt>LoggingPermission("control")</tt>. + * @throws NullPointerException if the given formatter is null. + */ + @Override + public synchronized void setFormatter(Formatter newFormatter) throws SecurityException { + checkAccess(); + if (newFormatter == null) { + throw new NullPointerException(); + } + this.formatter = newFormatter; + } + + /** + * Gets the push level. The default is <tt>Level.OFF</tt> meaning that + * this <tt>Handler</tt> will only push when the internal buffer is full. + * @return the push level. + */ + public final synchronized Level getPushLevel() { + return this.pushLevel; + } + + /** + * Sets the push level. This level is used to trigger a push so that + * all pending records are formatted and sent to the email server. When + * the push level triggers a send, the resulting email is flagged as + * high importance with urgent priority. + * @param level Level object. + * @throws NullPointerException if <tt>level</tt> is <tt>null</tt>. + * @throws SecurityException if a security manager exists and the + * caller does not have <tt>LoggingPermission("control")</tt>. + * @throws IllegalStateException if called from inside a push. + */ + public final synchronized void setPushLevel(final Level level) { + checkAccess(); + if (level == null) { + throw new NullPointerException(); + } + + if (isWriting) { + throw new IllegalStateException(); + } + this.pushLevel = level; + } + + /** + * Gets the push filter. The default is <tt>null</tt>. + * @return the push filter or <tt>null</tt>. + */ + public final synchronized Filter getPushFilter() { + return this.pushFilter; + } + + /** + * Sets the push filter. This filter is only called if the given + * <tt>LogRecord</tt> level was greater than the push level. If this + * filter returns <tt>true</tt>, all pending records are formatted and sent + * to the email server. When the push filter triggers a send, the resulting + * email is flagged as high importance with urgent priority. + * @param filter push filter or <tt>null</tt> + * @throws SecurityException if a security manager exists and the + * caller does not have <tt>LoggingPermission("control")</tt>. + * @throws IllegalStateException if called from inside a push. + */ + public final synchronized void setPushFilter(final Filter filter) { + checkAccess(); + if (isWriting) { + throw new IllegalStateException(); + } + this.pushFilter = filter; + } + + /** + * Gets the comparator used to order all <tt>LogRecord</tt> objects prior + * to formatting. If <tt>null</tt> then the order is unspecified. + * @return the <tt>LogRecord</tt> comparator. + */ + public final synchronized Comparator<? super LogRecord> getComparator() { + return this.comparator; + } + + /** + * Sets the comparator used to order all <tt>LogRecord</tt> objects prior + * to formatting. If <tt>null</tt> then the order is unspecified. + * @param c the <tt>LogRecord</tt> comparator. + * @throws SecurityException if a security manager exists and the + * caller does not have <tt>LoggingPermission("control")</tt>. + * @throws IllegalStateException if called from inside a push. + */ + public final synchronized void setComparator(Comparator<? super LogRecord> c) { + checkAccess(); + if (isWriting) { + throw new IllegalStateException(); + } + this.comparator = c; + } + + /** + * Gets the number of log records the internal buffer can hold. When + * capacity is reached, <tt>Handler</tt> will format all <tt>LogRecord</tt> + * objects into one email message. + * @return the capacity. + */ + public final synchronized int getCapacity() { + assert capacity != Integer.MIN_VALUE && capacity != 0 : capacity; + return Math.abs(capacity); + } + + /** + * Gets the <tt>Authenticator</tt> used to login to the email server. + * @return an <tt>Authenticator</tt> or <tt>null</tt> if none is required. + * @throws SecurityException if a security manager exists and the + * caller does not have <tt>LoggingPermission("control")</tt>. + */ + public final synchronized Authenticator getAuthenticator() { + checkAccess(); + return this.auth; + } + + /** + * Sets the <tt>Authenticator</tt> used to login to the email server. + * @param auth an <tt>Authenticator</tt> object or null if none is required. + * @throws SecurityException if a security manager exists and the + * caller does not have <tt>LoggingPermission("control")</tt>. + * @throws IllegalStateException if called from inside a push. + */ + public final void setAuthenticator(final Authenticator auth) { + this.setAuthenticator0(auth); + } + + /** + * Sets the <tt>Authenticator</tt> used to login to the email server. + * @param password a password, empty array can be used to only supply a + * user name set by <tt>mail.user</tt> property, or null if no credentials + * are required. + * @throws SecurityException if a security manager exists and the + * caller does not have <tt>LoggingPermission("control")</tt>. + * @throws IllegalStateException if called from inside a push. + * @see String#toCharArray() + * @since JavaMail 1.4.6 + */ + public final void setAuthenticator(final char... password) { + if (password == null) { + setAuthenticator0((Authenticator) null); + } else { + setAuthenticator0(DefaultAuthenticator.of(new String(password))); + } + } + + /** + * A private hook to handle possible future overrides. See public method. + * @param auth see public method. + * @throws SecurityException if a security manager exists and the + * caller does not have <tt>LoggingPermission("control")</tt>. + * @throws IllegalStateException if called from inside a push. + */ + private void setAuthenticator0(final Authenticator auth) { + checkAccess(); + + Session settings; + synchronized (this) { + if (isWriting) { + throw new IllegalStateException(); + } + this.auth = auth; + settings = updateSession(); + } + verifySettings(settings); + } + + /** + * Sets the mail properties used for the session. The key/value pairs + * are defined in the <tt>Java Mail API</tt> documentation. This + * <tt>Handler</tt> will also search the <tt>LogManager</tt> for defaults + * if needed. + * @param props a non <tt>null</tt> properties object. + * @throws SecurityException if a security manager exists and the + * caller does not have <tt>LoggingPermission("control")</tt>. + * @throws NullPointerException if <tt>props</tt> is <tt>null</tt>. + * @throws IllegalStateException if called from inside a push. + */ + public final void setMailProperties(Properties props) { + this.setMailProperties0(props); + } + + /** + * A private hook to handle overrides when the public method is declared + * non final. See public method for details. + * @param props see public method. + */ + private void setMailProperties0(Properties props) { + checkAccess(); + props = (Properties) props.clone(); //Allow subclass. + Session settings; + synchronized (this) { + if (isWriting) { + throw new IllegalStateException(); + } + this.mailProps = props; + settings = updateSession(); + } + verifySettings(settings); + } + + /** + * Gets a copy of the mail properties used for the session. + * @return a non null properties object. + * @throws SecurityException if a security manager exists and the + * caller does not have <tt>LoggingPermission("control")</tt>. + */ + public final Properties getMailProperties() { + checkAccess(); + final Properties props; + synchronized (this) { + props = this.mailProps; + } + return (Properties) props.clone(); + } + + /** + * Gets the attachment filters. If the attachment filter does not + * allow any <tt>LogRecord</tt> to be formatted, the attachment may + * be omitted from the email. + * @return a non null array of attachment filters. + */ + public final Filter[] getAttachmentFilters() { + return readOnlyAttachmentFilters().clone(); + } + + /** + * Sets the attachment filters. + * @param filters a non <tt>null</tt> array of filters. A <tt>null</tt> + * index value is allowed. A <tt>null</tt> value means that all + * records are allowed for the attachment at that index. + * @throws SecurityException if a security manager exists and the + * caller does not have <tt>LoggingPermission("control")</tt>. + * @throws NullPointerException if <tt>filters</tt> is <tt>null</tt> + * @throws IndexOutOfBoundsException if the number of attachment + * name formatters do not match the number of attachment formatters. + * @throws IllegalStateException if called from inside a push. + */ + public final void setAttachmentFilters(Filter... filters) { + checkAccess(); + if (filters.length == 0) { + filters = emptyFilterArray(); + } else { + filters = Arrays.copyOf(filters, filters.length, Filter[].class); + } + synchronized (this) { + if (this.attachmentFormatters.length != filters.length) { + throw attachmentMismatch(this.attachmentFormatters.length, filters.length); + } + + if (isWriting) { + throw new IllegalStateException(); + } + + if (size != 0) { + for (int i = 0; i < filters.length; ++i) { + if (filters[i] != attachmentFilters[i]) { + clearMatches(i); + break; + } + } + } + this.attachmentFilters = filters; + } + } + + /** + * Gets the attachment formatters. This <tt>Handler</tt> is using + * attachments only if the returned array length is non zero. + * @return a non <tt>null</tt> array of formatters. + */ + public final Formatter[] getAttachmentFormatters() { + Formatter[] formatters; + synchronized (this) { + formatters = this.attachmentFormatters; + } + return formatters.clone(); + } + + /** + * Sets the attachment <tt>Formatter</tt> object for this handler. + * The number of formatters determines the number of attachments per + * email. This method should be the first attachment method called. + * To remove all attachments, call this method with empty array. + * @param formatters a non null array of formatters. + * @throws SecurityException if a security manager exists and the + * caller does not have <tt>LoggingPermission("control")</tt>. + * @throws NullPointerException if the given array or any array index is + * <tt>null</tt>. + * @throws IllegalStateException if called from inside a push. + */ + public final void setAttachmentFormatters(Formatter... formatters) { + checkAccess(); + if (formatters.length == 0) { //Null check and length check. + formatters = emptyFormatterArray(); + } else { + formatters = Arrays.copyOf(formatters, + formatters.length, Formatter[].class); + for (int i = 0; i < formatters.length; ++i) { + if (formatters[i] == null) { + throw new NullPointerException(atIndexMsg(i)); + } + } + } + + synchronized (this) { + if (isWriting) { + throw new IllegalStateException(); + } + + this.attachmentFormatters = formatters; + this.alignAttachmentFilters(); + this.alignAttachmentNames(); + } + } + + /** + * Gets the attachment name formatters. + * If the attachment names were set using explicit names then + * the names can be returned by calling <tt>toString</tt> on each + * attachment name formatter. + * @return non <tt>null</tt> array of attachment name formatters. + */ + public final Formatter[] getAttachmentNames() { + final Formatter[] formatters; + synchronized (this) { + formatters = this.attachmentNames; + } + return formatters.clone(); + } + + /** + * Sets the attachment file name for each attachment. All control + * characters are removed from the attachment names. + * This method will create a set of custom formatters. + * @param names an array of names. + * @throws SecurityException if a security manager exists and the + * caller does not have <tt>LoggingPermission("control")</tt>. + * @throws IndexOutOfBoundsException if the number of attachment + * names do not match the number of attachment formatters. + * @throws IllegalArgumentException if any name is empty. + * @throws NullPointerException if any given array or name is <tt>null</tt>. + * @throws IllegalStateException if called from inside a push. + * @see Character#isISOControl(char) + * @see Character#isISOControl(int) + */ + public final void setAttachmentNames(final String... names) { + checkAccess(); + + final Formatter[] formatters; + if (names.length == 0) { + formatters = emptyFormatterArray(); + } else { + formatters = new Formatter[names.length]; + } + + for (int i = 0; i < names.length; ++i) { + final String name = names[i]; + if (name != null) { + if (name.length() > 0) { + formatters[i] = TailNameFormatter.of(name); + } else { + throw new IllegalArgumentException(atIndexMsg(i)); + } + } else { + throw new NullPointerException(atIndexMsg(i)); + } + } + + synchronized (this) { + if (this.attachmentFormatters.length != names.length) { + throw attachmentMismatch(this.attachmentFormatters.length, names.length); + } + + if (isWriting) { + throw new IllegalStateException(); + } + this.attachmentNames = formatters; + } + } + + /** + * Sets the attachment file name formatters. The format method of each + * attachment formatter will see only the <tt>LogRecord</tt> objects that + * passed its attachment filter during formatting. The format method will + * typically return an empty string. Instead of being used to format + * records, it is used to gather information about the contents of an + * attachment. The <tt>getTail</tt> method should be used to construct the + * attachment file name and reset any formatter collected state. All + * control characters will be removed from the output of the formatter. The + * <tt>toString</tt> method of the given formatter should be overridden to + * provide a useful attachment file name, if possible. + * @param formatters and array of attachment name formatters. + * @throws SecurityException if a security manager exists and the + * caller does not have <tt>LoggingPermission("control")</tt>. + * @throws IndexOutOfBoundsException if the number of attachment + * name formatters do not match the number of attachment formatters. + * @throws NullPointerException if any given array or name is <tt>null</tt>. + * @throws IllegalStateException if called from inside a push. + * @see Character#isISOControl(char) + * @see Character#isISOControl(int) + */ + public final void setAttachmentNames(Formatter... formatters) { + checkAccess(); + + if (formatters.length == 0) { + formatters = emptyFormatterArray(); + } else { + formatters = Arrays.copyOf(formatters, formatters.length, + Formatter[].class); + } + + for (int i = 0; i < formatters.length; ++i) { + if (formatters[i] == null) { + throw new NullPointerException(atIndexMsg(i)); + } + } + + synchronized (this) { + if (this.attachmentFormatters.length != formatters.length) { + throw attachmentMismatch(this.attachmentFormatters.length, + formatters.length); + } + + if (isWriting) { + throw new IllegalStateException(); + } + + this.attachmentNames = formatters; + } + } + + /** + * Gets the formatter used to create the subject line. + * If the subject was created using a literal string then + * the <tt>toString</tt> method can be used to get the subject line. + * @return the formatter. + */ + public final synchronized Formatter getSubject() { + return this.subjectFormatter; + } + + /** + * Sets a literal string for the email subject. All control characters are + * removed from the subject line. + * @param subject a non <tt>null</tt> string. + * @throws SecurityException if a security manager exists and the + * caller does not have <tt>LoggingPermission("control")</tt>. + * @throws NullPointerException if <tt>subject</tt> is <tt>null</tt>. + * @throws IllegalStateException if called from inside a push. + * @see Character#isISOControl(char) + * @see Character#isISOControl(int) + */ + public final void setSubject(final String subject) { + if (subject != null) { + this.setSubject(TailNameFormatter.of(subject)); + } else { + checkAccess(); + throw new NullPointerException(); + } + } + + /** + * Sets the subject formatter for email. The format method of the subject + * formatter will see all <tt>LogRecord</tt> objects that were published to + * this <tt>Handler</tt> during formatting and will typically return an + * empty string. This formatter is used to gather information to create a + * summary about what information is contained in the email. The + * <tt>getTail</tt> method should be used to construct the subject and reset + * any formatter collected state. All control characters + * will be removed from the formatter output. The <tt>toString</tt> + * method of the given formatter should be overridden to provide a useful + * subject, if possible. + * @param format the subject formatter. + * @throws SecurityException if a security manager exists and the + * caller does not have <tt>LoggingPermission("control")</tt>. + * @throws NullPointerException if <tt>format</tt> is <tt>null</tt>. + * @throws IllegalStateException if called from inside a push. + * @see Character#isISOControl(char) + * @see Character#isISOControl(int) + */ + public final void setSubject(final Formatter format) { + checkAccess(); + if (format == null) { + throw new NullPointerException(); + } + + synchronized (this) { + if (isWriting) { + throw new IllegalStateException(); + } + this.subjectFormatter = format; + } + } + + /** + * Protected convenience method to report an error to this Handler's + * ErrorManager. This method will prefix all non null error messages with + * <tt>Level.SEVERE.getName()</tt>. This allows the receiving error + * manager to determine if the <tt>msg</tt> parameter is a simple error + * message or a raw email message. + * @param msg a descriptive string (may be null) + * @param ex an exception (may be null) + * @param code an error code defined in ErrorManager + */ + @Override + protected void reportError(String msg, Exception ex, int code) { + try { + if (msg != null) { + errorManager.error(Level.SEVERE.getName() + .concat(": ").concat(msg), ex, code); + } else { + errorManager.error(null, ex, code); + } + } catch (RuntimeException | LinkageError GLASSFISH_21258) { + reportLinkageError(GLASSFISH_21258, code); + } + } + + /** + * Calls log manager checkAccess if this is sealed. + */ + private void checkAccess() { + if (sealed) { + LogManagerProperties.checkLogManagerAccess(); + } + } + + /** + * Determines the mimeType of a formatter from the getHead call. + * This could be made protected, or a new class could be created to do + * this type of conversion. Currently, this is only used for the body + * since the attachments are computed by filename. + * Package-private for unit testing. + * @param chunk any char sequence or null. + * @return return the mime type or null for text/plain. + */ + final String contentTypeOf(CharSequence chunk) { + if (!isEmpty(chunk)) { + final int MAX_CHARS = 25; + if (chunk.length() > MAX_CHARS) { + chunk = chunk.subSequence(0, MAX_CHARS); + } + try { + final String charset = getEncodingName(); + final byte[] b = chunk.toString().getBytes(charset); + final ByteArrayInputStream in = new ByteArrayInputStream(b); + assert in.markSupported() : in.getClass().getName(); + return URLConnection.guessContentTypeFromStream(in); + } catch (final IOException IOE) { + reportError(IOE.getMessage(), IOE, ErrorManager.FORMAT_FAILURE); + } + } + return null; //text/plain + } + + /** + * Determines the mimeType of a formatter by the class name. This method + * avoids calling getHead and getTail of content formatters during verify + * because they might trigger side effects or excessive work. The name + * formatters and subject are usually safe to call. + * Package-private for unit testing. + * + * @param f the formatter or null. + * @return return the mime type or null, meaning text/plain. + * @since JavaMail 1.5.6 + */ + final String contentTypeOf(final Formatter f) { + assert Thread.holdsLock(this); + if (f != null) { + String type = getContentType(f.getClass().getName()); + if (type != null) { + return type; + } + + for (Class<?> k = f.getClass(); k != Formatter.class; + k = k.getSuperclass()) { + String name; + try { + name = k.getSimpleName(); + } catch (final InternalError JDK8057919) { + name = k.getName(); + } + name = name.toLowerCase(Locale.ENGLISH); + for (int idx = name.indexOf('$') + 1; + (idx = name.indexOf("ml", idx)) > -1; idx += 2) { + if (idx > 0) { + if (name.charAt(idx - 1) == 'x') { + return "application/xml"; + } + if (idx > 1 && name.charAt(idx - 2) == 'h' + && name.charAt(idx - 1) == 't') { + return "text/html"; + } + } + } + } + } + return null; + } + + /** + * Determines if the given throwable is a no content exception. It is + * assumed Transport.sendMessage will call Message.writeTo so we need to + * ignore any exceptions that could be layered on top of that call chain to + * infer that sendMessage is failing because of writeTo. Package-private + * for unit testing. + * @param msg the message without content. + * @param t the throwable chain to test. + * @return true if the throwable is a missing content exception. + * @throws NullPointerException if any of the arguments are null. + * @since JavaMail 1.4.5 + */ + @SuppressWarnings({"UseSpecificCatch", "ThrowableResultIgnored"}) + final boolean isMissingContent(Message msg, Throwable t) { + final Object ccl = getAndSetContextClassLoader(MAILHANDLER_LOADER); + try { + msg.writeTo(new ByteArrayOutputStream(MIN_HEADER_SIZE)); + } catch (final RuntimeException RE) { + throw RE; //Avoid catch all. + } catch (final Exception noContent) { + final String txt = noContent.getMessage(); + if (!isEmpty(txt)) { + int limit = 0; + while (t != null) { + if (noContent.getClass() == t.getClass() + && txt.equals(t.getMessage())) { + return true; + } + + //Not all JavaMail implementations support JDK 1.4 exception + //chaining. + final Throwable cause = t.getCause(); + if (cause == null && t instanceof MessagingException) { + t = ((MessagingException) t).getNextException(); + } else { + t = cause; + } + + //Deal with excessive cause chains and cyclic throwables. + if (++limit == (1 << 16)) { + break; //Give up. + } + } + } + } finally { + getAndSetContextClassLoader(ccl); + } + return false; + } + + /** + * Converts a mime message to a raw string or formats the reason + * why message can't be changed to raw string and reports it. + * @param msg the mime message. + * @param ex the original exception. + * @param code the ErrorManager code. + * @since JavaMail 1.4.5 + */ + @SuppressWarnings("UseSpecificCatch") + private void reportError(Message msg, Exception ex, int code) { + try { + try { //Use direct call so we do not prefix raw email. + errorManager.error(toRawString(msg), ex, code); + } catch (final RuntimeException re) { + reportError(toMsgString(re), ex, code); + } catch (final Exception e) { + reportError(toMsgString(e), ex, code); + } + } catch (final LinkageError GLASSFISH_21258) { + reportLinkageError(GLASSFISH_21258, code); + } + } + + /** + * Reports the given linkage error or runtime exception. + * + * The current LogManager code will stop closing all remaining handlers if + * an error is thrown during resetLogger. This is a workaround for + * GLASSFISH-21258 and JDK-8152515. + * @param le the linkage error or a RuntimeException. + * @param code the ErrorManager code. + * @throws NullPointerException if error is null. + * @since JavaMail 1.5.3 + */ + private void reportLinkageError(final Throwable le, final int code) { + if (le == null) { + throw new NullPointerException(String.valueOf(code)); + } + + final Integer idx = MUTEX.get(); + if (idx == null || idx > MUTEX_LINKAGE) { + MUTEX.set(MUTEX_LINKAGE); + try { + Thread.currentThread().getUncaughtExceptionHandler() + .uncaughtException(Thread.currentThread(), le); + } catch (RuntimeException | LinkageError ignore) { + } finally { + if (idx != null) { + MUTEX.set(idx); + } else { + MUTEX.remove(); + } + } + } + } + + /** + * Determines the mimeType from the given file name. + * Used to override the body content type and used for all attachments. + * @param name the file name or class name. + * @return the mime type or null for text/plain. + */ + private String getContentType(final String name) { + assert Thread.holdsLock(this); + final String type = contentTypes.getContentType(name); + if ("application/octet-stream".equalsIgnoreCase(type)) { + return null; //Formatters return strings, default to text/plain. + } + return type; + } + + /** + * Gets the encoding set for this handler, mime encoding, or file encoding. + * @return the java charset name, never null. + * @since JavaMail 1.4.5 + */ + private String getEncodingName() { + String charset = getEncoding(); + if (charset == null) { + charset = MimeUtility.getDefaultJavaCharset(); + } + return charset; + } + + /** + * Set the content for a part using the encoding assigned to the handler. + * @param part the part to assign. + * @param buf the formatted data. + * @param type the mime type or null, meaning text/plain. + * @throws MessagingException if there is a problem. + */ + private void setContent(MimePart part, CharSequence buf, String type) throws MessagingException { + final String charset = getEncodingName(); + if (type != null && !"text/plain".equalsIgnoreCase(type)) { + type = contentWithEncoding(type, charset); + try { + DataSource source = new ByteArrayDataSource(buf.toString(), type); + part.setDataHandler(new DataHandler(source)); + } catch (final IOException IOE) { + reportError(IOE.getMessage(), IOE, ErrorManager.FORMAT_FAILURE); + part.setText(buf.toString(), charset); + } + } else { + part.setText(buf.toString(), MimeUtility.mimeCharset(charset)); + } + } + + /** + * Replaces the charset parameter with the current encoding. + * @param type the content type. + * @param encoding the java charset name. + * @return the type with a specified encoding. + */ + private String contentWithEncoding(String type, String encoding) { + assert encoding != null; + try { + final ContentType ct = new ContentType(type); + ct.setParameter("charset", MimeUtility.mimeCharset(encoding)); + encoding = ct.toString(); //See javax.mail.internet.ContentType. + if (!isEmpty(encoding)) { //Support pre K5687. + type = encoding; + } + } catch (final MessagingException ME) { + reportError(type, ME, ErrorManager.FORMAT_FAILURE); + } + return type; + } + + /** + * Sets the capacity for this handler. This method is kept private + * because we would have to define a public policy for when the size is + * greater than the capacity. + * E.G. do nothing, flush now, truncate now, push now and resize. + * @param newCapacity the max number of records. + * @throws SecurityException if a security manager exists and the + * caller does not have <tt>LoggingPermission("control")</tt>. + * @throws IllegalStateException if called from inside a push. + */ + private synchronized void setCapacity0(final int newCapacity) { + checkAccess(); + if (newCapacity <= 0) { + throw new IllegalArgumentException("Capacity must be greater than zero."); + } + + if (isWriting) { + throw new IllegalStateException(); + } + + if (this.capacity < 0) { //If closed, remain closed. + this.capacity = -newCapacity; + } else { + this.capacity = newCapacity; + } + } + + /** + * Gets the attachment filters using a happens-before relationship between + * this method and setAttachmentFilters. The attachment filters are treated + * as copy-on-write, so the returned array must never be modified or + * published outside this class. + * @return a read only array of filters. + */ + private Filter[] readOnlyAttachmentFilters() { + return this.attachmentFilters; + } + + /** + * Factory for empty formatter arrays. + * @return an empty array. + */ + private static Formatter[] emptyFormatterArray() { + return EMPTY_FORMATTERS; + } + + /** + * Factory for empty filter arrays. + * @return an empty array. + */ + private static Filter[] emptyFilterArray() { + return EMPTY_FILTERS; + } + + /** + * Expand or shrink the attachment name formatters with the attachment + * formatters. + * @return true if size was changed. + */ + private boolean alignAttachmentNames() { + assert Thread.holdsLock(this); + boolean fixed = false; + final int expect = this.attachmentFormatters.length; + final int current = this.attachmentNames.length; + if (current != expect) { + this.attachmentNames = Arrays.copyOf(attachmentNames, expect, + Formatter[].class); + fixed = current != 0; + } + + //Copy of zero length array is cheap, warm up copyOf. + if (expect == 0) { + this.attachmentNames = emptyFormatterArray(); + assert this.attachmentNames.length == 0; + } else { + for (int i = 0; i < expect; ++i) { + if (this.attachmentNames[i] == null) { + this.attachmentNames[i] = TailNameFormatter.of( + toString(this.attachmentFormatters[i])); + } + } + } + return fixed; + } + + /** + * Expand or shrink the attachment filters with the attachment formatters. + * @return true if the size was changed. + */ + private boolean alignAttachmentFilters() { + assert Thread.holdsLock(this); + + boolean fixed = false; + final int expect = this.attachmentFormatters.length; + final int current = this.attachmentFilters.length; + if (current != expect) { + this.attachmentFilters = Arrays.copyOf(attachmentFilters, expect, + Filter[].class); + clearMatches(current); + fixed = current != 0; + + //Array elements default to null so skip filling if body filter + //is null. If not null then only assign to expanded elements. + final Filter body = this.filter; + if (body != null) { + for (int i = current; i < expect; ++i) { + this.attachmentFilters[i] = body; + } + } + } + + //Copy of zero length array is cheap, warm up copyOf. + if (expect == 0) { + this.attachmentFilters = emptyFilterArray(); + assert this.attachmentFilters.length == 0; + } + return fixed; + } + + /** + * Sets the size to zero and clears the current buffer. + */ + private void reset() { + assert Thread.holdsLock(this); + if (size < data.length) { + Arrays.fill(data, 0, size, null); + } else { + Arrays.fill(data, null); + } + this.size = 0; + } + + /** + * Expands the internal buffer up to the capacity. + */ + private void grow() { + assert Thread.holdsLock(this); + final int len = data.length; + int newCapacity = len + (len >> 1) + 1; + if (newCapacity > capacity || newCapacity < len) { + newCapacity = capacity; + } + assert len != capacity : len; + this.data = Arrays.copyOf(data, newCapacity, LogRecord[].class); + this.matched = Arrays.copyOf(matched, newCapacity); + } + + /** + * Configures the handler properties from the log manager. + * @param props the given mail properties. Maybe null and are never + * captured by this handler. + * @throws SecurityException if a security manager exists and the + * caller does not have <tt>LoggingPermission("control")</tt>. + */ + private synchronized void init(final Properties props) { + assert this.errorManager != null; + final String p = getClass().getName(); + this.mailProps = new Properties(); //See method param comments. + final Object ccl = getAndSetContextClassLoader(MAILHANDLER_LOADER); + try { + this.contentTypes = FileTypeMap.getDefaultFileTypeMap(); + } finally { + getAndSetContextClassLoader(ccl); + } + + //Assign any custom error manager first so it can detect all failures. + initErrorManager(p); + + initLevel(p); + initFilter(p); + initCapacity(p); + initAuthenticator(p); + + initEncoding(p); + initFormatter(p); + initComparator(p); + initPushLevel(p); + initPushFilter(p); + + initSubject(p); + + initAttachmentFormaters(p); + initAttachmentFilters(p); + initAttachmentNames(p); + + if (props == null && fromLogManager(p.concat(".verify")) != null) { + verifySettings(initSession()); + } + intern(); //Show verify warnings first. + } + + /** + * Interns the error manager, formatters, and filters contained in this + * handler. The comparator is not interned. This method can only be + * called from init after all of formatters and filters are in a constructed + * and in a consistent state. + * @since JavaMail 1.5.0 + */ + private void intern() { + assert Thread.holdsLock(this); + try { + Object canidate; + Object result; + final Map<Object, Object> seen = new HashMap<>(); + try { + intern(seen, this.errorManager); + } catch (final SecurityException se) { + reportError(se.getMessage(), se, ErrorManager.OPEN_FAILURE); + } + + try { + canidate = this.filter; + result = intern(seen, canidate); + if (result != canidate && result instanceof Filter) { + this.filter = (Filter) result; + } + + canidate = this.formatter; + result = intern(seen, canidate); + if (result != canidate && result instanceof Formatter) { + this.formatter = (Formatter) result; + } + } catch (final SecurityException se) { + reportError(se.getMessage(), se, ErrorManager.OPEN_FAILURE); + } + + canidate = this.subjectFormatter; + result = intern(seen, canidate); + if (result != canidate && result instanceof Formatter) { + this.subjectFormatter = (Formatter) result; + } + + canidate = this.pushFilter; + result = intern(seen, canidate); + if (result != canidate && result instanceof Filter) { + this.pushFilter = (Filter) result; + } + + for (int i = 0; i < attachmentFormatters.length; ++i) { + canidate = attachmentFormatters[i]; + result = intern(seen, canidate); + if (result != canidate && result instanceof Formatter) { + attachmentFormatters[i] = (Formatter) result; + } + + canidate = attachmentFilters[i]; + result = intern(seen, canidate); + if (result != canidate && result instanceof Filter) { + attachmentFilters[i] = (Filter) result; + } + + canidate = attachmentNames[i]; + result = intern(seen, canidate); + if (result != canidate && result instanceof Formatter) { + attachmentNames[i] = (Formatter) result; + } + } + } catch (final Exception skip) { + reportError(skip.getMessage(), skip, ErrorManager.OPEN_FAILURE); + } catch (final LinkageError skip) { + reportError(skip.getMessage(), new InvocationTargetException(skip), + ErrorManager.OPEN_FAILURE); + } + } + + /** + * If possible performs an intern of the given object into the + * map. If the object can not be interned the given object is returned. + * @param m the map used to record the interned values. + * @param o the object to try an intern. + * @return the original object or an intern replacement. + * @throws SecurityException if this operation is not allowed by the + * security manager. + * @throws Exception if there is an unexpected problem. + * @since JavaMail 1.5.0 + */ + private Object intern(Map<Object, Object> m, Object o) throws Exception { + if (o == null) { + return null; + } + + /** + * The common case is that most objects will not intern. The given + * object has a public no argument constructor or is an instance of a + * TailNameFormatter. TailNameFormatter is safe use as a map key. + * For everything else we create a clone of the given object. + * This is done because of the following: + * 1. Clones can be used to test that a class provides an equals method + * and that the equals method works correctly. + * 2. Calling equals on the given object is assumed to be cheap. + * 3. The intern map can be filtered so it only contains objects that + * can be interned, which reduces the memory footprint. + * 4. Clones are method local garbage. + * 5. Hash code is only called on the clones so bias locking is not + * disabled on the objects the handler will use. + */ + final Object key; + if (o.getClass().getName().equals(TailNameFormatter.class.getName())) { + key = o; + } else { + //This call was already made in the LogManagerProperties so this + //shouldn't trigger loading of any lazy reflection code. + key = o.getClass().getConstructor().newInstance(); + } + + final Object use; + //Check the classloaders of each object avoiding the security manager. + if (key.getClass() == o.getClass()) { + Object found = m.get(key); //Transitive equals test. + if (found == null) { + //Ensure that equals is symmetric to prove intern is safe. + final boolean right = key.equals(o); + final boolean left = o.equals(key); + if (right && left) { + //Assume hashCode is defined at this point. + found = m.put(o, o); + if (found != null) { + reportNonDiscriminating(key, found); + found = m.remove(key); + if (found != o) { + reportNonDiscriminating(key, found); + m.clear(); //Try to restore order. + } + } + } else { + if (right != left) { + reportNonSymmetric(o, key); + } + } + use = o; + } else { + //Check for a discriminating equals method. + if (o.getClass() == found.getClass()) { + use = found; + } else { + reportNonDiscriminating(o, found); + use = o; + } + } + } else { + use = o; + } + return use; + } + + /** + * Factory method used to create a java.util.logging.SimpleFormatter. + * @return a new SimpleFormatter. + * @since JavaMail 1.5.6 + */ + private static Formatter createSimpleFormatter() { + //Don't force the byte code verifier to load the formatter. + return Formatter.class.cast(new SimpleFormatter()); + } + + /** + * Checks a char sequence value for null or empty. + * @param s the char sequence. + * @return true if the given string is null or zero length. + */ + private static boolean isEmpty(final CharSequence s) { + return s == null || s.length() == 0; + } + + /** + * Checks that a string is not empty and not equal to the literal "null". + * @param name the string to check for a value. + * @return true if the string has a valid value. + */ + private static boolean hasValue(final String name) { + return !isEmpty(name) && !"null".equalsIgnoreCase(name); + } + + /** + * Parses LogManager string values into objects used by this handler. + * @param p the handler class name used as the prefix. + * @throws NullPointerException if the given argument is null. + * @throws SecurityException if not allowed. + */ + private void initAttachmentFilters(final String p) { + assert Thread.holdsLock(this); + assert this.attachmentFormatters != null; + final String list = fromLogManager(p.concat(".attachment.filters")); + if (!isEmpty(list)) { + final String[] names = list.split(","); + Filter[] a = new Filter[names.length]; + for (int i = 0; i < a.length; ++i) { + names[i] = names[i].trim(); + if (!"null".equalsIgnoreCase(names[i])) { + try { + a[i] = LogManagerProperties.newFilter(names[i]); + } catch (final SecurityException SE) { + throw SE; //Avoid catch all. + } catch (final Exception E) { + reportError(E.getMessage(), E, ErrorManager.OPEN_FAILURE); + } + } + } + + this.attachmentFilters = a; + if (alignAttachmentFilters()) { + reportError("Attachment filters.", + attachmentMismatch("Length mismatch."), ErrorManager.OPEN_FAILURE); + } + } else { + this.attachmentFilters = emptyFilterArray(); + alignAttachmentFilters(); + } + } + + /** + * Parses LogManager string values into objects used by this handler. + * @param p the handler class name used as the prefix. + * @throws NullPointerException if the given argument is null. + * @throws SecurityException if not allowed. + */ + private void initAttachmentFormaters(final String p) { + assert Thread.holdsLock(this); + final String list = fromLogManager(p.concat(".attachment.formatters")); + if (!isEmpty(list)) { + final Formatter[] a; + final String[] names = list.split(","); + if (names.length == 0) { + a = emptyFormatterArray(); + } else { + a = new Formatter[names.length]; + } + + for (int i = 0; i < a.length; ++i) { + names[i] = names[i].trim(); + if (!"null".equalsIgnoreCase(names[i])) { + try { + a[i] = LogManagerProperties.newFormatter(names[i]); + if (a[i] instanceof TailNameFormatter) { + final Exception CNFE = new ClassNotFoundException(a[i].toString()); + reportError("Attachment formatter.", CNFE, ErrorManager.OPEN_FAILURE); + a[i] = createSimpleFormatter(); + } + } catch (final SecurityException SE) { + throw SE; //Avoid catch all. + } catch (final Exception E) { + reportError(E.getMessage(), E, ErrorManager.OPEN_FAILURE); + a[i] = createSimpleFormatter(); + } + } else { + final Exception NPE = new NullPointerException(atIndexMsg(i)); + reportError("Attachment formatter.", NPE, ErrorManager.OPEN_FAILURE); + a[i] = createSimpleFormatter(); + } + } + + this.attachmentFormatters = a; + } else { + this.attachmentFormatters = emptyFormatterArray(); + } + } + + /** + * Parses LogManager string values into objects used by this handler. + * @param p the handler class name used as the prefix. + * @throws NullPointerException if the given argument is null. + * @throws SecurityException if not allowed. + */ + private void initAttachmentNames(final String p) { + assert Thread.holdsLock(this); + assert this.attachmentFormatters != null; + + final String list = fromLogManager(p.concat(".attachment.names")); + if (!isEmpty(list)) { + final String[] names = list.split(","); + final Formatter[] a = new Formatter[names.length]; + for (int i = 0; i < a.length; ++i) { + names[i] = names[i].trim(); + if (!"null".equalsIgnoreCase(names[i])) { + try { + try { + a[i] = LogManagerProperties.newFormatter(names[i]); + } catch (ClassNotFoundException + | ClassCastException literal) { + a[i] = TailNameFormatter.of(names[i]); + } + } catch (final SecurityException SE) { + throw SE; //Avoid catch all. + } catch (final Exception E) { + reportError(E.getMessage(), E, ErrorManager.OPEN_FAILURE); + } + } else { + final Exception NPE = new NullPointerException(atIndexMsg(i)); + reportError("Attachment names.", NPE, ErrorManager.OPEN_FAILURE); + } + } + + this.attachmentNames = a; + if (alignAttachmentNames()) { //Any null indexes are repaired. + reportError("Attachment names.", + attachmentMismatch("Length mismatch."), ErrorManager.OPEN_FAILURE); + } + } else { + this.attachmentNames = emptyFormatterArray(); + alignAttachmentNames(); + } + } + + /** + * Parses LogManager string values into objects used by this handler. + * @param p the handler class name used as the prefix. + * @throws NullPointerException if the given argument is null. + * @throws SecurityException if not allowed. + */ + private void initAuthenticator(final String p) { + assert Thread.holdsLock(this); + String name = fromLogManager(p.concat(".authenticator")); + if (name != null && !"null".equalsIgnoreCase(name)) { + if (name.length() != 0) { + try { + this.auth = LogManagerProperties + .newObjectFrom(name, Authenticator.class); + } catch (final SecurityException SE) { + throw SE; + } catch (final ClassNotFoundException + | ClassCastException literalAuth) { + this.auth = DefaultAuthenticator.of(name); + } catch (final Exception E) { + reportError(E.getMessage(), E, ErrorManager.OPEN_FAILURE); + } + } else { //Authenticator is installed to provide the user name. + this.auth = DefaultAuthenticator.of(name); + } + } + } + + /** + * Parses LogManager string values into objects used by this handler. + * @param p the handler class name used as the prefix. + * @throws NullPointerException if the given argument is null. + * @throws SecurityException if not allowed. + */ + private void initLevel(final String p) { + assert Thread.holdsLock(this); + try { + final String val = fromLogManager(p.concat(".level")); + if (val != null) { + logLevel = Level.parse(val); + } else { + logLevel = Level.WARNING; + } + } catch (final SecurityException SE) { + throw SE; //Avoid catch all. + } catch (final RuntimeException RE) { + reportError(RE.getMessage(), RE, ErrorManager.OPEN_FAILURE); + logLevel = Level.WARNING; + } + } + + /** + * Parses LogManager string values into objects used by this handler. + * @param p the handler class name used as the prefix. + * @throws NullPointerException if the given argument is null. + * @throws SecurityException if not allowed. + */ + private void initFilter(final String p) { + assert Thread.holdsLock(this); + try { + String name = fromLogManager(p.concat(".filter")); + if (hasValue(name)) { + filter = LogManagerProperties.newFilter(name); + } + } catch (final SecurityException SE) { + throw SE; //Avoid catch all. + } catch (final Exception E) { + reportError(E.getMessage(), E, ErrorManager.OPEN_FAILURE); + } + } + + /** + * Parses LogManager string values into objects used by this handler. + * @param p the handler class name used as the prefix. + * @throws NullPointerException if argument is null. + * @throws SecurityException if not allowed. + */ + private void initCapacity(final String p) { + assert Thread.holdsLock(this); + final int DEFAULT_CAPACITY = 1000; + try { + final String value = fromLogManager(p.concat(".capacity")); + if (value != null) { + this.setCapacity0(Integer.parseInt(value)); + } else { + this.setCapacity0(DEFAULT_CAPACITY); + } + } catch (final SecurityException SE) { + throw SE; //Avoid catch all. + } catch (final RuntimeException RE) { + reportError(RE.getMessage(), RE, ErrorManager.OPEN_FAILURE); + } + + if (capacity <= 0) { + capacity = DEFAULT_CAPACITY; + } + + this.data = new LogRecord[1]; + this.matched = new int[this.data.length]; + } + + /** + * Parses LogManager string values into objects used by this handler. + * @param p the handler class name used as the prefix. + * @throws NullPointerException if the given argument is null. + * @throws SecurityException if not allowed. + */ + private void initEncoding(final String p) { + assert Thread.holdsLock(this); + try { + String e = fromLogManager(p.concat(".encoding")); + if (e != null) { + setEncoding0(e); + } + } catch (final SecurityException SE) { + throw SE; //Avoid catch all. + } catch (UnsupportedEncodingException | RuntimeException UEE) { + reportError(UEE.getMessage(), UEE, ErrorManager.OPEN_FAILURE); + } + } + + /** + * Used to get or create the default ErrorManager used before init. + * @return the super error manager or a new ErrorManager. + * @since JavaMail 1.5.3 + */ + private ErrorManager defaultErrorManager() { + ErrorManager em; + try { //Try to share the super error manager. + em = super.getErrorManager(); + } catch (RuntimeException | LinkageError ignore) { + em = null; + } + + //Don't assume that the super call is not null. + if (em == null) { + em = new ErrorManager(); + } + return em; + } + + /** + * Parses LogManager string values into objects used by this handler. + * @param p the handler class name used as the prefix. + * @throws NullPointerException if the given argument is null. + * @throws SecurityException if not allowed. + */ + private void initErrorManager(final String p) { + assert Thread.holdsLock(this); + try { + String name = fromLogManager(p.concat(".errorManager")); + if (name != null) { + setErrorManager0(LogManagerProperties.newErrorManager(name)); + } + } catch (final SecurityException SE) { + throw SE; //Avoid catch all. + } catch (final Exception E) { + reportError(E.getMessage(), E, ErrorManager.OPEN_FAILURE); + } + } + + /** + * Parses LogManager string values into objects used by this handler. + * @param p the handler class name used as the prefix. + * @throws NullPointerException if the given argument is null. + * @throws SecurityException if not allowed. + */ + private void initFormatter(final String p) { + assert Thread.holdsLock(this); + try { + String name = fromLogManager(p.concat(".formatter")); + if (hasValue(name)) { + final Formatter f + = LogManagerProperties.newFormatter(name); + assert f != null; + if (f instanceof TailNameFormatter == false) { + formatter = f; + } else { + formatter = createSimpleFormatter(); + } + } else { + formatter = createSimpleFormatter(); + } + } catch (final SecurityException SE) { + throw SE; //Avoid catch all. + } catch (final Exception E) { + reportError(E.getMessage(), E, ErrorManager.OPEN_FAILURE); + formatter = createSimpleFormatter(); + } + } + + /** + * Parses LogManager string values into objects used by this handler. + * @param p the handler class name used as the prefix. + * @throws NullPointerException if the given argument is null. + * @throws SecurityException if not allowed. + */ + private void initComparator(final String p) { + assert Thread.holdsLock(this); + try { + String name = fromLogManager(p.concat(".comparator")); + String reverse = fromLogManager(p.concat(".comparator.reverse")); + if (hasValue(name)) { + comparator = LogManagerProperties.newComparator(name); + if (Boolean.parseBoolean(reverse)) { + assert comparator != null : "null"; + comparator = LogManagerProperties.reverseOrder(comparator); + } + } else { + if (!isEmpty(reverse)) { + throw new IllegalArgumentException( + "No comparator to reverse."); + } + } + } catch (final SecurityException SE) { + throw SE; //Avoid catch all. + } catch (final Exception E) { + reportError(E.getMessage(), E, ErrorManager.OPEN_FAILURE); + } + } + + /** + * Parses LogManager string values into objects used by this handler. + * @param p the handler class name used as the prefix. + * @throws NullPointerException if the given argument is null. + * @throws SecurityException if not allowed. + */ + private void initPushLevel(final String p) { + assert Thread.holdsLock(this); + try { + final String val = fromLogManager(p.concat(".pushLevel")); + if (val != null) { + this.pushLevel = Level.parse(val); + } + } catch (final RuntimeException RE) { + reportError(RE.getMessage(), RE, ErrorManager.OPEN_FAILURE); + } + + if (this.pushLevel == null) { + this.pushLevel = Level.OFF; + } + } + + /** + * Parses LogManager string values into objects used by this handler. + * @param p the handler class name used as the prefix. + * @throws NullPointerException if the given argument is null. + * @throws SecurityException if not allowed. + */ + private void initPushFilter(final String p) { + assert Thread.holdsLock(this); + try { + String name = fromLogManager(p.concat(".pushFilter")); + if (hasValue(name)) { + this.pushFilter = LogManagerProperties.newFilter(name); + } + } catch (final SecurityException SE) { + throw SE; //Avoid catch all. + } catch (final Exception E) { + reportError(E.getMessage(), E, ErrorManager.OPEN_FAILURE); + } + } + + /** + * Parses LogManager string values into objects used by this handler. + * @param p the handler class name used as the prefix. + * @throws NullPointerException if the given argument is null. + * @throws SecurityException if not allowed. + */ + private void initSubject(final String p) { + assert Thread.holdsLock(this); + String name = fromLogManager(p.concat(".subject")); + if (name == null) { //Soft dependency on CollectorFormatter. + name = "com.sun.mail.util.logging.CollectorFormatter"; + } + + if (hasValue(name)) { + try { + this.subjectFormatter = LogManagerProperties.newFormatter(name); + } catch (final SecurityException SE) { + throw SE; //Avoid catch all. + } catch (ClassNotFoundException + | ClassCastException literalSubject) { + this.subjectFormatter = TailNameFormatter.of(name); + } catch (final Exception E) { + this.subjectFormatter = TailNameFormatter.of(name); + reportError(E.getMessage(), E, ErrorManager.OPEN_FAILURE); + } + } else { //User has forced empty or literal null. + this.subjectFormatter = TailNameFormatter.of(name); + } + } + + /** + * Check if any attachment would actually format the given + * <tt>LogRecord</tt>. This method does not check if the handler + * is level is set to OFF or if the handler is closed. + * @param record a <tt>LogRecord</tt> + * @return true if the <tt>LogRecord</tt> would be formatted. + */ + private boolean isAttachmentLoggable(final LogRecord record) { + final Filter[] filters = readOnlyAttachmentFilters(); + for (int i = 0; i < filters.length; ++i) { + final Filter f = filters[i]; + if (f == null || f.isLoggable(record)) { + setMatchedPart(i); + return true; + } + } + return false; + } + + /** + * Check if this <tt>Handler</tt> would push after storing the + * <tt>LogRecord</tt> into its internal buffer. + * @param record a <tt>LogRecord</tt> + * @return true if the <tt>LogRecord</tt> triggers an email push. + * @throws NullPointerException if tryMutex was not called. + */ + private boolean isPushable(final LogRecord record) { + assert Thread.holdsLock(this); + final int value = getPushLevel().intValue(); + if (value == offValue || record.getLevel().intValue() < value) { + return false; + } + + final Filter push = getPushFilter(); + if (push == null) { + return true; + } + + final int match = getMatchedPart(); + if ((match == -1 && getFilter() == push) + || (match >= 0 && attachmentFilters[match] == push)) { + return true; + } else { + return push.isLoggable(record); + } + } + + /** + * Used to perform push or flush. + * @param priority true for high priority otherwise false for normal. + * @param code the error manager code. + */ + private void push(final boolean priority, final int code) { + if (tryMutex()) { + try { + final Message msg = writeLogRecords(code); + if (msg != null) { + send(msg, priority, code); + } + } catch (final LinkageError JDK8152515) { + reportLinkageError(JDK8152515, code); + } finally { + releaseMutex(); + } + } else { + reportUnPublishedError(null); + } + } + + /** + * Used to send the generated email or write its contents to the + * error manager for this handler. This method does not hold any + * locks so new records can be added to this handler during a send or + * failure. + * @param msg the message or null. + * @param priority true for high priority or false for normal. + * @param code the ErrorManager code. + * @throws NullPointerException if message is null. + */ + private void send(Message msg, boolean priority, int code) { + try { + envelopeFor(msg, priority); + final Object ccl = getAndSetContextClassLoader(MAILHANDLER_LOADER); + try { //JDK-8025251 + Transport.send(msg); //Calls save changes. + } finally { + getAndSetContextClassLoader(ccl); + } + } catch (final RuntimeException re) { + reportError(msg, re, code); + } catch (final Exception e) { + reportError(msg, e, code); + } + } + + /** + * Performs a sort on the records if needed. + * Any exception thrown during a sort is considered a formatting error. + */ + private void sort() { + assert Thread.holdsLock(this); + if (comparator != null) { + try { + if (size != 1) { + Arrays.sort(data, 0, size, comparator); + } else { + if (comparator.compare(data[0], data[0]) != 0) { + throw new IllegalArgumentException( + comparator.getClass().getName()); + } + } + } catch (final RuntimeException RE) { + reportError(RE.getMessage(), RE, ErrorManager.FORMAT_FAILURE); + } + } + } + + /** + * Formats all records in the buffer and places the output in a Message. + * This method under most conditions will catch, report, and continue when + * exceptions occur. This method holds a lock on this handler. + * @param code the error manager code. + * @return null if there are no records or is currently in a push. + * Otherwise a new message is created with a formatted message and + * attached session. + */ + private Message writeLogRecords(final int code) { + try { + synchronized (this) { + if (size > 0 && !isWriting) { + isWriting = true; + try { + return writeLogRecords0(); + } finally { + isWriting = false; + if (size > 0) { + reset(); + } + } + } + } + } catch (final RuntimeException re) { + reportError(re.getMessage(), re, code); + } catch (final Exception e) { + reportError(e.getMessage(), e, code); + } + return null; + } + + /** + * Formats all records in the buffer and places the output in a Message. + * This method under most conditions will catch, report, and continue when + * exceptions occur. + * + * @return null if there are no records or is currently in a push. Otherwise + * a new message is created with a formatted message and attached session. + * @throws MessagingException if there is a problem. + * @throws IOException if there is a problem. + * @throws RuntimeException if there is an unexpected problem. + * @since JavaMail 1.5.3 + */ + private Message writeLogRecords0() throws Exception { + assert Thread.holdsLock(this); + sort(); + if (session == null) { + initSession(); + } + MimeMessage msg = new MimeMessage(session); + + /** + * Parts are lazily created when an attachment performs a getHead + * call. Therefore, a null part at an index means that the head is + * required. + */ + MimeBodyPart[] parts = new MimeBodyPart[attachmentFormatters.length]; + + /** + * The buffers are lazily created when the part requires a getHead. + */ + StringBuilder[] buffers = new StringBuilder[parts.length]; + StringBuilder buf = null; + final MimePart body; + if (parts.length == 0) { + msg.setDescription(descriptionFrom( + getFormatter(), getFilter(), subjectFormatter)); + body = msg; + } else { + msg.setDescription(descriptionFrom( + comparator, pushLevel, pushFilter)); + body = createBodyPart(); + } + + appendSubject(msg, head(subjectFormatter)); + final Formatter bodyFormat = getFormatter(); + final Filter bodyFilter = getFilter(); + + Locale lastLocale = null; + for (int ix = 0; ix < size; ++ix) { + boolean formatted = false; + final int match = matched[ix]; + final LogRecord r = data[ix]; + data[ix] = null; //Clear while formatting. + + final Locale locale = localeFor(r); + appendSubject(msg, format(subjectFormatter, r)); + Filter lmf = null; //Identity of last matched filter. + if (bodyFilter == null || match == -1 || parts.length == 0 + || (match < -1 && bodyFilter.isLoggable(r))) { + lmf = bodyFilter; + if (buf == null) { + buf = new StringBuilder(); + buf.append(head(bodyFormat)); + } + formatted = true; + buf.append(format(bodyFormat, r)); + if (locale != null && !locale.equals(lastLocale)) { + appendContentLang(body, locale); + } + } + + for (int i = 0; i < parts.length; ++i) { + //A match index less than the attachment index means that + //the filter has not seen this record. + final Filter af = attachmentFilters[i]; + if (af == null || lmf == af || match == i + || (match < i && af.isLoggable(r))) { + if (lmf == null && af != null) { + lmf = af; + } + if (parts[i] == null) { + parts[i] = createBodyPart(i); + buffers[i] = new StringBuilder(); + buffers[i].append(head(attachmentFormatters[i])); + appendFileName(parts[i], head(attachmentNames[i])); + } + formatted = true; + appendFileName(parts[i], format(attachmentNames[i], r)); + buffers[i].append(format(attachmentFormatters[i], r)); + if (locale != null && !locale.equals(lastLocale)) { + appendContentLang(parts[i], locale); + } + } + } + + if (formatted) { + if (body != msg && locale != null + && !locale.equals(lastLocale)) { + appendContentLang(msg, locale); + } + } else { //Belongs to no mime part. + reportFilterError(r); + } + lastLocale = locale; + } + this.size = 0; + + for (int i = parts.length - 1; i >= 0; --i) { + if (parts[i] != null) { + appendFileName(parts[i], tail(attachmentNames[i], "err")); + buffers[i].append(tail(attachmentFormatters[i], "")); + + if (buffers[i].length() > 0) { + String name = parts[i].getFileName(); + if (isEmpty(name)) { //Exceptional case. + name = toString(attachmentFormatters[i]); + parts[i].setFileName(name); + } + setContent(parts[i], buffers[i], getContentType(name)); + } else { + setIncompleteCopy(msg); + parts[i] = null; //Skip this part. + } + buffers[i] = null; + } + } + + if (buf != null) { + buf.append(tail(bodyFormat, "")); + //This body part is always added, even if the buffer is empty, + //so the body is never considered an incomplete-copy. + } else { + buf = new StringBuilder(0); + } + + appendSubject(msg, tail(subjectFormatter, "")); + + String contentType = contentTypeOf(buf); + String altType = contentTypeOf(bodyFormat); + setContent(body, buf, altType == null ? contentType : altType); + if (body != msg) { + final MimeMultipart multipart = new MimeMultipart(); + //assert body instanceof BodyPart : body; + multipart.addBodyPart((BodyPart) body); + + for (int i = 0; i < parts.length; ++i) { + if (parts[i] != null) { + multipart.addBodyPart(parts[i]); + } + } + msg.setContent(multipart); + } + + return msg; + } + + /** + * Checks all of the settings if the caller requests a verify and a verify + * was not performed yet and no verify is in progress. A verify is + * performed on create because this handler may be at the end of a handler + * chain and therefore may not see any log records until LogManager.reset() + * is called and at that time all of the settings have been cleared. + * @param session the current session or null. + * @since JavaMail 1.4.4 + */ + private void verifySettings(final Session session) { + try { + if (session != null) { + final Properties props = session.getProperties(); + final Object check = props.put("verify", ""); + if (check instanceof String) { + String value = (String) check; + //Perform the verify if needed. + if (hasValue(value)) { + verifySettings0(session, value); + } + } else { + if (check != null) { //Pass some invalid string. + verifySettings0(session, check.getClass().toString()); + } + } + } + } catch (final LinkageError JDK8152515) { + reportLinkageError(JDK8152515, ErrorManager.OPEN_FAILURE); + } + } + + /** + * Checks all of the settings using the given setting. + * This triggers the LogManagerProperties to copy all of the mail + * settings without explictly knowing them. Once all of the properties + * are copied this handler can handle LogManager.reset clearing all of the + * properties. It is expected that this method is, at most, only called + * once per session. + * @param session the current session. + * @param verify the type of verify to perform. + * @since JavaMail 1.4.4 + */ + private void verifySettings0(Session session, String verify) { + assert verify != null : (String) null; + if (!"local".equals(verify) && !"remote".equals(verify) + && !"limited".equals(verify) && !"resolve".equals(verify) + && !"login".equals(verify)) { + reportError("Verify must be 'limited', local', " + + "'resolve', 'login', or 'remote'.", + new IllegalArgumentException(verify), + ErrorManager.OPEN_FAILURE); + return; + } + + final MimeMessage abort = new MimeMessage(session); + final String msg; + if (!"limited".equals(verify)) { + msg = "Local address is " + + InternetAddress.getLocalAddress(session) + '.'; + + try { //Verify subclass or declared mime charset. + Charset.forName(getEncodingName()); + } catch (final RuntimeException RE) { + UnsupportedEncodingException UEE = + new UnsupportedEncodingException(RE.toString()); + UEE.initCause(RE); + reportError(msg, UEE, ErrorManager.FORMAT_FAILURE); + } + } else { + msg = "Skipping local address check."; + } + + //Perform all of the copy actions first. + String[] atn; + synchronized (this) { //Create the subject. + appendSubject(abort, head(subjectFormatter)); + appendSubject(abort, tail(subjectFormatter, "")); + atn = new String[attachmentNames.length]; + for (int i = 0; i < atn.length; ++i) { + atn[i] = head(attachmentNames[i]); + if (atn[i].length() == 0) { + atn[i] = tail(attachmentNames[i], ""); + } else { + atn[i] = atn[i].concat(tail(attachmentNames[i], "")); + } + } + } + + setIncompleteCopy(abort); //Original body part is never added. + envelopeFor(abort, true); + saveChangesNoContent(abort, msg); + try { + //Ensure transport provider is installed. + Address[] all = abort.getAllRecipients(); + if (all == null) { //Don't pass null to sendMessage. + all = new InternetAddress[0]; + } + Transport t; + try { + final Address[] any = all.length != 0 ? all : abort.getFrom(); + if (any != null && any.length != 0) { + t = session.getTransport(any[0]); + session.getProperty("mail.transport.protocol"); //Force copy + } else { + MessagingException me = new MessagingException( + "No recipient or from address."); + reportError(msg, me, ErrorManager.OPEN_FAILURE); + throw me; + } + } catch (final MessagingException protocol) { + //Switching the CCL emulates the current send behavior. + Object ccl = getAndSetContextClassLoader(MAILHANDLER_LOADER); + try { + t = session.getTransport(); + } catch (final MessagingException fail) { + throw attach(protocol, fail); + } finally { + getAndSetContextClassLoader(ccl); + } + } + + String local = null; + if ("remote".equals(verify) || "login".equals(verify)) { + MessagingException closed = null; + t.connect(); + try { + try { + //Capture localhost while connection is open. + local = getLocalHost(t); + + //A message without content will fail at message writeTo + //when sendMessage is called. This allows the handler + //to capture all mail properties set in the LogManager. + if ("remote".equals(verify)) { + t.sendMessage(abort, all); + } + } finally { + try { + t.close(); + } catch (final MessagingException ME) { + closed = ME; + } + } + //Close the transport before reporting errors. + if ("remote".equals(verify)) { + reportUnexpectedSend(abort, verify, null); + } else { + final String protocol = t.getURLName().getProtocol(); + verifyProperties(session, protocol); + } + } catch (final SendFailedException sfe) { + Address[] recip = sfe.getInvalidAddresses(); + if (recip != null && recip.length != 0) { + setErrorContent(abort, verify, sfe); + reportError(abort, sfe, ErrorManager.OPEN_FAILURE); + } + + recip = sfe.getValidSentAddresses(); + if (recip != null && recip.length != 0) { + reportUnexpectedSend(abort, verify, sfe); + } + } catch (final MessagingException ME) { + if (!isMissingContent(abort, ME)) { + setErrorContent(abort, verify, ME); + reportError(abort, ME, ErrorManager.OPEN_FAILURE); + } + } + + if (closed != null) { + setErrorContent(abort, verify, closed); + reportError(abort, closed, ErrorManager.CLOSE_FAILURE); + } + } else { + //Force a property copy, JDK-7092981. + final String protocol = t.getURLName().getProtocol(); + verifyProperties(session, protocol); + String mailHost = session.getProperty("mail." + + protocol + ".host"); + if (isEmpty(mailHost)) { + mailHost = session.getProperty("mail.host"); + } else { + session.getProperty("mail.host"); + } + + local = session.getProperty("mail." + protocol + ".localhost"); + if (isEmpty(local)) { + local = session.getProperty("mail." + + protocol + ".localaddress"); + } else { + session.getProperty("mail." + protocol + ".localaddress"); + } + + if ("resolve".equals(verify)) { + try { //Resolve the remote host name. + String transportHost = t.getURLName().getHost(); + if (!isEmpty(transportHost)) { + verifyHost(transportHost); + if (!transportHost.equalsIgnoreCase(mailHost)) { + verifyHost(mailHost); + } + } else { + verifyHost(mailHost); + } + } catch (final RuntimeException | IOException IOE) { + MessagingException ME = + new MessagingException(msg, IOE); + setErrorContent(abort, verify, ME); + reportError(abort, ME, ErrorManager.OPEN_FAILURE); + } + } + } + + if (!"limited".equals(verify)) { + try { //Verify host name and hit the host name cache. + if (!"remote".equals(verify) && !"login".equals(verify)) { + local = getLocalHost(t); + } + verifyHost(local); + } catch (final RuntimeException | IOException IOE) { + MessagingException ME = new MessagingException(msg, IOE); + setErrorContent(abort, verify, ME); + reportError(abort, ME, ErrorManager.OPEN_FAILURE); + } + + try { //Verify that the DataHandler can be loaded. + Object ccl = getAndSetContextClassLoader(MAILHANDLER_LOADER); + try { + //Always load the multipart classes. + MimeMultipart multipart = new MimeMultipart(); + MimeBodyPart[] ambp = new MimeBodyPart[atn.length]; + final MimeBodyPart body; + final String bodyContentType; + synchronized (this) { + bodyContentType = contentTypeOf(getFormatter()); + body = createBodyPart(); + for (int i = 0; i < atn.length; ++i) { + ambp[i] = createBodyPart(i); + ambp[i].setFileName(atn[i]); + //Convert names to mime type under lock. + atn[i] = getContentType(atn[i]); + } + } + + body.setDescription(verify); + setContent(body, "", bodyContentType); + multipart.addBodyPart(body); + for (int i = 0; i < ambp.length; ++i) { + ambp[i].setDescription(verify); + setContent(ambp[i], "", atn[i]); + } + + abort.setContent(multipart); + abort.saveChanges(); + abort.writeTo(new ByteArrayOutputStream(MIN_HEADER_SIZE)); + } finally { + getAndSetContextClassLoader(ccl); + } + } catch (final IOException IOE) { + MessagingException ME = new MessagingException(msg, IOE); + setErrorContent(abort, verify, ME); + reportError(abort, ME, ErrorManager.FORMAT_FAILURE); + } + } + + //Verify all recipients. + if (all.length != 0) { + verifyAddresses(all); + } else { + throw new MessagingException("No recipient addresses."); + } + + //Verify from and sender addresses. + Address[] from = abort.getFrom(); + Address sender = abort.getSender(); + if (sender instanceof InternetAddress) { + ((InternetAddress) sender).validate(); + } + + //If from address is declared then check sender. + if (abort.getHeader("From", ",") != null && from.length != 0) { + verifyAddresses(from); + for (int i = 0; i < from.length; ++i) { + if (from[i].equals(sender)) { + MessagingException ME = new MessagingException( + "Sender address '" + sender + + "' equals from address."); + throw new MessagingException(msg, ME); + } + } + } else { + if (sender == null) { + MessagingException ME = new MessagingException( + "No from or sender address."); + throw new MessagingException(msg, ME); + } + } + + //Verify reply-to addresses. + verifyAddresses(abort.getReplyTo()); + } catch (final RuntimeException RE) { + setErrorContent(abort, verify, RE); + reportError(abort, RE, ErrorManager.OPEN_FAILURE); + } catch (final Exception ME) { + setErrorContent(abort, verify, ME); + reportError(abort, ME, ErrorManager.OPEN_FAILURE); + } + } + + /** + * Handles all exceptions thrown when save changes is called on a message + * that doesn't have any content. + * + * @param abort the message requiring save changes. + * @param msg the error description. + * @since JavaMail 1.6.0 + */ + private void saveChangesNoContent(final Message abort, final String msg) { + if (abort != null) { + try { + try { + abort.saveChanges(); + } catch (final NullPointerException xferEncoding) { + //Workaround GNU JavaMail bug in MimeUtility.getEncoding + //when the mime message has no content. + try { + String cte = "Content-Transfer-Encoding"; + if (abort.getHeader(cte) == null) { + abort.setHeader(cte, "base64"); + abort.saveChanges(); + } else { + throw xferEncoding; + } + } catch (RuntimeException | MessagingException e) { + if (e != xferEncoding) { + e.addSuppressed(xferEncoding); + } + throw e; + } + } + } catch (RuntimeException | MessagingException ME) { + reportError(msg, ME, ErrorManager.FORMAT_FAILURE); + } + } + } + + /** + * Cache common session properties into the LogManagerProperties. This is + * a workaround for JDK-7092981. + * + * @param session the session. + * @param protocol the mail protocol. + * @throws NullPointerException if session is null. + * @since JavaMail 1.6.0 + */ + private static void verifyProperties(Session session, String protocol) { + session.getProperty("mail.from"); + session.getProperty("mail." + protocol + ".from"); + session.getProperty("mail.dsn.ret"); + session.getProperty("mail." + protocol + ".dsn.ret"); + session.getProperty("mail.dsn.notify"); + session.getProperty("mail." + protocol + ".dsn.notify"); + session.getProperty("mail." + protocol + ".port"); + session.getProperty("mail.user"); + session.getProperty("mail." + protocol + ".user"); + session.getProperty("mail." + protocol + ".localport"); + } + + /** + * Perform a lookup of the host address or FQDN. + * @param host the host or null. + * @return the address. + * @throws IOException if the host name is not valid. + * @throws SecurityException if security manager is present and doesn't + * allow access to check connect permission. + * @since JavaMail 1.5.0 + */ + private static InetAddress verifyHost(String host) throws IOException { + InetAddress a; + if (isEmpty(host)) { + a = InetAddress.getLocalHost(); + } else { + a = InetAddress.getByName(host); + } + if (a.getCanonicalHostName().length() == 0) { + throw new UnknownHostException(); + } + return a; + } + + /** + * Calls validate for every address given. + * If the addresses given are null, empty or not an InternetAddress then + * the check is skipped. + * @param all any address array, null or empty. + * @throws AddressException if there is a problem. + * @since JavaMail 1.4.5 + */ + private static void verifyAddresses(Address[] all) throws AddressException { + if (all != null) { + for (int i = 0; i < all.length; ++i) { + final Address a = all[i]; + if (a instanceof InternetAddress) { + ((InternetAddress) a).validate(); + } + } + } + } + + /** + * Reports that an empty content message was sent and should not have been. + * @param msg the MimeMessage. + * @param verify the verify enum. + * @param cause the exception that caused the problem or null. + * @since JavaMail 1.4.5 + */ + private void reportUnexpectedSend(MimeMessage msg, String verify, Exception cause) { + final MessagingException write = new MessagingException( + "An empty message was sent.", cause); + setErrorContent(msg, verify, write); + reportError(msg, write, ErrorManager.OPEN_FAILURE); + } + + /** + * Creates and sets the message content from the given Throwable. + * When verify fails, this method fixes the 'abort' message so that any + * created envelope data can be used in the error manager. + * @param msg the message with or without content. + * @param verify the verify enum. + * @param t the throwable or null. + * @since JavaMail 1.4.5 + */ + private void setErrorContent(MimeMessage msg, String verify, Throwable t) { + try { //Add content so toRawString doesn't fail. + final MimeBodyPart body; + final String subjectType; + final String msgDesc; + synchronized (this) { + body = createBodyPart(); + msgDesc = descriptionFrom(comparator, pushLevel, pushFilter); + subjectType = getClassId(subjectFormatter); + } + + body.setDescription("Formatted using " + + (t == null ? Throwable.class.getName() + : t.getClass().getName()) + ", filtered with " + + verify + ", and named by " + + subjectType + '.'); + setContent(body, toMsgString(t), "text/plain"); + final MimeMultipart multipart = new MimeMultipart(); + multipart.addBodyPart(body); + msg.setContent(multipart); + msg.setDescription(msgDesc); + setAcceptLang(msg); + msg.saveChanges(); + } catch (MessagingException | RuntimeException ME) { + reportError("Unable to create body.", ME, ErrorManager.OPEN_FAILURE); + } + } + + /** + * Used to update the cached session object based on changes in + * mail properties or authenticator. + * @return the current session or null if no verify is required. + */ + private Session updateSession() { + assert Thread.holdsLock(this); + final Session settings; + if (mailProps.getProperty("verify") != null) { + settings = initSession(); + assert settings == session : session; + } else { + session = null; //Remove old session. + settings = null; + } + return settings; + } + + /** + * Creates a session using a proxy properties object. + * @return the session that was created and assigned. + */ + private Session initSession() { + assert Thread.holdsLock(this); + final String p = getClass().getName(); + LogManagerProperties proxy = new LogManagerProperties(mailProps, p); + session = Session.getInstance(proxy, auth); + return session; + } + + /** + * Creates all of the envelope information for a message. + * This method is safe to call outside of a lock because the message + * provides the safe snapshot of the mail properties. + * @param msg the Message to write the envelope information. + * @param priority true for high priority. + */ + private void envelopeFor(Message msg, boolean priority) { + setAcceptLang(msg); + setFrom(msg); + if (!setRecipient(msg, "mail.to", Message.RecipientType.TO)) { + setDefaultRecipient(msg, Message.RecipientType.TO); + } + setRecipient(msg, "mail.cc", Message.RecipientType.CC); + setRecipient(msg, "mail.bcc", Message.RecipientType.BCC); + setReplyTo(msg); + setSender(msg); + setMailer(msg); + setAutoSubmitted(msg); + if (priority) { + setPriority(msg); + } + + try { + msg.setSentDate(new java.util.Date()); + } catch (final MessagingException ME) { + reportError(ME.getMessage(), ME, ErrorManager.FORMAT_FAILURE); + } + } + + /** + * Factory to create the in-line body part. + * @return a body part with default headers set. + * @throws MessagingException if there is a problem. + */ + private MimeBodyPart createBodyPart() throws MessagingException { + assert Thread.holdsLock(this); + final MimeBodyPart part = new MimeBodyPart(); + part.setDisposition(Part.INLINE); + part.setDescription(descriptionFrom(getFormatter(), + getFilter(), subjectFormatter)); + setAcceptLang(part); + return part; + } + + /** + * Factory to create the attachment body part. + * @param index the attachment index. + * @return a body part with default headers set. + * @throws MessagingException if there is a problem. + * @throws IndexOutOfBoundsException if the given index is not an valid + * attachment index. + */ + private MimeBodyPart createBodyPart(int index) throws MessagingException { + assert Thread.holdsLock(this); + final MimeBodyPart part = new MimeBodyPart(); + part.setDisposition(Part.ATTACHMENT); + part.setDescription(descriptionFrom( + attachmentFormatters[index], + attachmentFilters[index], + attachmentNames[index])); + setAcceptLang(part); + return part; + } + + /** + * Gets the description for the MimeMessage itself. + * The push level and filter are included because they play a role in + * formatting of a message when triggered or not triggered. + * @param c the comparator. + * @param l the pushLevel. + * @param f the pushFilter + * @return the description. + * @throws NullPointerException if level is null. + * @since JavaMail 1.4.5 + */ + private String descriptionFrom(Comparator<?> c, Level l, Filter f) { + return "Sorted using "+ (c == null ? "no comparator" + : c.getClass().getName()) + ", pushed when "+ l.getName() + + ", and " + (f == null ? "no push filter" + : f.getClass().getName()) + '.'; + } + + /** + * Creates a description for a body part. + * @param f the content formatter. + * @param filter the content filter. + * @param name the naming formatter. + * @return the description for the body part. + */ + private String descriptionFrom(Formatter f, Filter filter, Formatter name) { + return "Formatted using " + getClassId(f) + + ", filtered with " + (filter == null ? "no filter" + : filter.getClass().getName()) +", and named by " + + getClassId(name) + '.'; + } + + /** + * Gets a class name represents the behavior of the formatter. + * The class name may not be assignable to a Formatter. + * @param f the formatter. + * @return a class name that represents the given formatter. + * @throws NullPointerException if the parameter is null. + * @since JavaMail 1.4.5 + */ + private String getClassId(final Formatter f) { + if (f instanceof TailNameFormatter) { + return String.class.getName(); //Literal string. + } else { + return f.getClass().getName(); + } + } + + /** + * Ensure that a formatter creates a valid string for a part name. + * @param f the formatter. + * @return the to string value or the class name. + */ + private String toString(final Formatter f) { + //Should never be null but, guard against formatter bugs. + final String name = f.toString(); + if (!isEmpty(name)) { + return name; + } else { + return getClassId(f); + } + } + + /** + * Constructs a file name from a formatter. This method is called often + * but, rarely does any work. + * @param part to append to. + * @param chunk non null string to append. + */ + private void appendFileName(final Part part, final String chunk) { + if (chunk != null) { + if (chunk.length() > 0) { + appendFileName0(part, chunk); + } + } else { + reportNullError(ErrorManager.FORMAT_FAILURE); + } + } + + /** + * It is assumed that file names are short and that in most cases + * getTail will be the only method that will produce a result. + * @param part to append to. + * @param chunk non null string to append. + */ + private void appendFileName0(final Part part, String chunk) { + try { + //Remove all control character groups. + chunk = chunk.replaceAll("[\\x00-\\x1F\\x7F]+", ""); + final String old = part.getFileName(); + part.setFileName(old != null ? old.concat(chunk) : chunk); + } catch (final MessagingException ME) { + reportError(ME.getMessage(), ME, ErrorManager.FORMAT_FAILURE); + } + } + + /** + * Constructs a subject line from a formatter. + * @param msg to append to. + * @param chunk non null string to append. + */ + private void appendSubject(final Message msg, final String chunk) { + if (chunk != null) { + if (chunk.length() > 0) { + appendSubject0(msg, chunk); + } + } else { + reportNullError(ErrorManager.FORMAT_FAILURE); + } + } + + /** + * It is assumed that subject lines are short and that in most cases + * getTail will be the only method that will produce a result. + * @param msg to append to. + * @param chunk non null string to append. + */ + private void appendSubject0(final Message msg, String chunk) { + try { + //Remove all control character groups. + chunk = chunk.replaceAll("[\\x00-\\x1F\\x7F]+", ""); + final String charset = getEncodingName(); + final String old = msg.getSubject(); + assert msg instanceof MimeMessage : msg; + ((MimeMessage) msg).setSubject(old != null ? old.concat(chunk) + : chunk, MimeUtility.mimeCharset(charset)); + } catch (final MessagingException ME) { + reportError(ME.getMessage(), ME, ErrorManager.FORMAT_FAILURE); + } + } + + /** + * Gets the locale for the given log record from the resource bundle. + * If the resource bundle is using the root locale then the default locale + * is returned. + * @param r the log record. + * @return null if not localized otherwise, the locale of the record. + * @since JavaMail 1.4.5 + */ + private Locale localeFor(final LogRecord r) { + Locale l; + final ResourceBundle rb = r.getResourceBundle(); + if (rb != null) { + l = rb.getLocale(); + if (l == null || isEmpty(l.getLanguage())) { + //The language of the fallback bundle (root) is unknown. + //1. Use default locale. Should only be wrong if the app is + // used with a langauge that was unintended. (unlikely) + //2. Mark it as not localized (force null, info loss). + //3. Use the bundle name (encoded) as an experimental language. + l = Locale.getDefault(); + } + } else { + l = null; + } + return l; + } + + /** + * Appends the content language to the given mime part. + * The language tag is only appended if the given language has not been + * specified. This method is only used when we have LogRecords that are + * localized with an assigned resource bundle. + * @param p the mime part. + * @param l the locale to append. + * @throws NullPointerException if any argument is null. + * @since JavaMail 1.4.5 + */ + private void appendContentLang(final MimePart p, final Locale l) { + try { + String lang = LogManagerProperties.toLanguageTag(l); + if (lang.length() != 0) { + String header = p.getHeader("Content-Language", null); + if (isEmpty(header)) { + p.setHeader("Content-Language", lang); + } else if (!header.equalsIgnoreCase(lang)) { + lang = ",".concat(lang); + int idx = 0; + while ((idx = header.indexOf(lang, idx)) > -1) { + idx += lang.length(); + if (idx == header.length() + || header.charAt(idx) == ',') { + break; + } + } + + if (idx < 0) { + int len = header.lastIndexOf("\r\n\t"); + if (len < 0) { //If not folded. + len = (18 + 2) + header.length(); + } else { + len = (header.length() - len) + 8; + } + + //Perform folding of header if needed. + if ((len + lang.length()) > 76) { + header = header.concat("\r\n\t".concat(lang)); + } else { + header = header.concat(lang); + } + p.setHeader("Content-Language", header); + } + } + } + } catch (final MessagingException ME) { + reportError(ME.getMessage(), ME, ErrorManager.FORMAT_FAILURE); + } + } + + /** + * Sets the accept language to the default locale of the JVM. + * If the locale is the root locale the header is not added. + * @param p the part to set. + * @since JavaMail 1.4.5 + */ + private void setAcceptLang(final Part p) { + try { + final String lang = LogManagerProperties + .toLanguageTag(Locale.getDefault()); + if (lang.length() != 0) { + p.setHeader("Accept-Language", lang); + } + } catch (final MessagingException ME) { + reportError(ME.getMessage(), ME, ErrorManager.FORMAT_FAILURE); + } + } + + /** + * Used when a log record was loggable prior to being inserted + * into the buffer but at the time of formatting was no longer loggable. + * Filters were changed after publish but prior to a push or a bug in the + * body filter or one of the attachment filters. + * @param record that was not formatted. + * @since JavaMail 1.4.5 + */ + private void reportFilterError(final LogRecord record) { + assert Thread.holdsLock(this); + final Formatter f = createSimpleFormatter(); + final String msg = "Log record " + record.getSequenceNumber() + + " was filtered from all message parts. " + + head(f) + format(f, record) + tail(f, ""); + final String txt = getFilter() + ", " + + Arrays.asList(readOnlyAttachmentFilters()); + reportError(msg, new IllegalArgumentException(txt), + ErrorManager.FORMAT_FAILURE); + } + + /** + * Reports symmetric contract violations an equals implementation. + * @param o the test object must be non null. + * @param found the possible intern, must be non null. + * @throws NullPointerException if any argument is null. + * @since JavaMail 1.5.0 + */ + private void reportNonSymmetric(final Object o, final Object found) { + reportError("Non symmetric equals implementation." + , new IllegalArgumentException(o.getClass().getName() + + " is not equal to " + found.getClass().getName()) + , ErrorManager.OPEN_FAILURE); + } + + /** + * Reports equals implementations that do not discriminate between objects + * of different types or subclass types. + * @param o the test object must be non null. + * @param found the possible intern, must be non null. + * @throws NullPointerException if any argument is null. + * @since JavaMail 1.5.0 + */ + private void reportNonDiscriminating(final Object o, final Object found) { + reportError("Non discriminating equals implementation." + , new IllegalArgumentException(o.getClass().getName() + + " should not be equal to " + found.getClass().getName()) + , ErrorManager.OPEN_FAILURE); + } + + /** + * Used to outline the bytes to report a null pointer exception. + * See BUD ID 6533165. + * @param code the ErrorManager code. + */ + private void reportNullError(final int code) { + reportError("null", new NullPointerException(), code); + } + + /** + * Creates the head or reports a formatting error. + * @param f the formatter. + * @return the head string or an empty string. + */ + private String head(final Formatter f) { + try { + return f.getHead(this); + } catch (final RuntimeException RE) { + reportError(RE.getMessage(), RE, ErrorManager.FORMAT_FAILURE); + return ""; + } + } + + /** + * Creates the formatted log record or reports a formatting error. + * @param f the formatter. + * @param r the log record. + * @return the formatted string or an empty string. + */ + private String format(final Formatter f, final LogRecord r) { + try { + return f.format(r); + } catch (final RuntimeException RE) { + reportError(RE.getMessage(), RE, ErrorManager.FORMAT_FAILURE); + return ""; + } + } + + /** + * Creates the tail or reports a formatting error. + * @param f the formatter. + * @param def the default string to use when there is an error. + * @return the tail string or the given default string. + */ + private String tail(final Formatter f, final String def) { + try { + return f.getTail(this); + } catch (final RuntimeException RE) { + reportError(RE.getMessage(), RE, ErrorManager.FORMAT_FAILURE); + return def; + } + } + + /** + * Sets the x-mailer header. + * @param msg the target message. + */ + private void setMailer(final Message msg) { + try { + final Class<?> mail = MailHandler.class; + final Class<?> k = getClass(); + String value; + if (k == mail) { + value = mail.getName(); + } else { + try { + value = MimeUtility.encodeText(k.getName()); + } catch (final UnsupportedEncodingException E) { + reportError(E.getMessage(), E, ErrorManager.FORMAT_FAILURE); + value = k.getName().replaceAll("[^\\x00-\\x7F]", "\uu001A"); + } + value = MimeUtility.fold(10, mail.getName() + " using the " + + value + " extension."); + } + msg.setHeader("X-Mailer", value); + } catch (final MessagingException ME) { + reportError(ME.getMessage(), ME, ErrorManager.FORMAT_FAILURE); + } + } + + /** + * Sets the priority and importance headers. + * @param msg the target message. + */ + private void setPriority(final Message msg) { + try { + msg.setHeader("Importance", "High"); + msg.setHeader("Priority", "urgent"); + msg.setHeader("X-Priority", "2"); //High + } catch (final MessagingException ME) { + reportError(ME.getMessage(), ME, ErrorManager.FORMAT_FAILURE); + } + } + + /** + * Used to signal that body parts are missing from a message. Also used + * when LogRecords were passed to an attachment formatter but the formatter + * produced no output, which is allowed. Used during a verify because all + * parts are omitted, none of the content formatters are used. This is + * not used when a filter prevents LogRecords from being formatted. + * This header is defined in RFC 2156 and RFC 4021. + * @param msg the message. + * @since JavaMail 1.4.5 + */ + private void setIncompleteCopy(final Message msg) { + try { + msg.setHeader("Incomplete-Copy", ""); + } catch (final MessagingException ME) { + reportError(ME.getMessage(), ME, ErrorManager.FORMAT_FAILURE); + } + } + + /** + * Signals that this message was generated by automatic process. + * This header is defined in RFC 3834 section 5. + * @param msg the message. + * @since JavaMail 1.4.6 + */ + private void setAutoSubmitted(final Message msg) { + if (allowRestrictedHeaders()) { + try { //RFC 3834 (5.2) + msg.setHeader("auto-submitted", "auto-generated"); + } catch (final MessagingException ME) { + reportError(ME.getMessage(), ME, ErrorManager.FORMAT_FAILURE); + } + } + } + + /** + * Sets from address header. + * @param msg the target message. + */ + private void setFrom(final Message msg) { + final String from = getSession(msg).getProperty("mail.from"); + if (from != null) { + try { + final Address[] address = InternetAddress.parse(from, false); + if (address.length > 0) { + if (address.length == 1) { + msg.setFrom(address[0]); + } else { //Greater than 1 address. + msg.addFrom(address); + } + } + //Can't place an else statement here because the 'from' is + //not null which causes the local address computation + //to fail. Assume the user wants to omit the from address + //header. + } catch (final MessagingException ME) { + reportError(ME.getMessage(), ME, ErrorManager.FORMAT_FAILURE); + setDefaultFrom(msg); + } + } else { + setDefaultFrom(msg); + } + } + + /** + * Sets the from header to the local address. + * @param msg the target message. + */ + private void setDefaultFrom(final Message msg) { + try { + msg.setFrom(); + } catch (final MessagingException ME) { + reportError(ME.getMessage(), ME, ErrorManager.FORMAT_FAILURE); + } + } + + /** + * Computes the default to-address if none was specified. This can + * fail if the local address can't be computed. + * @param msg the message + * @param type the recipient type. + * @since JavaMail 1.5.0 + */ + private void setDefaultRecipient(final Message msg, + final Message.RecipientType type) { + try { + Address a = InternetAddress.getLocalAddress(getSession(msg)); + if (a != null) { + msg.setRecipient(type, a); + } else { + final MimeMessage m = new MimeMessage(getSession(msg)); + m.setFrom(); //Should throw an exception with a cause. + Address[] from = m.getFrom(); + if (from.length > 0) { + msg.setRecipients(type, from); + } else { + throw new MessagingException("No local address."); + } + } + } catch (MessagingException | RuntimeException ME) { + reportError("Unable to compute a default recipient.", + ME, ErrorManager.FORMAT_FAILURE); + } + } + + /** + * Sets reply-to address header. + * @param msg the target message. + */ + private void setReplyTo(final Message msg) { + final String reply = getSession(msg).getProperty("mail.reply.to"); + if (!isEmpty(reply)) { + try { + final Address[] address = InternetAddress.parse(reply, false); + if (address.length > 0) { + msg.setReplyTo(address); + } + } catch (final MessagingException ME) { + reportError(ME.getMessage(), ME, ErrorManager.FORMAT_FAILURE); + } + } + } + + /** + * Sets sender address header. + * @param msg the target message. + */ + private void setSender(final Message msg) { + assert msg instanceof MimeMessage : msg; + final String sender = getSession(msg).getProperty("mail.sender"); + if (!isEmpty(sender)) { + try { + final InternetAddress[] address = + InternetAddress.parse(sender, false); + if (address.length > 0) { + ((MimeMessage) msg).setSender(address[0]); + if (address.length > 1) { + reportError("Ignoring other senders.", + tooManyAddresses(address, 1), + ErrorManager.FORMAT_FAILURE); + } + } + } catch (final MessagingException ME) { + reportError(ME.getMessage(), ME, ErrorManager.FORMAT_FAILURE); + } + } + } + + /** + * A common factory used to create the too many addresses exception. + * @param address the addresses, never null. + * @param offset the starting address to display. + * @return the too many addresses exception. + */ + private AddressException tooManyAddresses(Address[] address, int offset) { + Object l = Arrays.asList(address).subList(offset, address.length); + return new AddressException(l.toString()); + } + + /** + * Sets the recipient for the given message. + * @param msg the message. + * @param key the key to search in the session. + * @param type the recipient type. + * @return true if the key was contained in the session. + */ + private boolean setRecipient(final Message msg, + final String key, final Message.RecipientType type) { + boolean containsKey; + final String value = getSession(msg).getProperty(key); + containsKey = value != null; + if (!isEmpty(value)) { + try { + final Address[] address = InternetAddress.parse(value, false); + if (address.length > 0) { + msg.setRecipients(type, address); + } + } catch (final MessagingException ME) { + reportError(ME.getMessage(), ME, ErrorManager.FORMAT_FAILURE); + } + } + return containsKey; + } + + /** + * Converts an email message to a raw string. This raw string + * is passed to the error manager to allow custom error managers + * to recreate the original MimeMessage object. + * @param msg a Message object. + * @return the raw string or null if msg was null. + * @throws MessagingException if there was a problem with the message. + * @throws IOException if there was a problem. + */ + private String toRawString(final Message msg) throws MessagingException, IOException { + if (msg != null) { + Object ccl = getAndSetContextClassLoader(MAILHANDLER_LOADER); + try { //JDK-8025251 + int nbytes = Math.max(msg.getSize() + MIN_HEADER_SIZE, MIN_HEADER_SIZE); + ByteArrayOutputStream out = new ByteArrayOutputStream(nbytes); + msg.writeTo(out); //Headers can be UTF-8 or US-ASCII. + return out.toString("UTF-8"); + } finally { + getAndSetContextClassLoader(ccl); + } + } else { //Must match this.reportError behavior, see push method. + return null; //Null is the safe choice. + } + } + + /** + * Converts a throwable to a message string. + * @param t any throwable or null. + * @return the throwable with a stack trace or the literal null. + */ + private String toMsgString(final Throwable t) { + if (t == null) { + return "null"; + } + + final String charset = getEncodingName(); + try { + final ByteArrayOutputStream out = + new ByteArrayOutputStream(MIN_HEADER_SIZE); + + //Create an output stream writer so streams are not double buffered. + try (OutputStreamWriter ows = new OutputStreamWriter(out, charset); + PrintWriter pw = new PrintWriter(ows)) { + pw.println(t.getMessage()); + t.printStackTrace(pw); + pw.flush(); + } //Close OSW before generating string. JDK-6995537 + return out.toString(charset); + } catch (final RuntimeException unexpected) { + return t.toString() + ' ' + unexpected.toString(); + } catch (final Exception badMimeCharset) { + return t.toString() + ' ' + badMimeCharset.toString(); + } + } + + /** + * Replaces the current context class loader with our class loader. + * @param ccl null for boot class loader, a class loader, a class used to + * get the class loader, or a source object to get the class loader. + * @return null for the boot class loader, a class loader, or a marker + * object to signal that no modification was required. + * @since JavaMail 1.5.3 + */ + private Object getAndSetContextClassLoader(final Object ccl) { + if (ccl != GetAndSetContext.NOT_MODIFIED) { + try { + final PrivilegedAction<?> pa; + if (ccl instanceof PrivilegedAction) { + pa = (PrivilegedAction<?>) ccl; + } else { + pa = new GetAndSetContext(ccl); + } + return AccessController.doPrivileged(pa); + } catch (final SecurityException ignore) { + } + } + return GetAndSetContext.NOT_MODIFIED; + } + + /** + * A factory used to create a common attachment mismatch type. + * @param msg the exception message. + * @return a RuntimeException to represent the type of error. + */ + private static RuntimeException attachmentMismatch(final String msg) { + return new IndexOutOfBoundsException(msg); + } + + /** + * Outline the attachment mismatch message. See Bug ID 6533165. + * @param expected the expected array length. + * @param found the array length that was given. + * @return a RuntimeException populated with a message. + */ + private static RuntimeException attachmentMismatch(int expected, int found) { + return attachmentMismatch("Attachments mismatched, expected " + + expected + " but given " + found + '.'); + } + + /** + * Try to attach a suppressed exception to a MessagingException in any order + * that is possible. + * @param required the exception expected to see as a reported failure. + * @param optional the suppressed exception. + * @return either the required or the optional exception. + */ + private static MessagingException attach( + MessagingException required, Exception optional) { + if (optional != null && !required.setNextException(optional)) { + if (optional instanceof MessagingException) { + final MessagingException head = (MessagingException) optional; + if (head.setNextException(required)) { + return head; + } + } + + if (optional != required) { + required.addSuppressed(optional); + } + } + return required; + } + + /** + * Gets the local host from the given service object. + * @param s the service to check. + * @return the local host or null. + * @since JavaMail 1.5.3 + */ + private String getLocalHost(final Service s) { + try { + return LogManagerProperties.getLocalHost(s); + } catch (SecurityException | NoSuchMethodException + | LinkageError ignore) { + } catch (final Exception ex) { + reportError(s.toString(), ex, ErrorManager.OPEN_FAILURE); + } + return null; + } + + /** + * Google App Engine doesn't support Message.getSession. + * @param msg the message. + * @return the session from the given message. + * @throws NullPointerException if the given message is null. + * @since JavaMail 1.5.3 + */ + private Session getSession(final Message msg) { + if (msg == null) { + throw new NullPointerException(); + } + return new MessageContext(msg).getSession(); + } + + /** + * Determines if restricted headers are allowed in the current environment. + * + * @return true if restricted headers are allowed. + * @since JavaMail 1.5.3 + */ + private boolean allowRestrictedHeaders() { + //GAE will prevent delivery of email with forbidden headers. + //Assume the environment is GAE if access to the LogManager is + //forbidden. + return LogManagerProperties.hasLogManager(); + } + + /** + * Outline the creation of the index error message. See JDK-6533165. + * @param i the index. + * @return the error message. + */ + private static String atIndexMsg(final int i) { + return "At index: " + i + '.'; + } + + /** + * Used for storing a password from the LogManager or literal string. + * @since JavaMail 1.4.6 + */ + private static final class DefaultAuthenticator extends Authenticator { + + /** + * Creates an Authenticator for the given password. This method is used + * so class verification of assignments in MailHandler doesn't require + * loading this class which otherwise can occur when using the + * constructor. Default access to avoid generating extra class files. + * + * @param pass the password. + * @return an Authenticator for the password. + * @since JavaMail 1.5.6 + */ + static Authenticator of(final String pass) { + return new DefaultAuthenticator(pass); + } + + /** + * The password to use. + */ + private final String pass; + + /** + * Use the factory method instead of this constructor. + * @param pass the password. + */ + private DefaultAuthenticator(final String pass) { + assert pass != null; + this.pass = pass; + } + + @Override + protected final PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(getDefaultUserName(), pass); + } + } + + /** + * Performs a get and set of the context class loader with privileges + * enabled. + * @since JavaMail 1.4.6 + */ + private static final class GetAndSetContext implements PrivilegedAction<Object> { + /** + * A marker object used to signal that the class loader was not + * modified. + */ + public static final Object NOT_MODIFIED = GetAndSetContext.class; + /** + * The source containing the class loader. + */ + private final Object source; + /** + * Create the action. + * @param source null for boot class loader, a class loader, a class + * used to get the class loader, or a source object to get the class + * loader. Default access to avoid generating extra class files. + */ + GetAndSetContext(final Object source) { + this.source = source; + } + + /** + * Gets the class loader from the source and sets the CCL only if + * the source and CCL are not the same. + * @return the replaced context class loader which can be null or + * NOT_MODIFIED to indicate that nothing was modified. + */ + @SuppressWarnings("override") //JDK-6954234 + public final Object run() { + final Thread current = Thread.currentThread(); + final ClassLoader ccl = current.getContextClassLoader(); + final ClassLoader loader; + if (source == null) { + loader = null; //boot class loader + } else if (source instanceof ClassLoader) { + loader = (ClassLoader) source; + } else if (source instanceof Class) { + loader = ((Class<?>) source).getClassLoader(); + } else if (source instanceof Thread) { + loader = ((Thread) source).getContextClassLoader(); + } else { + assert !(source instanceof Class) : source; + loader = source.getClass().getClassLoader(); + } + + if (ccl != loader) { + current.setContextClassLoader(loader); + return ccl; + } else { + return NOT_MODIFIED; + } + } + } + + /** + * Used for naming attachment file names and the main subject line. + */ + private static final class TailNameFormatter extends Formatter { + + /** + * Creates or gets a formatter from the given name. This method is used + * so class verification of assignments in MailHandler doesn't require + * loading this class which otherwise can occur when using the + * constructor. Default access to avoid generating extra class files. + * + * @param name any not null string. + * @return a formatter for that string. + * @since JavaMail 1.5.6 + */ + static Formatter of(final String name) { + return new TailNameFormatter(name); + } + + /** + * The value used as the output. + */ + private final String name; + + /** + * Use the factory method instead of this constructor. + * @param name any not null string. + */ + private TailNameFormatter(final String name) { + assert name != null; + this.name = name; + } + + @Override + public final String format(LogRecord record) { + return ""; + } + + @Override + public final String getTail(Handler h) { + return name; + } + + /** + * Equals method. + * @param o the other object. + * @return true if equal + * @since JavaMail 1.4.4 + */ + @Override + public final boolean equals(Object o) { + if (o instanceof TailNameFormatter) { + return name.equals(((TailNameFormatter) o).name); + } + return false; + } + + /** + * Hash code method. + * @return the hash code. + * @since JavaMail 1.4.4 + */ + @Override + public final int hashCode() { + return getClass().hashCode() + name.hashCode(); + } + + @Override + public final String toString() { + return name; + } + } +}
diff --git a/mail/src/main/java/com/sun/mail/util/logging/SeverityComparator.java b/mail/src/main/java/com/sun/mail/util/logging/SeverityComparator.java new file mode 100644 index 0000000..338a780 --- /dev/null +++ b/mail/src/main/java/com/sun/mail/util/logging/SeverityComparator.java
@@ -0,0 +1,332 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2018 Jason Mehrens. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package com.sun.mail.util.logging; + +import java.io.Serializable; +import java.util.Comparator; +import java.util.logging.Level; +import java.util.logging.LogRecord; + +/** + * Orders log records by level, thrown, sequence, and time. + * + * This comparator orders LogRecords by how severely each is attributed to + * failures in a program. The primary ordering is determined by the use of the + * logging API throughout a program by specifying a level to each log message. + * The secondary ordering is determined at runtime by the type of errors and + * exceptions generated by the program. The remaining ordering assumes that + * older log records are less severe than newer log records. + * + * <p> + * The following LogRecord properties determine severity ordering: + * <ol> + * <li> The natural comparison of the LogRecord + * {@linkplain Level#intValue level}. + * <li> The expected recovery order of {@linkplain LogRecord#getThrown() thrown} + * property of a LogRecord and its cause chain. This ordering is derived from + * the JLS 11.1.1. The Kinds of Exceptions and JLS 11.5 The Exception Hierarchy. + * This is performed by {@linkplain #apply(java.lang.Throwable) finding} the + * throwable that best describes the entire cause chain. Once a specific + * throwable of each chain is identified it is then ranked lowest to highest by + * the following rules: + * + * <ul> + * <li>All LogRecords with a {@code Throwable} defined as + * "{@link #isNormal(java.lang.Throwable) normal occurrence}". + * <li>All LogRecords that do not have a thrown object. + * <li>All checked exceptions. This is any class that is assignable to the + * {@code java.lang.Throwable} class and is not a + * {@code java.lang.RuntimeException} or a {@code java.lang.Error}. + * <li>All unchecked exceptions. This is all {@code java.lang.RuntimeException} + * objects. + * <li>All errors that indicate a serious problem. This is all + * {@code java.lang.Error} objects. + * </ul> + * <li> The natural comparison of the LogRecord + * {@linkplain LogRecord#getSequenceNumber() sequence}. + * <li> The natural comparison of the LogRecord + * {@linkplain LogRecord#getMillis() millis}. + * </ol> + * + * @author Jason Mehrens + * @since JavaMail 1.5.2 + */ +public class SeverityComparator implements Comparator<LogRecord>, Serializable { + + /** + * The generated serial version UID. + */ + private static final long serialVersionUID = -2620442245251791965L; + + /** + * A single instance that is shared among the logging package. + * The field is declared as java.util.Comparator so + * WebappClassLoader.clearReferencesStaticFinal() method will ignore this + * field. + */ + private static final Comparator<LogRecord> INSTANCE + = new SeverityComparator(); + + /** + * A shared instance of a SeverityComparator. This is package private so the + * public API is not polluted with more methods. + * + * @return a shared instance of a SeverityComparator. + */ + static SeverityComparator getInstance() { + return (SeverityComparator) INSTANCE; + } + + /** + * Identifies a single throwable that best describes the given throwable and + * the entire {@linkplain Throwable#getCause() cause} chain. This method can + * be overridden to change the behavior of + * {@link #compare(java.util.logging.LogRecord, java.util.logging.LogRecord)}. + * + * @param chain the throwable or null. + * @return null if null was given, otherwise the throwable that best + * describes the entire chain. + * @see #isNormal(java.lang.Throwable) + */ + public Throwable apply(final Throwable chain) { + //Matches the j.u.f.UnaryOperator<Throwable> interface. + int limit = 0; + Throwable root = chain; + Throwable high = null; + Throwable normal = null; + for (Throwable cause = chain; cause != null; cause = cause.getCause()) { + root = cause; //Find the deepest cause. + + //Find the deepest nomral occurrance. + if (isNormal(cause)) { + normal = cause; + } + + //Find the deepest error that happened before a normal occurance. + if (normal == null && cause instanceof Error) { + high = cause; + } + + //Deal with excessive cause chains and cyclic throwables. + if (++limit == (1 << 16)) { + break; //Give up. + } + } + return high != null ? high : normal != null ? normal : root; + } + + /** + * {@link #apply(java.lang.Throwable) Reduces} each throwable chain argument + * then compare each throwable result. + * + * @param tc1 the first throwable chain or null. + * @param tc2 the second throwable chain or null. + * @return a negative integer, zero, or a positive integer as the first + * argument is less than, equal to, or greater than the second. + * @see #apply(java.lang.Throwable) + * @see #compareThrowable(java.lang.Throwable, java.lang.Throwable) + */ + public final int applyThenCompare(Throwable tc1, Throwable tc2) { + return tc1 == tc2 ? 0 : compareThrowable(apply(tc1), apply(tc2)); + } + + /** + * Compares two throwable objects or null. This method does not + * {@link #apply(java.lang.Throwable) reduce} each argument before + * comparing. This is method can be overridden to change the behavior of + * {@linkplain #compare(LogRecord, LogRecord)}. + * + * @param t1 the first throwable or null. + * @param t2 the second throwable or null. + * @return a negative integer, zero, or a positive integer as the first + * argument is less than, equal to, or greater than the second. + * @see #isNormal(java.lang.Throwable) + */ + public int compareThrowable(final Throwable t1, final Throwable t2) { + if (t1 == t2) { //Reflexive test including null. + return 0; + } else { + //Only one or the other is null at this point. + //Force normal occurrence to be lower than null. + if (t1 == null) { + return isNormal(t2) ? 1 : -1; + } else { + if (t2 == null) { + return isNormal(t1) ? -1 : 1; + } + } + + //From this point on neither are null. + //Follow the shortcut if we can. + if (t1.getClass() == t2.getClass()) { + return 0; + } + + //Ensure normal occurrence flow control is ordered low. + if (isNormal(t1)) { + return isNormal(t2) ? 0 : -1; + } else { + if (isNormal(t2)) { + return 1; + } + } + + //Rank the two unidenticial throwables using the rules from + //JLS 11.1.1. The Kinds of Exceptions and + //JLS 11.5 The Exception Hierarchy. + if (t1 instanceof Error) { + return t2 instanceof Error ? 0 : 1; + } else if (t1 instanceof RuntimeException) { + return t2 instanceof Error ? -1 + : t2 instanceof RuntimeException ? 0 : 1; + } else { + return t2 instanceof Error + || t2 instanceof RuntimeException ? -1 : 0; + } + } + } + + /** + * Compares two log records based on severity. + * + * @param o1 the first log record. + * @param o2 the second log record. + * @return a negative integer, zero, or a positive integer as the first + * argument is less than, equal to, or greater than the second. + * @throws NullPointerException if either argument is null. + */ + @SuppressWarnings("override") //JDK-6954234 + public int compare(final LogRecord o1, final LogRecord o2) { + if (o1 == null || o2 == null) { //Don't allow null. + throw new NullPointerException(toString(o1, o2)); + } + + /** + * LogRecords are mutable so a reflexive relationship test is a safety + * requirement. + */ + if (o1 == o2) { + return 0; + } + + int cmp = compare(o1.getLevel(), o2.getLevel()); + if (cmp == 0) { + cmp = applyThenCompare(o1.getThrown(), o2.getThrown()); + if (cmp == 0) { + cmp = compare(o1.getSequenceNumber(), o2.getSequenceNumber()); + if (cmp == 0) { + cmp = compare(o1.getMillis(), o2.getMillis()); + } + } + } + return cmp; + } + + /** + * Determines if the given object is also a comparator and it imposes the + * same ordering as this comparator. + * + * @param o the reference object with which to compare. + * @return true if this object equal to the argument; false otherwise. + */ + @Override + public boolean equals(final Object o) { + return o == null ? false : o.getClass() == getClass(); + } + + /** + * Returns a hash code value for the object. + * + * @return Returns a hash code value for the object. + */ + @Override + public int hashCode() { + return 31 * getClass().hashCode(); + } + + /** + * Determines if the given throwable instance is "normal occurrence". This + * is any checked or unchecked exception with 'Interrupt' in the class name + * or ancestral class name. Any {@code java.lang.ThreadDeath} object or + * subclasses. + * + * This method can be overridden to change the behavior of the + * {@linkplain #apply(java.lang.Throwable)} method. + * + * @param t a throwable or null. + * @return true the given throwable is a "normal occurrence". + */ + public boolean isNormal(final Throwable t) { + if (t == null) { //This is only needed when called directly. + return false; + } + + /** + * Use the class names to avoid loading more classes. + */ + final Class<?> root = Throwable.class; + final Class<?> error = Error.class; + for (Class<?> c = t.getClass(); c != root; c = c.getSuperclass()) { + if (error.isAssignableFrom(c)) { + if (c.getName().equals("java.lang.ThreadDeath")) { + return true; + } + } else { + //Interrupt, Interrupted or Interruption. + if (c.getName().contains("Interrupt")) { + return true; + } + } + } + return false; + } + + /** + * Compare two level objects. + * + * @param a the first level. + * @param b the second level. + * @return a negative integer, zero, or a positive integer as the first + * argument is less than, equal to, or greater than the second. + */ + private int compare(final Level a, final Level b) { + return a == b ? 0 : compare(a.intValue(), b.intValue()); + } + + /** + * Outline the message create string. + * + * @param o1 argument one. + * @param o2 argument two. + * @return the message string. + */ + private static String toString(final Object o1, final Object o2) { + return o1 + ", " + o2; + } + + /** + * Compare two longs. Can be removed when JDK 1.7 is required. + * + * @param x the first long. + * @param y the second long. + * @return a negative integer, zero, or a positive integer as the first + * argument is less than, equal to, or greater than the second. + */ + private int compare(final long x, final long y) { + return x < y ? -1 : x > y ? 1 : 0; + } +}
diff --git a/mail/src/main/java/com/sun/mail/util/logging/package.html b/mail/src/main/java/com/sun/mail/util/logging/package.html new file mode 100644 index 0000000..48dabfb --- /dev/null +++ b/mail/src/main/java/com/sun/mail/util/logging/package.html
@@ -0,0 +1,34 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> +<html> + <head> +<!-- + + Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + Copyright (c) 2009, 2018 Jason Mehrens. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + + <title></title> + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> + </head> + <body> + Contains JavaMail™ extensions for + the Java™ platform's core logging + facilities. This package contains classes used to export log messages + as a formatted email message. Classes in this package typically use + LogManager properties to set default values; see the specific + documentation for each concrete class. + </body> +</html>
diff --git a/mail/src/main/java/com/sun/mail/util/package.html b/mail/src/main/java/com/sun/mail/util/package.html new file mode 100644 index 0000000..db7a8ea --- /dev/null +++ b/mail/src/main/java/com/sun/mail/util/package.html
@@ -0,0 +1,60 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> +<HTML> +<HEAD> +<meta http-equiv="Content-Type" content="text/html; charset=utf-8"> +<!-- + + Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<TITLE>com.sun.mail.util package</TITLE> +</HEAD> +<BODY BGCOLOR="white"> + +<P> +Utility classes for use with the JavaMail API. +These utility classes are not part of the JavaMail specification. +While this package contains many classes used by the JavaMail implementation +and not intended for direct use by applications, the classes documented +here may be of use to applications. +</P> +<P> +Classes in this package log debugging information using +{@link java.util.logging} as described in the following table: +</P> +<TABLE BORDER SUMMARY="com.sun.mail.util Loggers"> +<TR> +<TH>Logger Name</TH> +<TH>Logging Level</TH> +<TH>Purpose</TH> +</TR> + +<TR> +<TD>com.sun.mail.util.socket</TD> +<TD>FINER</TD> +<TD>Debugging output related to creating sockets</TD> +</TR> +</TABLE> + +<P> +<strong>WARNING:</strong> The APIs in this package should be +considered <strong>EXPERIMENTAL</strong>. They may be changed in the +future in ways that are incompatible with applications using the +current APIs. +</P> + +</BODY> +</HTML>
diff --git a/mail/src/main/java/javax/mail/Address.java b/mail/src/main/java/javax/mail/Address.java new file mode 100644 index 0000000..2b87cf4 --- /dev/null +++ b/mail/src/main/java/javax/mail/Address.java
@@ -0,0 +1,66 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +import java.io.Serializable; + +/** + * This abstract class models the addresses in a message. + * Subclasses provide specific implementations. Subclasses + * will typically be serializable so that (for example) the + * use of Address objects in search terms can be serialized + * along with the search terms. + * + * @author John Mani + * @author Bill Shannon + */ + +public abstract class Address implements Serializable { + + private static final long serialVersionUID = -5822459626751992278L; + + /** + * Return a type string that identifies this address type. + * + * @return address type + * @see javax.mail.internet.InternetAddress + */ + public abstract String getType(); + + /** + * Return a String representation of this address object. + * + * @return string representation of this address + */ + @Override + public abstract String toString(); + + /** + * The equality operator. Subclasses should provide an + * implementation of this method that supports value equality + * (do the two Address objects represent the same destination?), + * not object reference equality. A subclass must also provide + * a corresponding implementation of the <code>hashCode</code> + * method that preserves the general contract of + * <code>equals</code> and <code>hashCode</code> - objects that + * compare as equal must have the same hashCode. + * + * @param address Address object + */ + @Override + public abstract boolean equals(Object address); +}
diff --git a/mail/src/main/java/javax/mail/AuthenticationFailedException.java b/mail/src/main/java/javax/mail/AuthenticationFailedException.java new file mode 100644 index 0000000..e0de809 --- /dev/null +++ b/mail/src/main/java/javax/mail/AuthenticationFailedException.java
@@ -0,0 +1,60 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +/** + * This exception is thrown when the connect method on a Store or + * Transport object fails due to an authentication failure (e.g., + * bad user name or password). + * + * @author Bill Shannon + */ + +public class AuthenticationFailedException extends MessagingException { + + private static final long serialVersionUID = 492080754054436511L; + + /** + * Constructs an AuthenticationFailedException. + */ + public AuthenticationFailedException() { + super(); + } + + /** + * Constructs an AuthenticationFailedException with the specified + * detail message. + * + * @param message The detailed error message + */ + public AuthenticationFailedException(String message) { + super(message); + } + + /** + * Constructs an AuthenticationFailedException with the specified + * detail message and embedded exception. The exception is chained + * to this exception. + * + * @param message The detailed error message + * @param e The embedded exception + * @since JavaMail 1.5 + */ + public AuthenticationFailedException(String message, Exception e) { + super(message, e); + } +}
diff --git a/mail/src/main/java/javax/mail/Authenticator.java b/mail/src/main/java/javax/mail/Authenticator.java new file mode 100644 index 0000000..5fa7838 --- /dev/null +++ b/mail/src/main/java/javax/mail/Authenticator.java
@@ -0,0 +1,138 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +import java.net.InetAddress; + +/** + * The class Authenticator represents an object that knows how to obtain + * authentication for a network connection. Usually, it will do this + * by prompting the user for information. + * <p> + * Applications use this class by creating a subclass, and registering + * an instance of that subclass with the session when it is created. + * When authentication is required, the system will invoke a method + * on the subclass (like getPasswordAuthentication). The subclass's + * method can query about the authentication being requested with a + * number of inherited methods (getRequestingXXX()), and form an + * appropriate message for the user. + * <p> + * All methods that request authentication have a default implementation + * that fails. + * + * @see java.net.Authenticator + * @see javax.mail.Session#getInstance(java.util.Properties, + * javax.mail.Authenticator) + * @see javax.mail.Session#getDefaultInstance(java.util.Properties, + * javax.mail.Authenticator) + * @see javax.mail.Session#requestPasswordAuthentication + * @see javax.mail.PasswordAuthentication + * + * @author Bill Foote + * @author Bill Shannon + */ + +// There are no abstract methods, but to be useful the user must +// subclass. +public abstract class Authenticator { + + private InetAddress requestingSite; + private int requestingPort; + private String requestingProtocol; + private String requestingPrompt; + private String requestingUserName; + + /** + * Ask the authenticator for a password. + * <p> + * + * @param addr The InetAddress of the site requesting authorization, + * or null if not known. + * @param port the port for the requested connection + * @param protocol The protocol that's requesting the connection + * (@see java.net.Authenticator.getProtocol()) + * @param prompt A prompt string for the user + * + * @return The username/password, or null if one can't be gotten. + */ + final synchronized PasswordAuthentication requestPasswordAuthentication( + InetAddress addr, int port, String protocol, + String prompt, String defaultUserName) { + requestingSite = addr; + requestingPort = port; + requestingProtocol = protocol; + requestingPrompt = prompt; + requestingUserName = defaultUserName; + return getPasswordAuthentication(); + } + + /** + * @return the InetAddress of the site requesting authorization, or null + * if it's not available. + */ + protected final InetAddress getRequestingSite() { + return requestingSite; + } + + /** + * @return the port for the requested connection + */ + protected final int getRequestingPort() { + return requestingPort; + } + + /** + * Give the protocol that's requesting the connection. Often this + * will be based on a URLName. + * + * @return the protcol + * + * @see javax.mail.URLName#getProtocol + */ + protected final String getRequestingProtocol() { + return requestingProtocol; + } + + /** + * @return the prompt string given by the requestor + */ + protected final String getRequestingPrompt() { + return requestingPrompt; + } + + /** + * @return the default user name given by the requestor + */ + protected final String getDefaultUserName() { + return requestingUserName; + } + + /** + * Called when password authentication is needed. Subclasses should + * override the default implementation, which returns null. <p> + * + * Note that if this method uses a dialog to prompt the user for this + * information, the dialog needs to block until the user supplies the + * information. This method can not simply return after showing the + * dialog. + * @return The PasswordAuthentication collected from the + * user, or null if none is provided. + */ + protected PasswordAuthentication getPasswordAuthentication() { + return null; + } +}
diff --git a/mail/src/main/java/javax/mail/BodyPart.java b/mail/src/main/java/javax/mail/BodyPart.java new file mode 100644 index 0000000..bb94f75 --- /dev/null +++ b/mail/src/main/java/javax/mail/BodyPart.java
@@ -0,0 +1,60 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +/** + * This class models a Part that is contained within a Multipart. + * This is an abstract class. Subclasses provide actual implementations.<p> + * + * BodyPart implements the Part interface. Thus, it contains a set of + * attributes and a "content". + * + * @author John Mani + * @author Bill Shannon + */ + +public abstract class BodyPart implements Part { + + /** + * The <code>Multipart</code> object containing this <code>BodyPart</code>, + * if known. + * @since JavaMail 1.1 + */ + protected Multipart parent; + + /** + * Return the containing <code>Multipart</code> object, + * or <code>null</code> if not known. + * + * @return the parent Multipart + */ + public Multipart getParent() { + return parent; + } + + /** + * Set the parent of this <code>BodyPart</code> to be the specified + * <code>Multipart</code>. Normally called by <code>Multipart</code>'s + * <code>addBodyPart</code> method. <code>parent</code> may be + * <code>null</code> if the <code>BodyPart</code> is being removed + * from its containing <code>Multipart</code>. + * @since JavaMail 1.1 + */ + void setParent(Multipart parent) { + this.parent = parent; + } +}
diff --git a/mail/src/main/java/javax/mail/EncodingAware.java b/mail/src/main/java/javax/mail/EncodingAware.java new file mode 100644 index 0000000..27efc70 --- /dev/null +++ b/mail/src/main/java/javax/mail/EncodingAware.java
@@ -0,0 +1,55 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +/** + * A {@link javax.activation.DataSource DataSource} that also implements + * <code>EncodingAware</code> may specify the Content-Transfer-Encoding + * to use for its data. Valid Content-Transfer-Encoding values specified + * by RFC 2045 are "7bit", "8bit", "quoted-printable", "base64", and "binary". + * <p> + * For example, a {@link javax.activation.FileDataSource FileDataSource} + * could be created that forces all files to be base64 encoded: + * <blockquote><pre> + * public class Base64FileDataSource extends FileDataSource + * implements EncodingAware { + * public Base64FileDataSource(File file) { + * super(file); + * } + * + * // implements EncodingAware.getEncoding() + * public String getEncoding() { + * return "base64"; + * } + * } + * </pre></blockquote><p> + * + * @since JavaMail 1.5 + * @author Bill Shannon + */ + +public interface EncodingAware { + + /** + * Return the MIME Content-Transfer-Encoding to use for this data, + * or null to indicate that an appropriate value should be chosen + * by the caller. + * + * @return the Content-Transfer-Encoding value, or null + */ + public String getEncoding(); +}
diff --git a/mail/src/main/java/javax/mail/EventQueue.java b/mail/src/main/java/javax/mail/EventQueue.java new file mode 100644 index 0000000..1264ebd --- /dev/null +++ b/mail/src/main/java/javax/mail/EventQueue.java
@@ -0,0 +1,159 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +import java.util.EventListener; +import java.util.Vector; +import java.util.Queue; +import java.util.WeakHashMap; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.Executor; +import javax.mail.event.MailEvent; + +/** + * Package private class used by Store & Folder to dispatch events. + * This class implements an event queue, and a dispatcher thread that + * dequeues and dispatches events from the queue. + * + * @author Bill Shannon + */ +class EventQueue implements Runnable { + + private volatile BlockingQueue<QueueElement> q; + private Executor executor; + + private static WeakHashMap<ClassLoader,EventQueue> appq; + + /** + * A special event that causes the queue processing task to terminate. + */ + static class TerminatorEvent extends MailEvent { + private static final long serialVersionUID = -2481895000841664111L; + + TerminatorEvent() { + super(new Object()); + } + + @Override + public void dispatch(Object listener) { + // Kill the event dispatching thread. + Thread.currentThread().interrupt(); + } + } + + /** + * A "struct" to put on the queue. + */ + static class QueueElement { + MailEvent event = null; + Vector<? extends EventListener> vector = null; + + QueueElement(MailEvent event, Vector<? extends EventListener> vector) { + this.event = event; + this.vector = vector; + } + } + + /** + * Construct an EventQueue using the specified Executor. + * If the Executor is null, threads will be created as needed. + */ + EventQueue(Executor ex) { + this.executor = ex; + } + + /** + * Enqueue an event. + */ + synchronized void enqueue(MailEvent event, + Vector<? extends EventListener> vector) { + // if this is the first event, create the queue and start the event task + if (q == null) { + q = new LinkedBlockingQueue<>(); + if (executor != null) { + executor.execute(this); + } else { + Thread qThread = new Thread(this, "JavaMail-EventQueue"); + qThread.setDaemon(true); // not a user thread + qThread.start(); + } + } + q.add(new QueueElement(event, vector)); + } + + /** + * Terminate the task running the queue, but only if there is a queue. + */ + synchronized void terminateQueue() { + if (q != null) { + Vector<EventListener> dummyListeners = new Vector<>(); + dummyListeners.setSize(1); // need atleast one listener + q.add(new QueueElement(new TerminatorEvent(), dummyListeners)); + q = null; + } + } + + /** + * Create (if necessary) an application-scoped event queue. + * Application scoping is based on the thread's context class loader. + */ + static synchronized EventQueue getApplicationEventQueue(Executor ex) { + ClassLoader cl = Session.getContextClassLoader(); + if (appq == null) + appq = new WeakHashMap<>(); + EventQueue q = appq.get(cl); + if (q == null) { + q = new EventQueue(ex); + appq.put(cl, q); + } + return q; + } + + /** + * Pull events off the queue and dispatch them. + */ + @Override + public void run() { + + BlockingQueue<QueueElement> bq = q; + if (bq == null) + return; + try { + loop: + for (;;) { + // block until an item is available + QueueElement qe = bq.take(); + MailEvent e = qe.event; + Vector<? extends EventListener> v = qe.vector; + + for (int i = 0; i < v.size(); i++) + try { + e.dispatch(v.elementAt(i)); + } catch (Throwable t) { + if (t instanceof InterruptedException) + break loop; + // ignore anything else thrown by the listener + } + + qe = null; e = null; v = null; + } + } catch (InterruptedException e) { + // just die + } + } +}
diff --git a/mail/src/main/java/javax/mail/FetchProfile.java b/mail/src/main/java/javax/mail/FetchProfile.java new file mode 100644 index 0000000..1b49454 --- /dev/null +++ b/mail/src/main/java/javax/mail/FetchProfile.java
@@ -0,0 +1,223 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +import java.util.Vector; + +/** + * Clients use a FetchProfile to list the Message attributes that + * it wishes to prefetch from the server for a range of messages.<p> + * + * Messages obtained from a Folder are light-weight objects that + * typically start off as empty references to the actual messages. + * Such a Message object is filled in "on-demand" when the appropriate + * get*() methods are invoked on that particular Message. Certain + * server-based message access protocols (Ex: IMAP) allow batch + * fetching of message attributes for a range of messages in a single + * request. Clients that want to use message attributes for a range of + * Messages (Example: to display the top-level headers in a headerlist) + * might want to use the optimization provided by such servers. The + * <code>FetchProfile</code> allows the client to indicate this desire + * to the server. <p> + * + * Note that implementations are not obligated to support + * FetchProfiles, since there might be cases where the backend service + * does not allow easy, efficient fetching of such profiles. <p> + * + * Sample code that illustrates the use of a FetchProfile is given + * below: + * <blockquote> + * <pre> + * + * Message[] msgs = folder.getMessages(); + * + * FetchProfile fp = new FetchProfile(); + * fp.add(FetchProfile.Item.ENVELOPE); + * fp.add("X-mailer"); + * folder.fetch(msgs, fp); + * + * </pre></blockquote><p> + * + * @see javax.mail.Folder#fetch + * @author John Mani + * @author Bill Shannon + */ + +public class FetchProfile { + + private Vector<Item> specials; // specials + private Vector<String> headers; // vector of header names + + /** + * This inner class is the base class of all items that + * can be requested in a FetchProfile. The items currently + * defined here are <code>ENVELOPE</code>, <code>CONTENT_INFO</code> + * and <code>FLAGS</code>. The <code>UIDFolder</code> interface + * defines the <code>UID</code> Item as well. <p> + * + * Note that this class only has a protected constructor, therby + * restricting new Item types to either this class or subclasses. + * This effectively implements a enumeration of allowed Item types. + * + * @see UIDFolder + */ + + public static class Item { + /** + * This is the Envelope item. <p> + * + * The Envelope is an aggregration of the common attributes + * of a Message. Implementations should include the following + * attributes: From, To, Cc, Bcc, ReplyTo, Subject and Date. + * More items may be included as well. <p> + * + * For implementations of the IMAP4 protocol (RFC 2060), the + * Envelope should include the ENVELOPE data item. More items + * may be included too. + */ + public static final Item ENVELOPE = new Item("ENVELOPE"); + + /** + * This item is for fetching information about the + * content of the message. <p> + * + * This includes all the attributes that describe the content + * of the message. Implementations should include the following + * attributes: ContentType, ContentDisposition, + * ContentDescription, Size and LineCount. Other items may be + * included as well. + */ + public static final Item CONTENT_INFO = new Item("CONTENT_INFO"); + + /** + * SIZE is a fetch profile item that can be included in a + * <code>FetchProfile</code> during a fetch request to a Folder. + * This item indicates that the sizes of the messages in the specified + * range should be prefetched. <p> + * + * @since JavaMail 1.5 + */ + public static final Item SIZE = new Item("SIZE"); + + /** + * This is the Flags item. + */ + public static final Item FLAGS = new Item("FLAGS"); + + private String name; + + /** + * Constructor for an item. The name is used only for debugging. + * + * @param name the item name + */ + protected Item(String name) { + this.name = name; + } + + /** + * Include the name in the toString return value for debugging. + */ + @Override + public String toString() { + return getClass().getName() + "[" + name + "]"; + } + } + + /** + * Create an empty FetchProfile. + */ + public FetchProfile() { + specials = null; + headers = null; + } + + /** + * Add the given special item as one of the attributes to + * be prefetched. + * + * @param item the special item to be fetched + * @see FetchProfile.Item#ENVELOPE + * @see FetchProfile.Item#CONTENT_INFO + * @see FetchProfile.Item#FLAGS + */ + public void add(Item item) { + if (specials == null) + specials = new Vector<>(); + specials.addElement(item); + } + + /** + * Add the specified header-field to the list of attributes + * to be prefetched. + * + * @param headerName header to be prefetched + */ + public void add(String headerName) { + if (headers == null) + headers = new Vector<>(); + headers.addElement(headerName); + } + + /** + * Returns true if the fetch profile contains the given special item. + * + * @param item the Item to test + * @return true if the fetch profile contains the given special item + */ + public boolean contains(Item item) { + return specials != null && specials.contains(item); + } + + /** + * Returns true if the fetch profile contains the given header name. + * + * @param headerName the header to test + * @return true if the fetch profile contains the given header name + */ + public boolean contains(String headerName) { + return headers != null && headers.contains(headerName); + } + + /** + * Get the items set in this profile. + * + * @return items set in this profile + */ + public Item[] getItems() { + if (specials == null) + return new Item[0]; + + Item[] s = new Item[specials.size()]; + specials.copyInto(s); + return s; + } + + /** + * Get the names of the header-fields set in this profile. + * + * @return headers set in this profile + */ + public String[] getHeaderNames() { + if (headers == null) + return new String[0]; + + String[] s = new String[headers.size()]; + headers.copyInto(s); + return s; + } +}
diff --git a/mail/src/main/java/javax/mail/Flags.java b/mail/src/main/java/javax/mail/Flags.java new file mode 100644 index 0000000..f97cad3 --- /dev/null +++ b/mail/src/main/java/javax/mail/Flags.java
@@ -0,0 +1,667 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +import java.io.Serializable; +import java.util.*; + +/** + * The Flags class represents the set of flags on a Message. Flags + * are composed of predefined system flags, and user defined flags. <p> + * + * A System flag is represented by the <code>Flags.Flag</code> + * inner class. A User defined flag is represented as a String. + * User flags are case-independent. <p> + * + * A set of standard system flags are predefined. Most folder + * implementations are expected to support these flags. Some + * implementations may also support arbitrary user-defined flags. The + * <code>getPermanentFlags</code> method on a Folder returns a Flags + * object that holds all the flags that are supported by that folder + * implementation. <p> + * + * A Flags object is serializable so that (for example) the + * use of Flags objects in search terms can be serialized + * along with the search terms. <p> + * + * <strong>Warning:</strong> + * Serialized objects of this class may not be compatible with future + * JavaMail API releases. The current serialization support is + * appropriate for short term storage. <p> + * + * The below code sample illustrates how to set, examine, and get the + * flags for a message. + * <pre> + * + * Message m = folder.getMessage(1); + * m.setFlag(Flags.Flag.DELETED, true); // set the DELETED flag + * + * // Check if DELETED flag is set on this message + * if (m.isSet(Flags.Flag.DELETED)) + * System.out.println("DELETED message"); + * + * // Examine ALL system flags for this message + * Flags flags = m.getFlags(); + * Flags.Flag[] sf = flags.getSystemFlags(); + * for (int i = 0; i < sf.length; i++) { + * if (sf[i] == Flags.Flag.DELETED) + * System.out.println("DELETED message"); + * else if (sf[i] == Flags.Flag.SEEN) + * System.out.println("SEEN message"); + * ...... + * ...... + * } + * </pre> + * <p> + * + * @see Folder#getPermanentFlags + * @author John Mani + * @author Bill Shannon + */ + +public class Flags implements Cloneable, Serializable { + + private int system_flags = 0; + // used as a case-independent Set that preserves the original case, + // the key is the lowercase flag name and the value is the original + private Hashtable<String, String> user_flags = null; + + private final static int ANSWERED_BIT = 0x01; + private final static int DELETED_BIT = 0x02; + private final static int DRAFT_BIT = 0x04; + private final static int FLAGGED_BIT = 0x08; + private final static int RECENT_BIT = 0x10; + private final static int SEEN_BIT = 0x20; + private final static int USER_BIT = 0x80000000; + + private static final long serialVersionUID = 6243590407214169028L; + + /** + * This inner class represents an individual system flag. A set + * of standard system flag objects are predefined here. + */ + public static final class Flag { + /** + * This message has been answered. This flag is set by clients + * to indicate that this message has been answered to. + */ + public static final Flag ANSWERED = new Flag(ANSWERED_BIT); + + /** + * This message is marked deleted. Clients set this flag to + * mark a message as deleted. The expunge operation on a folder + * removes all messages in that folder that are marked for deletion. + */ + public static final Flag DELETED = new Flag(DELETED_BIT); + + /** + * This message is a draft. This flag is set by clients + * to indicate that the message is a draft message. + */ + public static final Flag DRAFT = new Flag(DRAFT_BIT); + + /** + * This message is flagged. No semantic is defined for this flag. + * Clients alter this flag. + */ + public static final Flag FLAGGED = new Flag(FLAGGED_BIT); + + /** + * This message is recent. Folder implementations set this flag + * to indicate that this message is new to this folder, that is, + * it has arrived since the last time this folder was opened. <p> + * + * Clients cannot alter this flag. + */ + public static final Flag RECENT = new Flag(RECENT_BIT); + + /** + * This message is seen. This flag is implicitly set by the + * implementation when this Message's content is returned + * to the client in some form. The <code>getInputStream</code> + * and <code>getContent</code> methods on Message cause this + * flag to be set. <p> + * + * Clients can alter this flag. + */ + public static final Flag SEEN = new Flag(SEEN_BIT); + + /** + * A special flag that indicates that this folder supports + * user defined flags. <p> + * + * The implementation sets this flag. Clients cannot alter + * this flag but can use it to determine if a folder supports + * user defined flags by using + * <code>folder.getPermanentFlags().contains(Flags.Flag.USER)</code>. + */ + public static final Flag USER = new Flag(USER_BIT); + + // flags are stored as bits for efficiency + private int bit; + private Flag(int bit) { + this.bit = bit; + } + } + + + /** + * Construct an empty Flags object. + */ + public Flags() { } + + /** + * Construct a Flags object initialized with the given flags. + * + * @param flags the flags for initialization + */ + @SuppressWarnings("unchecked") + public Flags(Flags flags) { + this.system_flags = flags.system_flags; + if (flags.user_flags != null) + this.user_flags = (Hashtable)flags.user_flags.clone(); + } + + /** + * Construct a Flags object initialized with the given system flag. + * + * @param flag the flag for initialization + */ + public Flags(Flag flag) { + this.system_flags |= flag.bit; + } + + /** + * Construct a Flags object initialized with the given user flag. + * + * @param flag the flag for initialization + */ + public Flags(String flag) { + user_flags = new Hashtable<>(1); + user_flags.put(flag.toLowerCase(Locale.ENGLISH), flag); + } + + /** + * Add the specified system flag to this Flags object. + * + * @param flag the flag to add + */ + public void add(Flag flag) { + system_flags |= flag.bit; + } + + /** + * Add the specified user flag to this Flags object. + * + * @param flag the flag to add + */ + public void add(String flag) { + if (user_flags == null) + user_flags = new Hashtable<>(1); + user_flags.put(flag.toLowerCase(Locale.ENGLISH), flag); + } + + /** + * Add all the flags in the given Flags object to this + * Flags object. + * + * @param f Flags object + */ + public void add(Flags f) { + system_flags |= f.system_flags; // add system flags + + if (f.user_flags != null) { // add user-defined flags + if (user_flags == null) + user_flags = new Hashtable<>(1); + + Enumeration<String> e = f.user_flags.keys(); + + while (e.hasMoreElements()) { + String s = e.nextElement(); + user_flags.put(s, f.user_flags.get(s)); + } + } + } + + /** + * Remove the specified system flag from this Flags object. + * + * @param flag the flag to be removed + */ + public void remove(Flag flag) { + system_flags &= ~flag.bit; + } + + /** + * Remove the specified user flag from this Flags object. + * + * @param flag the flag to be removed + */ + public void remove(String flag) { + if (user_flags != null) + user_flags.remove(flag.toLowerCase(Locale.ENGLISH)); + } + + /** + * Remove all flags in the given Flags object from this + * Flags object. + * + * @param f the flag to be removed + */ + public void remove(Flags f) { + system_flags &= ~f.system_flags; // remove system flags + + if (f.user_flags != null) { + if (user_flags == null) + return; + + Enumeration<String> e = f.user_flags.keys(); + while (e.hasMoreElements()) + user_flags.remove(e.nextElement()); + } + } + + /** + * Remove any flags <strong>not</strong> in the given Flags object. + * Useful for clearing flags not supported by a server. If the + * given Flags object includes the Flags.Flag.USER flag, all user + * flags in this Flags object are retained. + * + * @param f the flags to keep + * @return true if this Flags object changed + * @since JavaMail 1.6 + */ + public boolean retainAll(Flags f) { + boolean changed = false; + int sf = system_flags & f.system_flags; + if (system_flags != sf) { + system_flags = sf; + changed = true; + } + + // if we have user flags, and the USER flag is not set in "f", + // determine which user flags to clear + if (user_flags != null && (f.system_flags & USER_BIT) == 0) { + if (f.user_flags != null) { + Enumeration<String> e = user_flags.keys(); + while (e.hasMoreElements()) { + String key = e.nextElement(); + if (!f.user_flags.containsKey(key)) { + user_flags.remove(key); + changed = true; + } + } + } else { + // if anything in user_flags, throw them away + changed = user_flags.size() > 0; + user_flags = null; + } + } + return changed; + } + + /** + * Check whether the specified system flag is present in this Flags object. + * + * @param flag the flag to test + * @return true of the given flag is present, otherwise false. + */ + public boolean contains(Flag flag) { + return (system_flags & flag.bit) != 0; + } + + /** + * Check whether the specified user flag is present in this Flags object. + * + * @param flag the flag to test + * @return true of the given flag is present, otherwise false. + */ + public boolean contains(String flag) { + if (user_flags == null) + return false; + else + return user_flags.containsKey(flag.toLowerCase(Locale.ENGLISH)); + } + + /** + * Check whether all the flags in the specified Flags object are + * present in this Flags object. + * + * @param f the flags to test + * @return true if all flags in the given Flags object are present, + * otherwise false. + */ + public boolean contains(Flags f) { + // Check system flags + if ((f.system_flags & system_flags) != f.system_flags) + return false; + + // Check user flags + if (f.user_flags != null) { + if (user_flags == null) + return false; + Enumeration<String> e = f.user_flags.keys(); + + while (e.hasMoreElements()) { + if (!user_flags.containsKey(e.nextElement())) + return false; + } + } + + // If we've made it till here, return true + return true; + } + + /** + * Check whether the two Flags objects are equal. + * + * @return true if they're equal + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Flags)) + return false; + + Flags f = (Flags)obj; + + // Check system flags + if (f.system_flags != this.system_flags) + return false; + + // Check user flags + int size = this.user_flags == null ? 0 : this.user_flags.size(); + int fsize = f.user_flags == null ? 0 : f.user_flags.size(); + if (size == 0 && fsize == 0) + return true; + if (f.user_flags != null && this.user_flags != null && fsize == size) + return user_flags.keySet().equals(f.user_flags.keySet()); + + return false; + } + + /** + * Compute a hash code for this Flags object. + * + * @return the hash code + */ + @Override + public int hashCode() { + int hash = system_flags; + if (user_flags != null) { + Enumeration<String> e = user_flags.keys(); + while (e.hasMoreElements()) + hash += e.nextElement().hashCode(); + } + return hash; + } + + /** + * Return all the system flags in this Flags object. Returns + * an array of size zero if no flags are set. + * + * @return array of Flags.Flag objects representing system flags + */ + public Flag[] getSystemFlags() { + Vector<Flag> v = new Vector<>(); + if ((system_flags & ANSWERED_BIT) != 0) + v.addElement(Flag.ANSWERED); + if ((system_flags & DELETED_BIT) != 0) + v.addElement(Flag.DELETED); + if ((system_flags & DRAFT_BIT) != 0) + v.addElement(Flag.DRAFT); + if ((system_flags & FLAGGED_BIT) != 0) + v.addElement(Flag.FLAGGED); + if ((system_flags & RECENT_BIT) != 0) + v.addElement(Flag.RECENT); + if ((system_flags & SEEN_BIT) != 0) + v.addElement(Flag.SEEN); + if ((system_flags & USER_BIT) != 0) + v.addElement(Flag.USER); + + Flag[] f = new Flag[v.size()]; + v.copyInto(f); + return f; + } + + /** + * Return all the user flags in this Flags object. Returns + * an array of size zero if no flags are set. + * + * @return array of Strings, each String represents a flag. + */ + public String[] getUserFlags() { + Vector<String> v = new Vector<>(); + if (user_flags != null) { + Enumeration<String> e = user_flags.elements(); + + while (e.hasMoreElements()) + v.addElement(e.nextElement()); + } + + String[] f = new String[v.size()]; + v.copyInto(f); + return f; + } + + /** + * Clear all of the system flags. + * + * @since JavaMail 1.6 + */ + public void clearSystemFlags() { + system_flags = 0; + } + + /** + * Clear all of the user flags. + * + * @since JavaMail 1.6 + */ + public void clearUserFlags() { + user_flags = null; + } + + /** + * Returns a clone of this Flags object. + */ + @SuppressWarnings("unchecked") + @Override + public Object clone() { + Flags f = null; + try { + f = (Flags)super.clone(); + } catch (CloneNotSupportedException cex) { + // ignore, can't happen + } + if (this.user_flags != null) + f.user_flags = (Hashtable)this.user_flags.clone(); + return f; + } + + /** + * Return a string representation of this Flags object. + * Note that the exact format of the string is subject to change. + */ + public String toString() { + StringBuilder sb = new StringBuilder(); + + if ((system_flags & ANSWERED_BIT) != 0) + sb.append("\\Answered "); + if ((system_flags & DELETED_BIT) != 0) + sb.append("\\Deleted "); + if ((system_flags & DRAFT_BIT) != 0) + sb.append("\\Draft "); + if ((system_flags & FLAGGED_BIT) != 0) + sb.append("\\Flagged "); + if ((system_flags & RECENT_BIT) != 0) + sb.append("\\Recent "); + if ((system_flags & SEEN_BIT) != 0) + sb.append("\\Seen "); + if ((system_flags & USER_BIT) != 0) + sb.append("\\* "); + + boolean first = true; + if (user_flags != null) { + Enumeration<String> e = user_flags.elements(); + + while (e.hasMoreElements()) { + if (first) + first = false; + else + sb.append(' '); + sb.append(e.nextElement()); + } + } + + if (first && sb.length() > 0) + sb.setLength(sb.length() - 1); // smash trailing space + + return sb.toString(); + } + + /***** + public static void main(String argv[]) throws Exception { + // a new flags object + Flags f1 = new Flags(); + f1.add(Flags.Flag.DELETED); + f1.add(Flags.Flag.SEEN); + f1.add(Flags.Flag.RECENT); + f1.add(Flags.Flag.ANSWERED); + + // check copy constructor with only system flags + Flags fc = new Flags(f1); + if (f1.equals(fc) && fc.equals(f1)) + System.out.println("success"); + else + System.out.println("fail"); + + // check clone with only system flags + fc = (Flags)f1.clone(); + if (f1.equals(fc) && fc.equals(f1)) + System.out.println("success"); + else + System.out.println("fail"); + + // add a user flag and make sure it still works right + f1.add("MyFlag"); + + // shouldn't be equal here + if (!f1.equals(fc) && !fc.equals(f1)) + System.out.println("success"); + else + System.out.println("fail"); + + // check clone + fc = (Flags)f1.clone(); + if (f1.equals(fc) && fc.equals(f1)) + System.out.println("success"); + else + System.out.println("fail"); + + // make sure user flag hash tables are separate + fc.add("AnotherFlag"); + if (!f1.equals(fc) && !fc.equals(f1)) + System.out.println("success"); + else + System.out.println("fail"); + + // check copy constructor + fc = new Flags(f1); + if (f1.equals(fc) && fc.equals(f1)) + System.out.println("success"); + else + System.out.println("fail"); + + // another new flags object + Flags f2 = new Flags(Flags.Flag.ANSWERED); + f2.add("MyFlag"); + + if (f1.contains(Flags.Flag.DELETED)) + System.out.println("success"); + else + System.out.println("fail"); + + if (f1.contains(Flags.Flag.SEEN)) + System.out.println("success"); + else + System.out.println("fail"); + + if (f1.contains(Flags.Flag.RECENT)) + System.out.println("success"); + else + System.out.println("fail"); + + if (f1.contains("MyFlag")) + System.out.println("success"); + else + System.out.println("fail"); + + if (f2.contains(Flags.Flag.ANSWERED)) + System.out.println("success"); + else + System.out.println("fail"); + + + System.out.println("----------------"); + + String[] s = f1.getUserFlags(); + for (int i = 0; i < s.length; i++) + System.out.println(s[i]); + System.out.println("----------------"); + s = f2.getUserFlags(); + for (int i = 0; i < s.length; i++) + System.out.println(s[i]); + + System.out.println("----------------"); + + if (f1.contains(f2)) // this should be true + System.out.println("success"); + else + System.out.println("fail"); + + if (!f2.contains(f1)) // this should be false + System.out.println("success"); + else + System.out.println("fail"); + + Flags f3 = new Flags(); + f3.add(Flags.Flag.DELETED); + f3.add(Flags.Flag.SEEN); + f3.add(Flags.Flag.RECENT); + f3.add(Flags.Flag.ANSWERED); + f3.add("ANOTHERFLAG"); + f3.add("MYFLAG"); + + f1.add("AnotherFlag"); + + if (f1.equals(f3)) + System.out.println("equals success"); + else + System.out.println("fail"); + if (f3.equals(f1)) + System.out.println("equals success"); + else + System.out.println("fail"); + System.out.println("f1 hash code " + f1.hashCode()); + System.out.println("f3 hash code " + f3.hashCode()); + if (f1.hashCode() == f3.hashCode()) + System.out.println("success"); + else + System.out.println("fail"); + } + ****/ +}
diff --git a/mail/src/main/java/javax/mail/Folder.java b/mail/src/main/java/javax/mail/Folder.java new file mode 100644 index 0000000..0ddce21 --- /dev/null +++ b/mail/src/main/java/javax/mail/Folder.java
@@ -0,0 +1,1657 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +import java.lang.*; +import java.util.ArrayList; +import java.util.EventListener; +import java.util.List; +import java.util.Vector; +import java.util.concurrent.Executor; +import javax.mail.search.SearchTerm; +import javax.mail.event.*; + +/** + * Folder is an abstract class that represents a folder for mail + * messages. Subclasses implement protocol specific Folders.<p> + * + * Folders can contain Messages, other Folders or both, thus providing + * a tree-like hierarchy rooted at the Store's default folder. (Note + * that some Folder implementations may not allow both Messages and + * other Folders in the same Folder).<p> + * + * The interpretation of folder names is implementation dependent. + * The different levels of hierarchy in a folder's full name + * are separated from each other by the hierarchy delimiter + * character.<p> + * + * The case-insensitive full folder name (that is, the full name + * relative to the default folder for a Store) <strong>INBOX</strong> + * is reserved to mean the "primary folder for this user on this + * server". Not all Stores will provide an INBOX folder, and not + * all users will have an INBOX folder at all times. The name + * <strong>INBOX</strong> is reserved to refer to this folder, + * when it exists, in Stores that provide it. <p> + * + * A Folder object obtained from a Store need not actually exist + * in the backend store. The <code>exists</code> method tests whether + * the folder exists or not. The <code>create</code> method + * creates a Folder. <p> + * + * A Folder is initially in the closed state. Certain methods are valid + * in this state; the documentation for those methods note this. A + * Folder is opened by calling its 'open' method. All Folder methods, + * except <code>open</code>, <code>delete</code> and + * <code>renameTo</code>, are valid in this state. <p> + * + * The only way to get a Folder is by invoking the + * <code>getFolder</code> method on Store, Folder, or Session, or by invoking + * the <code>list</code> or <code>listSubscribed</code> methods + * on Folder. Folder objects returned by the above methods are not + * cached by the Store. Thus, invoking the <code>getFolder</code> method + * with the same folder name multiple times will return distinct Folder + * objects. Likewise for the <code>list</code> and <code>listSubscribed</code> + * methods. <p> + * + * The Message objects within the Folder are cached by the Folder. + * Thus, invoking <code>getMessage(msgno)</code> on the same message number + * multiple times will return the same Message object, until an + * expunge is done on this Folder. <p> + * + * Message objects from a Folder are only valid while a Folder is open + * and should not be accessed after the Folder is closed, even if the + * Folder is subsequently reopened. Instead, new Message objects must + * be fetched from the Folder after the Folder is reopened. <p> + * + * Note that a Message's message number can change within a + * session if the containing Folder is expunged using the expunge + * method. Clients that use message numbers as references to messages + * should be aware of this and should be prepared to deal with this + * situation (probably by flushing out existing message number references + * and reloading them). Because of this complexity, it is better for + * clients to use Message objects as references to messages, rather than + * message numbers. Expunged Message objects still have to be + * pruned, but other Message objects in that folder are not affected by the + * expunge. + * + * @author John Mani + * @author Bill Shannon + */ + +public abstract class Folder implements AutoCloseable { + + /** + * The parent store. + */ + protected Store store; + + /** + * The open mode of this folder. The open mode is + * <code>Folder.READ_ONLY</code>, <code>Folder.READ_WRITE</code>, + * or -1 if not known. + * @since JavaMail 1.1 + */ + protected int mode = -1; + + /* + * The queue of events to be delivered. + */ + private final EventQueue q; + + /** + * Constructor that takes a Store object. + * + * @param store the Store that holds this folder + */ + protected Folder(Store store) { + this.store = store; + + // create or choose the appropriate event queue + Session session = store.getSession(); + String scope = + session.getProperties().getProperty("mail.event.scope", "folder"); + Executor executor = + (Executor)session.getProperties().get("mail.event.executor"); + if (scope.equalsIgnoreCase("application")) + q = EventQueue.getApplicationEventQueue(executor); + else if (scope.equalsIgnoreCase("session")) + q = session.getEventQueue(); + else if (scope.equalsIgnoreCase("store")) + q = store.getEventQueue(); + else // if (scope.equalsIgnoreCase("folder")) + q = new EventQueue(executor); + } + + /** + * Returns the name of this Folder. <p> + * + * This method can be invoked on a closed Folder. + * + * @return name of the Folder + */ + public abstract String getName(); + + /** + * Returns the full name of this Folder. If the folder resides under + * the root hierarchy of this Store, the returned name is relative + * to the root. Otherwise an absolute name, starting with the + * hierarchy delimiter, is returned. <p> + * + * This method can be invoked on a closed Folder. + * + * @return full name of the Folder + */ + public abstract String getFullName(); + + /** + * Return a URLName representing this folder. The returned URLName + * does <em>not</em> include the password used to access the store. + * + * @return the URLName representing this folder + * @exception MessagingException for failures + * @see URLName + * @since JavaMail 1.1 + */ + public URLName getURLName() throws MessagingException { + URLName storeURL = getStore().getURLName(); + String fullname = getFullName(); + StringBuilder encodedName = new StringBuilder(); + + if (fullname != null) { + /* + // We need to encode each of the folder's names. + char separator = getSeparator(); + StringTokenizer tok = new StringTokenizer( + fullname, new Character(separator).toString(), true); + + while (tok.hasMoreTokens()) { + String s = tok.nextToken(); + if (s.charAt(0) == separator) + encodedName.append(separator); + else + // XXX - should encode, but since there's no decoder... + //encodedName.append(java.net.URLEncoder.encode(s)); + encodedName.append(s); + } + */ + // append the whole thing, until we can encode + encodedName.append(fullname); + } + + /* + * Sure would be convenient if URLName had a + * constructor that took a base URLName. + */ + return new URLName(storeURL.getProtocol(), storeURL.getHost(), + storeURL.getPort(), encodedName.toString(), + storeURL.getUsername(), + null /* no password */); + } + + /** + * Returns the Store that owns this Folder object. + * This method can be invoked on a closed Folder. + * + * @return the Store + */ + public Store getStore() { + return store; + } + + /** + * Returns the parent folder of this folder. + * This method can be invoked on a closed Folder. If this folder + * is the top of a folder hierarchy, this method returns null. <p> + * + * Note that since Folder objects are not cached, invoking this method + * returns a new distinct Folder object. + * + * @return Parent folder + * @exception MessagingException for failures + */ + public abstract Folder getParent() throws MessagingException; + + /** + * Tests if this folder physically exists on the Store. + * This method can be invoked on a closed Folder. + * + * @return true if the folder exists, otherwise false + * @see #create + * @exception MessagingException typically if the connection + * to the server is lost. + */ + public abstract boolean exists() throws MessagingException; + + /** + * Returns a list of Folders belonging to this Folder's namespace + * that match the specified pattern. Patterns may contain the wildcard + * characters <code>"%"</code>, which matches any character except hierarchy + * delimiters, and <code>"*"</code>, which matches any character. <p> + * + * As an example, given the folder hierarchy: <pre> + * Personal/ + * Finance/ + * Stocks + * Bonus + * StockOptions + * Jokes + * </pre> + * <code>list("*")</code> on "Personal" will return the whole + * hierarchy. <br> + * <code>list("%")</code> on "Personal" will return "Finance" and + * "Jokes". <br> + * <code>list("Jokes")</code> on "Personal" will return "Jokes".<br> + * <code>list("Stock*")</code> on "Finance" will return "Stocks" + * and "StockOptions". <p> + * + * Folder objects are not cached by the Store, so invoking this + * method on the same pattern multiple times will return that many + * distinct Folder objects. <p> + * + * This method can be invoked on a closed Folder. + * + * @param pattern the match pattern + * @return array of matching Folder objects. An empty + * array is returned if no matching Folders exist. + * @see #listSubscribed + * @exception FolderNotFoundException if this folder does + * not exist. + * @exception MessagingException for other failures + */ + public abstract Folder[] list(String pattern) throws MessagingException; + + /** + * Returns a list of subscribed Folders belonging to this Folder's + * namespace that match the specified pattern. If the folder does + * not support subscription, this method should resolve to + * <code>list</code>. + * (The default implementation provided here, does just this). + * The pattern can contain wildcards as for <code>list</code>. <p> + * + * Note that, at a given level of the folder hierarchy, a particular + * folder may not be subscribed, but folders underneath that folder + * in the folder hierarchy may be subscribed. In order to allow + * walking the folder hierarchy, such unsubscribed folders may be + * returned, indicating that a folder lower in the hierarchy is + * subscribed. The <code>isSubscribed</code> method on a folder will + * tell whether any particular folder is actually subscribed. <p> + * + * Folder objects are not cached by the Store, so invoking this + * method on the same pattern multiple times will return that many + * distinct Folder objects. <p> + * + * This method can be invoked on a closed Folder. + * + * @param pattern the match pattern + * @return array of matching subscribed Folder objects. An + * empty array is returned if no matching + * subscribed folders exist. + * @see #list + * @exception FolderNotFoundException if this folder does + * not exist. + * @exception MessagingException for other failures + */ + public Folder[] listSubscribed(String pattern) throws MessagingException { + return list(pattern); + } + + /** + * Convenience method that returns the list of folders under this + * Folder. This method just calls the <code>list(String pattern)</code> + * method with <code>"%"</code> as the match pattern. This method can + * be invoked on a closed Folder. + * + * @return array of Folder objects under this Folder. An + * empty array is returned if no subfolders exist. + * @see #list + * @exception FolderNotFoundException if this folder does + * not exist. + * @exception MessagingException for other failures + */ + + public Folder[] list() throws MessagingException { + return list("%"); + } + + /** + * Convenience method that returns the list of subscribed folders + * under this Folder. This method just calls the + * <code>listSubscribed(String pattern)</code> method with <code>"%"</code> + * as the match pattern. This method can be invoked on a closed Folder. + * + * @return array of subscribed Folder objects under this + * Folder. An empty array is returned if no subscribed + * subfolders exist. + * @see #listSubscribed + * @exception FolderNotFoundException if this folder does + * not exist. + * @exception MessagingException for other failures + */ + public Folder[] listSubscribed() throws MessagingException { + return listSubscribed("%"); + } + + /** + * Return the delimiter character that separates this Folder's pathname + * from the names of immediate subfolders. This method can be invoked + * on a closed Folder. + * + * @exception FolderNotFoundException if the implementation + * requires the folder to exist, but it does not + * @return Hierarchy separator character + */ + public abstract char getSeparator() throws MessagingException; + + /** + * This folder can contain messages + */ + public final static int HOLDS_MESSAGES = 0x01; + + /** + * This folder can contain other folders + */ + public final static int HOLDS_FOLDERS = 0x02; + + /** + * Returns the type of this Folder, that is, whether this folder can hold + * messages or subfolders or both. The returned value is an integer + * bitfield with the appropriate bits set. This method can be invoked + * on a closed folder. + * + * @return integer with appropriate bits set + * @exception FolderNotFoundException if this folder does + * not exist. + * @see #HOLDS_FOLDERS + * @see #HOLDS_MESSAGES + */ + public abstract int getType() throws MessagingException; + + /** + * Create this folder on the Store. When this folder is created, any + * folders in its path that do not exist are also created. <p> + * + * If the creation is successful, a CREATED FolderEvent is delivered + * to any FolderListeners registered on this Folder and this Store. + * + * @param type The type of this folder. + * + * @return true if the creation succeeds, else false. + * @exception MessagingException for failures + * @see #HOLDS_FOLDERS + * @see #HOLDS_MESSAGES + * @see javax.mail.event.FolderEvent + */ + public abstract boolean create(int type) throws MessagingException; + + /** + * Returns true if this Folder is subscribed. <p> + * + * This method can be invoked on a closed Folder. <p> + * + * The default implementation provided here just returns true. + * + * @return true if this Folder is subscribed + */ + public boolean isSubscribed() { + return true; + } + + /** + * Subscribe or unsubscribe this Folder. Not all Stores support + * subscription. <p> + * + * This method can be invoked on a closed Folder. <p> + * + * The implementation provided here just throws the + * MethodNotSupportedException. + * + * @param subscribe true to subscribe, false to unsubscribe + * @exception FolderNotFoundException if this folder does + * not exist. + * @exception MethodNotSupportedException if this store + * does not support subscription + * @exception MessagingException for other failures + */ + public void setSubscribed(boolean subscribe) + throws MessagingException { + throw new MethodNotSupportedException(); + } + + /** + * Returns true if this Folder has new messages since the last time + * this indication was reset. When this indication is set or reset + * depends on the Folder implementation (and in the case of IMAP, + * depends on the server). This method can be used to implement + * a lightweight "check for new mail" operation on a Folder without + * opening it. (For example, a thread that monitors a mailbox and + * flags when it has new mail.) This method should indicate whether + * any messages in the Folder have the <code>RECENT</code> flag set. <p> + * + * Note that this is not an incremental check for new mail, i.e., + * it cannot be used to determine whether any new messages have + * arrived since the last time this method was invoked. To + * implement incremental checks, the Folder needs to be opened. <p> + * + * This method can be invoked on a closed Folder that can contain + * Messages. + * + * @return true if the Store has new Messages + * @exception FolderNotFoundException if this folder does + * not exist. + * @exception MessagingException for other failures + */ + public abstract boolean hasNewMessages() throws MessagingException; + + /** + * Return the Folder object corresponding to the given name. Note that + * this folder does not physically have to exist in the Store. The + * <code>exists()</code> method on a Folder indicates whether it really + * exists on the Store. <p> + * + * In some Stores, name can be an absolute path if it starts with the + * hierarchy delimiter. Otherwise, it is interpreted relative to + * this Folder. <p> + * + * Folder objects are not cached by the Store, so invoking this + * method on the same name multiple times will return that many + * distinct Folder objects. <p> + * + * This method can be invoked on a closed Folder. + * + * @param name name of the Folder + * @return Folder object + * @exception MessagingException for failures + */ + public abstract Folder getFolder(String name) + throws MessagingException; + + /** + * Delete this Folder. This method will succeed only on a closed + * Folder. <p> + * + * The <code>recurse</code> flag controls whether the deletion affects + * subfolders or not. If true, all subfolders are deleted, then this + * folder itself is deleted. If false, the behaviour is dependent on + * the folder type and is elaborated below: + * + * <ul> + * <li> + * The folder can contain only messages: (type == HOLDS_MESSAGES). + * <br> + * All messages within the folder are removed. The folder + * itself is then removed. An appropriate FolderEvent is generated by + * the Store and this folder. + * + * <li> + * The folder can contain only subfolders: (type == HOLDS_FOLDERS). + * <br> + * If this folder is empty (does not contain any + * subfolders at all), it is removed. An appropriate FolderEvent is + * generated by the Store and this folder.<br> + * If this folder contains any subfolders, the delete fails + * and returns false. + * + * <li> + * The folder can contain subfolders as well as messages: <br> + * If the folder is empty (no messages or subfolders), it + * is removed. If the folder contains no subfolders, but only messages, + * then all messages are removed. The folder itself is then removed. + * In both the above cases, an appropriate FolderEvent is + * generated by the Store and this folder. <p> + * + * If the folder contains subfolders there are 3 possible + * choices an implementation is free to do: + * + * <ol> + * <li> The operation fails, irrespective of whether this folder + * contains messages or not. Some implementations might elect to go + * with this simple approach. The delete() method returns false. + * + * <li> Any messages within the folder are removed. Subfolders + * are not removed. The folder itself is not removed or affected + * in any manner. The delete() method returns true. And the + * exists() method on this folder will return true indicating that + * this folder still exists. <br> + * An appropriate FolderEvent is generated by the Store and this folder. + * + * <li> Any messages within the folder are removed. Subfolders are + * not removed. The folder itself changes its type from + * HOLDS_FOLDERS | HOLDS_MESSAGES to HOLDS_FOLDERS. Thus new + * messages cannot be added to this folder, but new subfolders can + * be created underneath. The delete() method returns true indicating + * success. The exists() method on this folder will return true + * indicating that this folder still exists. <br> + * An appropriate FolderEvent is generated by the Store and this folder. + * </ol> + * </ul> + * + * @param recurse also delete subfolders? + * @return true if the Folder is deleted successfully + * @exception FolderNotFoundException if this folder does + * not exist + * @exception IllegalStateException if this folder is not in + * the closed state. + * @exception MessagingException for other failures + * @see javax.mail.event.FolderEvent + */ + public abstract boolean delete(boolean recurse) + throws MessagingException; + + /** + * Rename this Folder. This method will succeed only on a closed + * Folder. <p> + * + * If the rename is successful, a RENAMED FolderEvent is delivered + * to FolderListeners registered on this folder and its containing + * Store. + * + * @param f a folder representing the new name for this Folder + * @return true if the Folder is renamed successfully + * @exception FolderNotFoundException if this folder does + * not exist + * @exception IllegalStateException if this folder is not in + * the closed state. + * @exception MessagingException for other failures + * @see javax.mail.event.FolderEvent + */ + public abstract boolean renameTo(Folder f) throws MessagingException; + + /** + * The Folder is read only. The state and contents of this + * folder cannot be modified. + */ + public static final int READ_ONLY = 1; + + /** + * The state and contents of this folder can be modified. + */ + public static final int READ_WRITE = 2; + + /** + * Open this Folder. This method is valid only on Folders that + * can contain Messages and that are closed. <p> + * + * If this folder is opened successfully, an OPENED ConnectionEvent + * is delivered to any ConnectionListeners registered on this + * Folder. <p> + * + * The effect of opening multiple connections to the same folder + * on a specifc Store is implementation dependent. Some implementations + * allow multiple readers, but only one writer. Others allow + * multiple writers as well as readers. + * + * @param mode open the Folder READ_ONLY or READ_WRITE + * @exception FolderNotFoundException if this folder does + * not exist. + * @exception IllegalStateException if this folder is not in + * the closed state. + * @exception MessagingException for other failures + * @see #READ_ONLY + * @see #READ_WRITE + * @see #getType() + * @see javax.mail.event.ConnectionEvent + */ + public abstract void open(int mode) throws MessagingException; + + /** + * Close this Folder. This method is valid only on open Folders. <p> + * + * A CLOSED ConnectionEvent is delivered to any ConnectionListeners + * registered on this Folder. Note that the folder is closed even + * if this method terminates abnormally by throwing a + * MessagingException. + * + * @param expunge expunges all deleted messages if this flag is true + * @exception IllegalStateException if this folder is not opened + * @exception MessagingException for other failures + * @see javax.mail.event.ConnectionEvent + */ + public abstract void close(boolean expunge) throws MessagingException; + + /** + * Close this Folder and expunge deleted messages. <p> + * + * A CLOSED ConnectionEvent is delivered to any ConnectionListeners + * registered on this Folder. Note that the folder is closed even + * if this method terminates abnormally by throwing a + * MessagingException. <p> + * + * This method supports the {@link java.lang.AutoCloseable AutoCloseable} + * interface. <p> + * + * This implementation calls <code>close(true)</code>. + * + * @exception IllegalStateException if this folder is not opened + * @exception MessagingException for other failures + * @see javax.mail.event.ConnectionEvent + * @since JavaMail 1.6 + */ + @Override + public void close() throws MessagingException { + close(true); + } + + /** + * Indicates whether this Folder is in the 'open' state. + * @return true if this Folder is in the 'open' state. + */ + public abstract boolean isOpen(); + + /** + * Return the open mode of this folder. Returns + * <code>Folder.READ_ONLY</code>, <code>Folder.READ_WRITE</code>, + * or -1 if the open mode is not known (usually only because an older + * <code>Folder</code> provider has not been updated to use this new + * method). + * + * @exception IllegalStateException if this folder is not opened + * @return the open mode of this folder + * @since JavaMail 1.1 + */ + public synchronized int getMode() { + if (!isOpen()) + throw new IllegalStateException("Folder not open"); + return mode; + } + + /** + * Get the permanent flags supported by this Folder. Returns a Flags + * object that contains all the flags supported. <p> + * + * The special flag <code>Flags.Flag.USER </code> indicates that this Folder + * supports arbitrary user-defined flags. <p> + * + * The supported permanent flags for a folder may not be available + * until the folder is opened. + * + * @return permanent flags, or null if not known + */ + public abstract Flags getPermanentFlags(); + + /** + * Get total number of messages in this Folder. <p> + * + * This method can be invoked on a closed folder. However, note + * that for some folder implementations, getting the total message + * count can be an expensive operation involving actually opening + * the folder. In such cases, a provider can choose not to support + * this functionality in the closed state, in which case this method + * must return -1. <p> + * + * Clients invoking this method on a closed folder must be aware + * that this is a potentially expensive operation. Clients must + * also be prepared to handle a return value of -1 in this case. + * + * @return total number of messages. -1 may be returned + * by certain implementations if this method is + * invoked on a closed folder. + * @exception FolderNotFoundException if this folder does + * not exist. + * @exception MessagingException for other failures + */ + public abstract int getMessageCount() throws MessagingException; + + /** + * Get the number of new messages in this Folder. <p> + * + * This method can be invoked on a closed folder. However, note + * that for some folder implementations, getting the new message + * count can be an expensive operation involving actually opening + * the folder. In such cases, a provider can choose not to support + * this functionality in the closed state, in which case this method + * must return -1. <p> + * + * Clients invoking this method on a closed folder must be aware + * that this is a potentially expensive operation. Clients must + * also be prepared to handle a return value of -1 in this case. <p> + * + * This implementation returns -1 if this folder is closed. Else + * this implementation gets each Message in the folder using + * <code>getMessage(int)</code> and checks whether its + * <code>RECENT</code> flag is set. The total number of messages + * that have this flag set is returned. + * + * @return number of new messages. -1 may be returned + * by certain implementations if this method is + * invoked on a closed folder. + * @exception FolderNotFoundException if this folder does + * not exist. + * @exception MessagingException for other failures + */ + public synchronized int getNewMessageCount() + throws MessagingException { + if (!isOpen()) + return -1; + + int newmsgs = 0; + int total = getMessageCount(); + for (int i = 1; i <= total; i++) { + try { + if (getMessage(i).isSet(Flags.Flag.RECENT)) + newmsgs++; + } catch (MessageRemovedException me) { + // This is an expunged message, ignore it. + continue; + } + } + return newmsgs; + } + + /** + * Get the total number of unread messages in this Folder. <p> + * + * This method can be invoked on a closed folder. However, note + * that for some folder implementations, getting the unread message + * count can be an expensive operation involving actually opening + * the folder. In such cases, a provider can choose not to support + * this functionality in the closed state, in which case this method + * must return -1. <p> + * + * Clients invoking this method on a closed folder must be aware + * that this is a potentially expensive operation. Clients must + * also be prepared to handle a return value of -1 in this case. <p> + * + * This implementation returns -1 if this folder is closed. Else + * this implementation gets each Message in the folder using + * <code>getMessage(int)</code> and checks whether its + * <code>SEEN</code> flag is set. The total number of messages + * that do not have this flag set is returned. + * + * @return total number of unread messages. -1 may be returned + * by certain implementations if this method is + * invoked on a closed folder. + * @exception FolderNotFoundException if this folder does + * not exist. + * @exception MessagingException for other failures + */ + public synchronized int getUnreadMessageCount() + throws MessagingException { + if (!isOpen()) + return -1; + + int unread = 0; + int total = getMessageCount(); + for (int i = 1; i <= total; i++) { + try { + if (!getMessage(i).isSet(Flags.Flag.SEEN)) + unread++; + } catch (MessageRemovedException me) { + // This is an expunged message, ignore it. + continue; + } + } + return unread; + } + + /** + * Get the number of deleted messages in this Folder. <p> + * + * This method can be invoked on a closed folder. However, note + * that for some folder implementations, getting the deleted message + * count can be an expensive operation involving actually opening + * the folder. In such cases, a provider can choose not to support + * this functionality in the closed state, in which case this method + * must return -1. <p> + * + * Clients invoking this method on a closed folder must be aware + * that this is a potentially expensive operation. Clients must + * also be prepared to handle a return value of -1 in this case. <p> + * + * This implementation returns -1 if this folder is closed. Else + * this implementation gets each Message in the folder using + * <code>getMessage(int)</code> and checks whether its + * <code>DELETED</code> flag is set. The total number of messages + * that have this flag set is returned. + * + * @return number of deleted messages. -1 may be returned + * by certain implementations if this method is + * invoked on a closed folder. + * @exception FolderNotFoundException if this folder does + * not exist. + * @exception MessagingException for other failures + * @since JavaMail 1.3 + */ + public synchronized int getDeletedMessageCount() throws MessagingException { + if (!isOpen()) + return -1; + + int deleted = 0; + int total = getMessageCount(); + for (int i = 1; i <= total; i++) { + try { + if (getMessage(i).isSet(Flags.Flag.DELETED)) + deleted++; + } catch (MessageRemovedException me) { + // This is an expunged message, ignore it. + continue; + } + } + return deleted; + } + + /** + * Get the Message object corresponding to the given message + * number. A Message object's message number is the relative + * position of this Message in its Folder. Messages are numbered + * starting at 1 through the total number of message in the folder. + * Note that the message number for a particular Message can change + * during a session if other messages in the Folder are deleted and + * the Folder is expunged. <p> + * + * Message objects are light-weight references to the actual message + * that get filled up on demand. Hence Folder implementations are + * expected to provide light-weight Message objects. <p> + * + * Unlike Folder objects, repeated calls to getMessage with the + * same message number will return the same Message object, as + * long as no messages in this folder have been expunged. <p> + * + * Since message numbers can change within a session if the folder + * is expunged , clients are advised not to use message numbers as + * references to messages. Use Message objects instead. + * + * @param msgnum the message number + * @return the Message object + * @see #getMessageCount + * @see #fetch + * @exception FolderNotFoundException if this folder does + * not exist. + * @exception IllegalStateException if this folder is not opened + * @exception IndexOutOfBoundsException if the message number + * is out of range. + * @exception MessagingException for other failures + */ + public abstract Message getMessage(int msgnum) + throws MessagingException; + + /** + * Get the Message objects for message numbers ranging from start + * through end, both start and end inclusive. Note that message + * numbers start at 1, not 0. <p> + * + * Message objects are light-weight references to the actual message + * that get filled up on demand. Hence Folder implementations are + * expected to provide light-weight Message objects. <p> + * + * This implementation uses getMessage(index) to obtain the required + * Message objects. Note that the returned array must contain + * <code>(end-start+1)</code> Message objects. + * + * @param start the number of the first message + * @param end the number of the last message + * @return the Message objects + * @see #fetch + * @exception FolderNotFoundException if this folder does + * not exist. + * @exception IllegalStateException if this folder is not opened. + * @exception IndexOutOfBoundsException if the start or end + * message numbers are out of range. + * @exception MessagingException for other failures + */ + public synchronized Message[] getMessages(int start, int end) + throws MessagingException { + Message[] msgs = new Message[end - start +1]; + for (int i = start; i <= end; i++) + msgs[i - start] = getMessage(i); + return msgs; + } + + /** + * Get the Message objects for message numbers specified in + * the array. <p> + * + * Message objects are light-weight references to the actual message + * that get filled up on demand. Hence Folder implementations are + * expected to provide light-weight Message objects. <p> + * + * This implementation uses getMessage(index) to obtain the required + * Message objects. Note that the returned array must contain + * <code>msgnums.length</code> Message objects + * + * @param msgnums the array of message numbers + * @return the array of Message objects. + * @see #fetch + * @exception FolderNotFoundException if this folder does + * not exist. + * @exception IllegalStateException if this folder is not opened. + * @exception IndexOutOfBoundsException if any message number + * in the given array is out of range. + * @exception MessagingException for other failures + */ + public synchronized Message[] getMessages(int[] msgnums) + throws MessagingException { + int len = msgnums.length; + Message[] msgs = new Message[len]; + for (int i = 0; i < len; i++) + msgs[i] = getMessage(msgnums[i]); + return msgs; + } + + /** + * Get all Message objects from this Folder. Returns an empty array + * if the folder is empty. + * + * Clients can use Message objects (instead of sequence numbers) + * as references to the messages within a folder; this method supplies + * the Message objects to the client. Folder implementations are + * expected to provide light-weight Message objects, which get + * filled on demand. <p> + * + * This implementation invokes <code>getMessageCount()</code> to get + * the current message count and then uses <code>getMessage()</code> + * to get Message objects from 1 till the message count. + * + * @return array of Message objects, empty array if folder + * is empty. + * @see #fetch + * @exception FolderNotFoundException if this folder does + * not exist. + * @exception IllegalStateException if this folder is not opened. + * @exception MessagingException for other failures + */ + public synchronized Message[] getMessages() throws MessagingException { + if (!isOpen()) // otherwise getMessageCount might return -1 + throw new IllegalStateException("Folder not open"); + int total = getMessageCount(); + Message[] msgs = new Message[total]; + for (int i = 1; i <= total; i++) + msgs[i-1] = getMessage(i); + return msgs; + } + + /** + * Append given Messages to this folder. This method can be + * invoked on a closed Folder. An appropriate MessageCountEvent + * is delivered to any MessageCountListener registered on this + * folder when the messages arrive in the folder. <p> + * + * Folder implementations must not abort this operation if a + * Message in the given message array turns out to be an + * expunged Message. + * + * @param msgs array of Messages to be appended + * @exception FolderNotFoundException if this folder does + * not exist. + * @exception MessagingException if the append failed. + */ + public abstract void appendMessages(Message[] msgs) + throws MessagingException; + + /** + * Prefetch the items specified in the FetchProfile for the + * given Messages. <p> + * + * Clients use this method to indicate that the specified items are + * needed en-masse for the given message range. Implementations are + * expected to retrieve these items for the given message range in + * a efficient manner. Note that this method is just a hint to the + * implementation to prefetch the desired items. <p> + * + * An example is a client filling its header-view window with + * the Subject, From and X-mailer headers for all messages in the + * folder. + * <blockquote><pre> + * + * Message[] msgs = folder.getMessages(); + * + * FetchProfile fp = new FetchProfile(); + * fp.add(FetchProfile.Item.ENVELOPE); + * fp.add("X-mailer"); + * folder.fetch(msgs, fp); + * + * for (int i = 0; i < folder.getMessageCount(); i++) { + * display(msg[i].getFrom()); + * display(msg[i].getSubject()); + * display(msg[i].getHeader("X-mailer")); + * } + * + * </pre></blockquote><p> + * + * The implementation provided here just returns without + * doing anything useful. Providers wanting to provide a real + * implementation for this method should override this method. + * + * @param msgs fetch items for these messages + * @param fp the FetchProfile + * @exception IllegalStateException if this folder is not opened + * @exception MessagingException for other failures + */ + public void fetch(Message[] msgs, FetchProfile fp) + throws MessagingException { + return; + } + + /** + * Set the specified flags on the messages specified in the array. + * This will result in appropriate MessageChangedEvents being + * delivered to any MessageChangedListener registered on this + * Message's containing folder. <p> + * + * Note that the specified Message objects <strong>must</strong> + * belong to this folder. Certain Folder implementations can + * optimize the operation of setting Flags for a group of messages, + * so clients might want to use this method, rather than invoking + * <code>Message.setFlags</code> for each Message. <p> + * + * This implementation degenerates to invoking <code>setFlags()</code> + * on each Message object. Specific Folder implementations that can + * optimize this case should do so. + * Also, an implementation must not abort the operation if a Message + * in the array turns out to be an expunged Message. + * + * @param msgs the array of message objects + * @param flag Flags object containing the flags to be set + * @param value set the flags to this boolean value + * @exception IllegalStateException if this folder is not opened + * or if it has been opened READ_ONLY. + * @exception MessagingException for other failures + * @see Message#setFlags + * @see javax.mail.event.MessageChangedEvent + */ + public synchronized void setFlags(Message[] msgs, + Flags flag, boolean value) throws MessagingException { + for (int i = 0; i < msgs.length; i++) { + try { + msgs[i].setFlags(flag, value); + } catch (MessageRemovedException me) { + // This message is expunged, skip + } + } + } + + /** + * Set the specified flags on the messages numbered from start + * through end, both start and end inclusive. Note that message + * numbers start at 1, not 0. + * This will result in appropriate MessageChangedEvents being + * delivered to any MessageChangedListener registered on this + * Message's containing folder. <p> + * + * Certain Folder implementations can + * optimize the operation of setting Flags for a group of messages, + * so clients might want to use this method, rather than invoking + * <code>Message.setFlags</code> for each Message. <p> + * + * The default implementation uses <code>getMessage(int)</code> to + * get each <code>Message</code> object and then invokes + * <code>setFlags</code> on that object to set the flags. + * Specific Folder implementations that can optimize this case should do so. + * Also, an implementation must not abort the operation if a message + * number refers to an expunged message. + * + * @param start the number of the first message + * @param end the number of the last message + * @param flag Flags object containing the flags to be set + * @param value set the flags to this boolean value + * @exception IllegalStateException if this folder is not opened + * or if it has been opened READ_ONLY. + * @exception IndexOutOfBoundsException if the start or end + * message numbers are out of range. + * @exception MessagingException for other failures + * @see Message#setFlags + * @see javax.mail.event.MessageChangedEvent + */ + public synchronized void setFlags(int start, int end, + Flags flag, boolean value) throws MessagingException { + for (int i = start; i <= end; i++) { + try { + Message msg = getMessage(i); + msg.setFlags(flag, value); + } catch (MessageRemovedException me) { + // This message is expunged, skip + } + } + } + + /** + * Set the specified flags on the messages whose message numbers + * are in the array. + * This will result in appropriate MessageChangedEvents being + * delivered to any MessageChangedListener registered on this + * Message's containing folder. <p> + * + * Certain Folder implementations can + * optimize the operation of setting Flags for a group of messages, + * so clients might want to use this method, rather than invoking + * <code>Message.setFlags</code> for each Message. <p> + * + * The default implementation uses <code>getMessage(int)</code> to + * get each <code>Message</code> object and then invokes + * <code>setFlags</code> on that object to set the flags. + * Specific Folder implementations that can optimize this case should do so. + * Also, an implementation must not abort the operation if a message + * number refers to an expunged message. + * + * @param msgnums the array of message numbers + * @param flag Flags object containing the flags to be set + * @param value set the flags to this boolean value + * @exception IllegalStateException if this folder is not opened + * or if it has been opened READ_ONLY. + * @exception IndexOutOfBoundsException if any message number + * in the given array is out of range. + * @exception MessagingException for other failures + * @see Message#setFlags + * @see javax.mail.event.MessageChangedEvent + */ + public synchronized void setFlags(int[] msgnums, + Flags flag, boolean value) throws MessagingException { + for (int i = 0; i < msgnums.length; i++) { + try { + Message msg = getMessage(msgnums[i]); + msg.setFlags(flag, value); + } catch (MessageRemovedException me) { + // This message is expunged, skip + } + } + } + + /** + * Copy the specified Messages from this Folder into another + * Folder. This operation appends these Messages to the + * destination Folder. The destination Folder does not have to + * be opened. An appropriate MessageCountEvent + * is delivered to any MessageCountListener registered on the + * destination folder when the messages arrive in the folder. <p> + * + * Note that the specified Message objects <strong>must</strong> + * belong to this folder. Folder implementations might be able + * to optimize this method by doing server-side copies. <p> + * + * This implementation just invokes <code>appendMessages()</code> + * on the destination folder to append the given Messages. Specific + * folder implementations that support server-side copies should + * do so, if the destination folder's Store is the same as this + * folder's Store. + * Also, an implementation must not abort the operation if a + * Message in the array turns out to be an expunged Message. + * + * @param msgs the array of message objects + * @param folder the folder to copy the messages to + * @exception FolderNotFoundException if the destination + * folder does not exist. + * @exception IllegalStateException if this folder is not opened. + * @exception MessagingException for other failures + * @see #appendMessages + */ + public void copyMessages(Message[] msgs, Folder folder) + throws MessagingException { + if (!folder.exists()) + throw new FolderNotFoundException( + folder.getFullName() + " does not exist", + folder); + + folder.appendMessages(msgs); + } + + /** + * Expunge (permanently remove) messages marked DELETED. Returns an + * array containing the expunged message objects. The + * <code>getMessageNumber</code> method + * on each of these message objects returns that Message's original + * (that is, prior to the expunge) sequence number. A MessageCountEvent + * containing the expunged messages is delivered to any + * MessageCountListeners registered on the folder. <p> + * + * Expunge causes the renumbering of Message objects subsequent to + * the expunged messages. Clients that use message numbers as + * references to messages should be aware of this and should be + * prepared to deal with the situation (probably by flushing out + * existing message number caches and reloading them). Because of + * this complexity, it is better for clients to use Message objects + * as references to messages, rather than message numbers. Any + * expunged Messages objects still have to be pruned, but other + * Messages in that folder are not affected by the expunge. <p> + * + * After a message is expunged, only the <code>isExpunged</code> and + * <code>getMessageNumber</code> methods are still valid on the + * corresponding Message object; other methods may throw + * <code>MessageRemovedException</code> + * + * @return array of expunged Message objects + * @exception FolderNotFoundException if this folder does not + * exist + * @exception IllegalStateException if this folder is not opened. + * @exception MessagingException for other failures + * @see Message#isExpunged + * @see javax.mail.event.MessageCountEvent + */ + public abstract Message[] expunge() throws MessagingException; + + /** + * Search this Folder for messages matching the specified + * search criterion. Returns an array containing the matching + * messages . Returns an empty array if no matches were found. <p> + * + * This implementation invokes + * <code>search(term, getMessages())</code>, to apply the search + * over all the messages in this folder. Providers that can implement + * server-side searching might want to override this method to provide + * a more efficient implementation. + * + * @param term the search criterion + * @return array of matching messages + * @exception javax.mail.search.SearchException if the search + * term is too complex for the implementation to handle. + * @exception FolderNotFoundException if this folder does + * not exist. + * @exception IllegalStateException if this folder is not opened. + * @exception MessagingException for other failures + * @see javax.mail.search.SearchTerm + */ + public Message[] search(SearchTerm term) throws MessagingException { + return search(term, getMessages()); + } + + /** + * Search the given array of messages for those that match the + * specified search criterion. Returns an array containing the + * matching messages. Returns an empty array if no matches were + * found. <p> + * + * Note that the specified Message objects <strong>must</strong> + * belong to this folder. <p> + * + * This implementation iterates through the given array of messages, + * and applies the search criterion on each message by calling + * its <code>match()</code> method with the given term. The + * messages that succeed in the match are returned. Providers + * that can implement server-side searching might want to override + * this method to provide a more efficient implementation. If the + * search term is too complex or contains user-defined terms that + * cannot be executed on the server, providers may elect to either + * throw a SearchException or degenerate to client-side searching by + * calling <code>super.search()</code> to invoke this implementation. + * + * @param term the search criterion + * @param msgs the messages to be searched + * @return array of matching messages + * @exception javax.mail.search.SearchException if the search + * term is too complex for the implementation to handle. + * @exception IllegalStateException if this folder is not opened + * @exception MessagingException for other failures + * @see javax.mail.search.SearchTerm + */ + public Message[] search(SearchTerm term, Message[] msgs) + throws MessagingException { + List<Message> matchedMsgs = new ArrayList<>(); + + // Run thru the given messages + for (Message msg : msgs) { + try { + if (msg.match(term)) // matched + matchedMsgs.add(msg); // add it + } catch(MessageRemovedException mrex) { } + } + + return matchedMsgs.toArray(new Message[matchedMsgs.size()]); + } + + /* + * The set of listeners are stored in Vectors appropriate to their + * type. We mark all listener Vectors as "volatile" because, while + * we initialize them inside this folder's synchronization lock, + * they are accessed (checked for null) in the "notify" methods, + * which can't be synchronized due to lock ordering constraints. + * Since the listener fields (the handles on the Vector objects) + * are only ever set, and are never cleared, we believe this is + * safe. The code that dispatches the notifications will either + * see the null and assume there are no listeners or will see the + * Vector and will process the listeners. There's an inherent race + * between adding a listener and notifying the listeners; the lack + * of synchronization during notification does not make the race + * condition significantly worse. If one thread is setting a + * listener at the "same" time an event is being dispatched, the + * dispatch code might not see the listener right away. The + * dispatch code doesn't have to worry about the Vector handle + * being set to null, and thus using an out-of-date set of + * listeners, because we never set the field to null. + */ + + // Vector of connection listeners. + private volatile Vector<ConnectionListener> connectionListeners = null; + + /** + * Add a listener for Connection events on this Folder. <p> + * + * The implementation provided here adds this listener + * to an internal list of ConnectionListeners. + * + * @param l the Listener for Connection events + * @see javax.mail.event.ConnectionEvent + */ + public synchronized void + addConnectionListener(ConnectionListener l) { + if (connectionListeners == null) + connectionListeners = new Vector<>(); + connectionListeners.addElement(l); + } + + /** + * Remove a Connection event listener. <p> + * + * The implementation provided here removes this listener + * from the internal list of ConnectionListeners. + * + * @param l the listener + * @see #addConnectionListener + */ + public synchronized void + removeConnectionListener(ConnectionListener l) { + if (connectionListeners != null) + connectionListeners.removeElement(l); + } + + /** + * Notify all ConnectionListeners. Folder implementations are + * expected to use this method to broadcast connection events. <p> + * + * The provided implementation queues the event into + * an internal event queue. An event dispatcher thread dequeues + * events from the queue and dispatches them to the registered + * ConnectionListeners. Note that the event dispatching occurs + * in a separate thread, thus avoiding potential deadlock problems. + * + * @param type the ConnectionEvent type + * @see javax.mail.event.ConnectionEvent + */ + protected void notifyConnectionListeners(int type) { + if (connectionListeners != null) { + ConnectionEvent e = new ConnectionEvent(this, type); + queueEvent(e, connectionListeners); + } + + /* Fix for broken JDK1.1.x Garbage collector : + * The 'conservative' GC in JDK1.1.x occasionally fails to + * garbage-collect Threads which are in the wait state. + * This would result in thread (and consequently memory) leaks. + * + * We attempt to fix this by sending a 'terminator' event + * to the queue, after we've sent the CLOSED event. The + * terminator event causes the event-dispatching thread to + * self destruct. + */ + if (type == ConnectionEvent.CLOSED) + q.terminateQueue(); + } + + // Vector of folder listeners + private volatile Vector<FolderListener> folderListeners = null; + + /** + * Add a listener for Folder events on this Folder. <p> + * + * The implementation provided here adds this listener + * to an internal list of FolderListeners. + * + * @param l the Listener for Folder events + * @see javax.mail.event.FolderEvent + */ + public synchronized void addFolderListener(FolderListener l) { + if (folderListeners == null) + folderListeners = new Vector<>(); + folderListeners.addElement(l); + } + + /** + * Remove a Folder event listener. <p> + * + * The implementation provided here removes this listener + * from the internal list of FolderListeners. + * + * @param l the listener + * @see #addFolderListener + */ + public synchronized void removeFolderListener(FolderListener l) { + if (folderListeners != null) + folderListeners.removeElement(l); + } + + /** + * Notify all FolderListeners registered on this Folder and + * this folder's Store. Folder implementations are expected + * to use this method to broadcast Folder events. <p> + * + * The implementation provided here queues the event into + * an internal event queue. An event dispatcher thread dequeues + * events from the queue and dispatches them to the + * FolderListeners registered on this folder. The implementation + * also invokes <code>notifyFolderListeners</code> on this folder's + * Store to notify any FolderListeners registered on the store. + * + * @param type type of FolderEvent + * @see #notifyFolderRenamedListeners + */ + protected void notifyFolderListeners(int type) { + if (folderListeners != null) { + FolderEvent e = new FolderEvent(this, this, type); + queueEvent(e, folderListeners); + } + store.notifyFolderListeners(type, this); + } + + /** + * Notify all FolderListeners registered on this Folder and + * this folder's Store about the renaming of this folder. + * Folder implementations are expected to use this method to + * broadcast Folder events indicating the renaming of folders. <p> + * + * The implementation provided here queues the event into + * an internal event queue. An event dispatcher thread dequeues + * events from the queue and dispatches them to the + * FolderListeners registered on this folder. The implementation + * also invokes <code>notifyFolderRenamedListeners</code> on this + * folder's Store to notify any FolderListeners registered on the store. + * + * @param folder Folder representing the new name. + * @see #notifyFolderListeners + * @since JavaMail 1.1 + */ + protected void notifyFolderRenamedListeners(Folder folder) { + if (folderListeners != null) { + FolderEvent e = new FolderEvent(this, this, folder, + FolderEvent.RENAMED); + queueEvent(e, folderListeners); + } + store.notifyFolderRenamedListeners(this, folder); + } + + // Vector of MessageCount listeners + private volatile Vector<MessageCountListener> messageCountListeners = null; + + /** + * Add a listener for MessageCount events on this Folder. <p> + * + * The implementation provided here adds this listener + * to an internal list of MessageCountListeners. + * + * @param l the Listener for MessageCount events + * @see javax.mail.event.MessageCountEvent + */ + public synchronized void addMessageCountListener(MessageCountListener l) { + if (messageCountListeners == null) + messageCountListeners = new Vector<>(); + messageCountListeners.addElement(l); + } + + /** + * Remove a MessageCount listener. <p> + * + * The implementation provided here removes this listener + * from the internal list of MessageCountListeners. + * + * @param l the listener + * @see #addMessageCountListener + */ + public synchronized void + removeMessageCountListener(MessageCountListener l) { + if (messageCountListeners != null) + messageCountListeners.removeElement(l); + } + + /** + * Notify all MessageCountListeners about the addition of messages + * into this folder. Folder implementations are expected to use this + * method to broadcast MessageCount events for indicating arrival of + * new messages. <p> + * + * The provided implementation queues the event into + * an internal event queue. An event dispatcher thread dequeues + * events from the queue and dispatches them to the registered + * MessageCountListeners. Note that the event dispatching occurs + * in a separate thread, thus avoiding potential deadlock problems. + * + * @param msgs the messages that were added + */ + protected void notifyMessageAddedListeners(Message[] msgs) { + if (messageCountListeners == null) + return; + + MessageCountEvent e = new MessageCountEvent( + this, + MessageCountEvent.ADDED, + false, + msgs); + + queueEvent(e, messageCountListeners); + } + + /** + * Notify all MessageCountListeners about the removal of messages + * from this Folder. Folder implementations are expected to use this + * method to broadcast MessageCount events indicating removal of + * messages. <p> + * + * The provided implementation queues the event into + * an internal event queue. An event dispatcher thread dequeues + * events from the queue and dispatches them to the registered + * MessageCountListeners. Note that the event dispatching occurs + * in a separate thread, thus avoiding potential deadlock problems. + * + * @param removed was the message removed by this client? + * @param msgs the messages that were removed + */ + protected void notifyMessageRemovedListeners(boolean removed, + Message[] msgs) { + if (messageCountListeners == null) + return; + + MessageCountEvent e = new MessageCountEvent( + this, + MessageCountEvent.REMOVED, + removed, + msgs); + queueEvent(e, messageCountListeners); + } + + // Vector of MessageChanged listeners. + private volatile Vector<MessageChangedListener> messageChangedListeners + = null; + + /** + * Add a listener for MessageChanged events on this Folder. <p> + * + * The implementation provided here adds this listener + * to an internal list of MessageChangedListeners. + * + * @param l the Listener for MessageChanged events + * @see javax.mail.event.MessageChangedEvent + */ + public synchronized void + addMessageChangedListener(MessageChangedListener l) { + if (messageChangedListeners == null) + messageChangedListeners = new Vector<>(); + messageChangedListeners.addElement(l); + } + + /** + * Remove a MessageChanged listener. <p> + * + * The implementation provided here removes this listener + * from the internal list of MessageChangedListeners. + * + * @param l the listener + * @see #addMessageChangedListener + */ + public synchronized void + removeMessageChangedListener(MessageChangedListener l) { + if (messageChangedListeners != null) + messageChangedListeners.removeElement(l); + } + + /** + * Notify all MessageChangedListeners. Folder implementations are + * expected to use this method to broadcast MessageChanged events. <p> + * + * The provided implementation queues the event into + * an internal event queue. An event dispatcher thread dequeues + * events from the queue and dispatches them to registered + * MessageChangedListeners. Note that the event dispatching occurs + * in a separate thread, thus avoiding potential deadlock problems. + * + * @param type the MessageChangedEvent type + * @param msg the message that changed + */ + protected void notifyMessageChangedListeners(int type, Message msg) { + if (messageChangedListeners == null) + return; + + MessageChangedEvent e = new MessageChangedEvent(this, type, msg); + queueEvent(e, messageChangedListeners); + } + + /* + * Add the event and vector of listeners to the queue to be delivered. + */ + @SuppressWarnings("unchecked") + private void queueEvent(MailEvent event, + Vector<? extends EventListener> vector) { + /* + * Copy the vector in order to freeze the state of the set + * of EventListeners the event should be delivered to prior + * to delivery. This ensures that any changes made to the + * Vector from a target listener's method during the delivery + * of this event will not take effect until after the event is + * delivered. + */ + Vector<? extends EventListener> v = (Vector)vector.clone(); + q.enqueue(event, v); + } + + @Override + protected void finalize() throws Throwable { + try { + q.terminateQueue(); + } finally { + super.finalize(); + } + } + + /** + * override the default toString(), it will return the String + * from Folder.getFullName() or if that is null, it will use + * the default toString() behavior. + */ + + @Override + public String toString() { + String s = getFullName(); + if (s != null) + return s; + else + return super.toString(); + } +}
diff --git a/mail/src/main/java/javax/mail/FolderClosedException.java b/mail/src/main/java/javax/mail/FolderClosedException.java new file mode 100644 index 0000000..7e8a4b4 --- /dev/null +++ b/mail/src/main/java/javax/mail/FolderClosedException.java
@@ -0,0 +1,83 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +/** + * This exception is thrown when a method is invoked on a Messaging object + * and the Folder that owns that object has died due to some reason. <p> + * + * Following the exception, the Folder is reset to the "closed" state. + * All messaging objects owned by the Folder should be considered invalid. + * The Folder can be reopened using the "open" method to reestablish the + * lost connection. <p> + * + * The getMessage() method returns more detailed information about the + * error that caused this exception. <p> + * + * @author John Mani + */ + +public class FolderClosedException extends MessagingException { + transient private Folder folder; + + private static final long serialVersionUID = 1687879213433302315L; + + /** + * Constructs a FolderClosedException. + * + * @param folder The Folder + */ + public FolderClosedException(Folder folder) { + this(folder, null); + } + + /** + * Constructs a FolderClosedException with the specified + * detail message. + * + * @param folder The Folder + * @param message The detailed error message + */ + public FolderClosedException(Folder folder, String message) { + super(message); + this.folder = folder; + } + + /** + * Constructs a FolderClosedException with the specified + * detail message and embedded exception. The exception is chained + * to this exception. + * + * @param folder The Folder + * @param message The detailed error message + * @param e The embedded exception + * @since JavaMail 1.5 + */ + public FolderClosedException(Folder folder, String message, Exception e) { + super(message, e); + this.folder = folder; + } + + /** + * Returns the dead Folder object + * + * @return the dead Folder object + */ + public Folder getFolder() { + return folder; + } +}
diff --git a/mail/src/main/java/javax/mail/FolderNotFoundException.java b/mail/src/main/java/javax/mail/FolderNotFoundException.java new file mode 100644 index 0000000..0ea6745 --- /dev/null +++ b/mail/src/main/java/javax/mail/FolderNotFoundException.java
@@ -0,0 +1,100 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +import java.lang.*; + +/** + * This exception is thrown by Folder methods, when those + * methods are invoked on a non existent folder. + * + * @author John Mani + */ + +public class FolderNotFoundException extends MessagingException { + transient private Folder folder; + + private static final long serialVersionUID = 472612108891249403L; + + /** + * Constructs a FolderNotFoundException with no detail message. + */ + public FolderNotFoundException() { + super(); + } + + /** + * Constructs a FolderNotFoundException. + * + * @param folder The Folder + * @since JavaMail 1.2 + */ + public FolderNotFoundException(Folder folder) { + super(); + this.folder = folder; + } + + /** + * Constructs a FolderNotFoundException with the specified + * detail message. + * + * @param folder The Folder + * @param s The detailed error message + * @since JavaMail 1.2 + */ + public FolderNotFoundException(Folder folder, String s) { + super(s); + this.folder = folder; + } + + /** + * Constructs a FolderNotFoundException with the specified + * detail message and embedded exception. The exception is chained + * to this exception. + * + * @param folder The Folder + * @param s The detailed error message + * @param e The embedded exception + * @since JavaMail 1.5 + */ + public FolderNotFoundException(Folder folder, String s, Exception e) { + super(s, e); + this.folder = folder; + } + + /** + * Constructs a FolderNotFoundException with the specified detail message + * and the specified folder. + * + * @param s The detail message + * @param folder The Folder + */ + public FolderNotFoundException(String s, Folder folder) { + super(s); + this.folder = folder; + } + + /** + * Returns the offending Folder object. + * + * @return the Folder object. Note that the returned value can be + * <code>null</code>. + */ + public Folder getFolder() { + return folder; + } +}
diff --git a/mail/src/main/java/javax/mail/Header.java b/mail/src/main/java/javax/mail/Header.java new file mode 100644 index 0000000..924f84c --- /dev/null +++ b/mail/src/main/java/javax/mail/Header.java
@@ -0,0 +1,70 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + + +/** + * The Header class stores a name/value pair to represent headers. + * + * @author John Mani + */ + +public class Header { + + /** + * The name of the header. + * + * @since JavaMail 1.4 + */ + protected String name; + + /** + * The value of the header. + * + * @since JavaMail 1.4 + */ + protected String value; + + /** + * Construct a Header object. + * + * @param name name of the header + * @param value value of the header + */ + public Header(String name, String value) { + this.name = name; + this.value = value; + } + + /** + * Returns the name of this header. + * + * @return name of the header + */ + public String getName() { + return name; + } + + /** + * Returns the value of this header. + * + * @return value of the header + */ + public String getValue() { + return value; + } +}
diff --git a/mail/src/main/java/javax/mail/IllegalWriteException.java b/mail/src/main/java/javax/mail/IllegalWriteException.java new file mode 100644 index 0000000..8011c4c --- /dev/null +++ b/mail/src/main/java/javax/mail/IllegalWriteException.java
@@ -0,0 +1,60 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + + +/** + * The exception thrown when a write is attempted on a read-only attribute + * of any Messaging object. + * + * @author John Mani + */ + +public class IllegalWriteException extends MessagingException { + + private static final long serialVersionUID = 3974370223328268013L; + + /** + * Constructs an IllegalWriteException with no detail message. + */ + public IllegalWriteException() { + super(); + } + + /** + * Constructs an IllegalWriteException with the specified + * detail message. + * + * @param s The detailed error message + */ + public IllegalWriteException(String s) { + super(s); + } + + /** + * Constructs an IllegalWriteException with the specified + * detail message and embedded exception. The exception is chained + * to this exception. + * + * @param s The detailed error message + * @param e The embedded exception + * @since JavaMail 1.5 + */ + public IllegalWriteException(String s, Exception e) { + super(s, e); + } +}
diff --git a/mail/src/main/java/javax/mail/MailSessionDefinition.java b/mail/src/main/java/javax/mail/MailSessionDefinition.java new file mode 100644 index 0000000..788fa4e --- /dev/null +++ b/mail/src/main/java/javax/mail/MailSessionDefinition.java
@@ -0,0 +1,108 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.annotation.Repeatable; + +/** + * Annotation used by Java EE applications to define a <code>MailSession</code> + * to be registered with JNDI. The <code>MailSession</code> may be configured + * by setting the annotation elements for commonly used <code>Session</code> + * properties. Additional standard and vendor-specific properties may be + * specified using the <code>properties</code> element. + * <p> + * The session will be registered under the name specified in the + * <code>name</code> element. It may be defined to be in any valid + * <code>Java EE</code> namespace, and will determine the accessibility of + * the session from other components. + * + * @since JavaMail 1.5 + */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Repeatable(MailSessionDefinitions.class) +public @interface MailSessionDefinition { + + /** + * Description of this mail session. + * + * @return the description + */ + String description() default ""; + + /** + * JNDI name by which the mail session will be registered. + * + * @return the JNDI name + */ + String name(); + + /** + * Store protocol name. + * + * @return the store protocol name + */ + String storeProtocol() default ""; + + /** + * Transport protocol name. + * + * @return the transport protocol name + */ + String transportProtocol() default ""; + + /** + * Host name for the mail server. + * + * @return the host name + */ + String host() default ""; + + /** + * User name to use for authentication. + * + * @return the user name + */ + String user() default ""; + + /** + * Password to use for authentication. + * + * @return the password + */ + String password() default ""; + + /** + * From address for the user. + * + * @return the from address + */ + String from() default ""; + + /** + * Properties to include in the Session. + * Properties are specified using the format: + * <i>propertyName=propertyValue</i> with one property per array element. + * + * @return the properties + */ + String[] properties() default {}; +}
diff --git a/mail/src/main/java/javax/mail/MailSessionDefinitions.java b/mail/src/main/java/javax/mail/MailSessionDefinitions.java new file mode 100644 index 0000000..b97462b --- /dev/null +++ b/mail/src/main/java/javax/mail/MailSessionDefinitions.java
@@ -0,0 +1,34 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +import java.lang.annotation.Target; +import java.lang.annotation.Retention; +import java.lang.annotation.ElementType; +import java.lang.annotation.RetentionPolicy; + +/** + * Declares one or more <code>MailSessionDefinition</code> annotations. + * + * @see MailSessionDefinition + * @since JavaMail 1.5 + */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface MailSessionDefinitions { + MailSessionDefinition[] value(); +}
diff --git a/mail/src/main/java/javax/mail/Message.java b/mail/src/main/java/javax/mail/Message.java new file mode 100644 index 0000000..370fd28 --- /dev/null +++ b/mail/src/main/java/javax/mail/Message.java
@@ -0,0 +1,704 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +import java.util.Date; +import java.io.*; +import javax.mail.search.SearchTerm; + +/** + * This class models an email message. This is an abstract class. + * Subclasses provide actual implementations. <p> + * + * Message implements the Part interface. Message contains a set of + * attributes and a "content". Messages within a folder also have a + * set of flags that describe its state within the folder.<p> + * + * Message defines some new attributes in addition to those defined + * in the <code>Part</code> interface. These attributes specify meta-data + * for the message - i.e., addressing and descriptive information about + * the message. <p> + * + * Message objects are obtained either from a Folder or by constructing + * a new Message object of the appropriate subclass. Messages that have + * been received are normally retrieved from a folder named "INBOX". <p> + * + * A Message object obtained from a folder is just a lightweight + * reference to the actual message. The Message is 'lazily' filled + * up (on demand) when each item is requested from the message. Note + * that certain folder implementations may return Message objects that + * are pre-filled with certain user-specified items. + + * To send a message, an appropriate subclass of Message (e.g., + * MimeMessage) is instantiated, the attributes and content are + * filled in, and the message is sent using the <code>Transport.send</code> + * method. <p> + * + * @author John Mani + * @author Bill Shannon + * @author Max Spivak + * @see javax.mail.Part + */ + +public abstract class Message implements Part { + + /** + * The number of this message within its folder, or zero if + * the message was not retrieved from a folder. + */ + protected int msgnum = 0; + + /** + * True if this message has been expunged. + */ + protected boolean expunged = false; + + /** + * The containing folder, if this message is obtained from a folder + */ + protected Folder folder = null; + + /** + * The Session object for this Message + */ + protected Session session = null; + + /** + * No-arg version of the constructor. + */ + protected Message() { } + + /** + * Constructor that takes a Folder and a message number. + * Used by Folder implementations. + * + * @param folder containing folder + * @param msgnum this message's sequence number within this folder + */ + protected Message(Folder folder, int msgnum) { + this.folder = folder; + this.msgnum = msgnum; + session = folder.store.session; + } + + /** + * Constructor that takes a Session. Used for client created + * Message objects. + * + * @param session A Session object + */ + protected Message(Session session) { + this.session = session; + } + + /** + * Return the Session used when this message was created. + * + * @return the message's Session + * @since JavaMail 1.5 + */ + public Session getSession() { + return session; + } + + /** + * Returns the "From" attribute. The "From" attribute contains + * the identity of the person(s) who wished this message to + * be sent. <p> + * + * In certain implementations, this may be different + * from the entity that actually sent the message. <p> + * + * This method returns <code>null</code> if this attribute + * is not present in this message. Returns an empty array if + * this attribute is present, but contains no addresses. + * + * @return array of Address objects + * @exception MessagingException for failures + */ + public abstract Address[] getFrom() throws MessagingException; + + /** + * Set the "From" attribute in this Message. The value of this + * attribute is obtained from the property "mail.user". If this + * property is absent, the system property "user.name" is used. + * + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + public abstract void setFrom() throws MessagingException; + + /** + * Set the "From" attribute in this Message. + * + * @param address the sender + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + public abstract void setFrom(Address address) + throws MessagingException; + + /** + * Add these addresses to the existing "From" attribute + * + * @param addresses the senders + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + public abstract void addFrom(Address[] addresses) + throws MessagingException; + + /** + * This inner class defines the types of recipients allowed by + * the Message class. The currently defined types are TO, + * CC and BCC. + * + * Note that this class only has a protected constructor, thereby + * restricting new Recipient types to either this class or subclasses. + * This effectively implements an enumeration of the allowed Recipient + * types. + * + * The following code sample shows how to use this class to obtain + * the "TO" recipients from a message. + * <blockquote><pre> + * + * Message msg = folder.getMessages(1); + * Address[] a = m.getRecipients(Message.RecipientType.TO); + * + * </pre></blockquote> + * + * @see javax.mail.Message#getRecipients + * @see javax.mail.Message#setRecipients + * @see javax.mail.Message#addRecipients + */ + public static class RecipientType implements Serializable { + /** + * The "To" (primary) recipients. + */ + public static final RecipientType TO = new RecipientType("To"); + /** + * The "Cc" (carbon copy) recipients. + */ + public static final RecipientType CC = new RecipientType("Cc"); + /** + * The "Bcc" (blind carbon copy) recipients. + */ + public static final RecipientType BCC = new RecipientType("Bcc"); + + /** + * The type of recipient, usually the name of a corresponding + * Internet standard header. + * + * @serial + */ + protected String type; + + private static final long serialVersionUID = -7479791750606340008L; + + /** + * Constructor for use by subclasses. + * + * @param type the recipient type + */ + protected RecipientType(String type) { + this.type = type; + } + + /** + * When deserializing a RecipientType, we need to make sure to + * return only one of the known static final instances defined + * in this class. Subclasses must implement their own + * <code>readResolve</code> method that checks for their known + * instances before calling this super method. + * + * @return the RecipientType object instance + * @exception ObjectStreamException for object stream errors + */ + protected Object readResolve() throws ObjectStreamException { + if (type.equals("To")) + return TO; + else if (type.equals("Cc")) + return CC; + else if (type.equals("Bcc")) + return BCC; + else + throw new InvalidObjectException( + "Attempt to resolve unknown RecipientType: " + type); + } + + @Override + public String toString() { + return type; + } + } + + /** + * Get all the recipient addresses of the given type. <p> + * + * This method returns <code>null</code> if no recipients of + * the given type are present in this message. It may return an + * empty array if the header is present, but contains no addresses. + * + * @param type the recipient type + * @return array of Address objects + * @exception MessagingException for failures + * @see Message.RecipientType#TO + * @see Message.RecipientType#CC + * @see Message.RecipientType#BCC + */ + public abstract Address[] getRecipients(RecipientType type) + throws MessagingException; + + /** + * Get all the recipient addresses for the message. + * The default implementation extracts the TO, CC, and BCC + * recipients using the <code>getRecipients</code> method. <p> + * + * This method returns <code>null</code> if none of the recipient + * headers are present in this message. It may Return an empty array + * if any recipient header is present, but contains no addresses. + * + * @return array of Address objects + * @exception MessagingException for failures + * @see Message.RecipientType#TO + * @see Message.RecipientType#CC + * @see Message.RecipientType#BCC + * @see #getRecipients + */ + public Address[] getAllRecipients() throws MessagingException { + Address[] to = getRecipients(RecipientType.TO); + Address[] cc = getRecipients(RecipientType.CC); + Address[] bcc = getRecipients(RecipientType.BCC); + + if (cc == null && bcc == null) + return to; // a common case + + int numRecip = + (to != null ? to.length : 0) + + (cc != null ? cc.length : 0) + + (bcc != null ? bcc.length : 0); + Address[] addresses = new Address[numRecip]; + int pos = 0; + if (to != null) { + System.arraycopy(to, 0, addresses, pos, to.length); + pos += to.length; + } + if (cc != null) { + System.arraycopy(cc, 0, addresses, pos, cc.length); + pos += cc.length; + } + if (bcc != null) { + System.arraycopy(bcc, 0, addresses, pos, bcc.length); + // pos += bcc.length; + } + return addresses; + } + + /** + * Set the recipient addresses. All addresses of the specified + * type are replaced by the addresses parameter. + * + * @param type the recipient type + * @param addresses the addresses + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + public abstract void setRecipients(RecipientType type, Address[] addresses) + throws MessagingException; + + /** + * Set the recipient address. All addresses of the specified + * type are replaced by the address parameter. <p> + * + * The default implementation uses the <code>setRecipients</code> method. + * + * @param type the recipient type + * @param address the address + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception MessagingException for other failures + */ + public void setRecipient(RecipientType type, Address address) + throws MessagingException { + if (address == null) + setRecipients(type, null); + else { + Address[] a = new Address[1]; + a[0] = address; + setRecipients(type, a); + } + } + + /** + * Add these recipient addresses to the existing ones of the given type. + * + * @param type the recipient type + * @param addresses the addresses + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + public abstract void addRecipients(RecipientType type, Address[] addresses) + throws MessagingException; + + /** + * Add this recipient address to the existing ones of the given type. <p> + * + * The default implementation uses the <code>addRecipients</code> method. + * + * @param type the recipient type + * @param address the address + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception MessagingException for other failures + */ + public void addRecipient(RecipientType type, Address address) + throws MessagingException { + Address[] a = new Address[1]; + a[0] = address; + addRecipients(type, a); + } + + /** + * Get the addresses to which replies should be directed. + * This will usually be the sender of the message, but + * some messages may direct replies to a different address. <p> + * + * The default implementation simply calls the <code>getFrom</code> + * method. <p> + * + * This method returns <code>null</code> if the corresponding + * header is not present. Returns an empty array if the header + * is present, but contains no addresses. + * + * @return addresses to which replies should be directed + * @exception MessagingException for failures + * @see #getFrom + */ + public Address[] getReplyTo() throws MessagingException { + return getFrom(); + } + + /** + * Set the addresses to which replies should be directed. + * (Normally only a single address will be specified.) + * Not all message types allow this to be specified separately + * from the sender of the message. <p> + * + * The default implementation provided here just throws the + * MethodNotSupportedException. + * + * @param addresses addresses to which replies should be directed + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MethodNotSupportedException if the underlying + * implementation does not support setting this + * attribute + * @exception MessagingException for other failures + */ + public void setReplyTo(Address[] addresses) throws MessagingException { + throw new MethodNotSupportedException("setReplyTo not supported"); + } + + /** + * Get the subject of this message. + * + * @return the subject + * @exception MessagingException for failures + */ + public abstract String getSubject() throws MessagingException; + + /** + * Set the subject of this message. + * + * @param subject the subject + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + public abstract void setSubject(String subject) + throws MessagingException; + + /** + * Get the date this message was sent. + * + * @return the date this message was sent + * @exception MessagingException for failures + */ + public abstract Date getSentDate() throws MessagingException; + + /** + * Set the sent date of this message. + * + * @param date the sent date of this message + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + public abstract void setSentDate(Date date) throws MessagingException; + + /** + * Get the date this message was received. + * + * @return the date this message was received + * @exception MessagingException for failures + */ + public abstract Date getReceivedDate() throws MessagingException; + + /** + * Returns a <code>Flags</code> object containing the flags for + * this message. <p> + * + * Modifying any of the flags in this returned Flags object will + * not affect the flags of this message. Use <code>setFlags()</code> + * to do that. <p> + * + * @return Flags object containing the flags for this message + * @see javax.mail.Flags + * @see #setFlags + * @exception MessagingException for failures + */ + public abstract Flags getFlags() throws MessagingException; + + /** + * Check whether the flag specified in the <code>flag</code> + * argument is set in this message. <p> + * + * The default implementation uses <code>getFlags</code>. + * + * @param flag the flag + * @return value of the specified flag for this message + * @see javax.mail.Flags.Flag + * @see javax.mail.Flags.Flag#ANSWERED + * @see javax.mail.Flags.Flag#DELETED + * @see javax.mail.Flags.Flag#DRAFT + * @see javax.mail.Flags.Flag#FLAGGED + * @see javax.mail.Flags.Flag#RECENT + * @see javax.mail.Flags.Flag#SEEN + * @exception MessagingException for failures + */ + public boolean isSet(Flags.Flag flag) throws MessagingException { + return getFlags().contains(flag); + } + + /** + * Set the specified flags on this message to the specified value. + * Note that any flags in this message that are not specified in + * the given <code>Flags</code> object are unaffected. <p> + * + * This will result in a <code>MessageChangedEvent</code> being + * delivered to any MessageChangedListener registered on this + * Message's containing folder. + * + * @param flag Flags object containing the flags to be set + * @param set the value to be set + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values. + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + * @see javax.mail.event.MessageChangedEvent + */ + public abstract void setFlags(Flags flag, boolean set) + throws MessagingException; + + /** + * Set the specified flag on this message to the specified value. + * + * This will result in a <code>MessageChangedEvent</code> being + * delivered to any MessageChangedListener registered on this + * Message's containing folder. <p> + * + * The default implementation uses the <code>setFlags</code> method. + * + * @param flag Flags.Flag object containing the flag to be set + * @param set the value to be set + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values. + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + * @see javax.mail.event.MessageChangedEvent + */ + public void setFlag(Flags.Flag flag, boolean set) + throws MessagingException { + Flags f = new Flags(flag); + setFlags(f, set); + } + + /** + * Get the Message number for this Message. + * A Message object's message number is the relative + * position of this Message in its Folder. Note that the message + * number for a particular Message can change during a session + * if other messages in the Folder are deleted and expunged. <p> + * + * Valid message numbers start at 1. Messages that do not belong + * to any folder (like newly composed or derived messages) have 0 + * as their message number. + * + * @return the message number + */ + public int getMessageNumber() { + return msgnum; + } + + /** + * Set the Message number for this Message. This method is + * invoked only by the implementation classes. + * + * @param msgnum the message number + */ + protected void setMessageNumber(int msgnum) { + this.msgnum = msgnum; + } + + /** + * Get the folder from which this message was obtained. If + * this is a new message or nested message, this method returns + * null. + * + * @return the containing folder + */ + public Folder getFolder() { + return folder; + } + + /** + * Checks whether this message is expunged. All other methods except + * <code>getMessageNumber()</code> are invalid on an expunged + * Message object. <p> + * + * Messages that are expunged due to an explict <code>expunge()</code> + * request on the containing Folder are removed from the Folder + * immediately. Messages that are externally expunged by another source + * are marked "expunged" and return true for the isExpunged() method, + * but they are not removed from the Folder until an explicit + * <code>expunge()</code> is done on the Folder. <p> + * + * See the description of <code>expunge()</code> for more details on + * expunge handling. + * + * @return true if the message is expunged + * @see Folder#expunge + */ + public boolean isExpunged() { + return expunged; + } + + /** + * Sets the expunged flag for this Message. This method is to + * be used only by the implementation classes. + * + * @param expunged the expunged flag + */ + protected void setExpunged(boolean expunged) { + this.expunged = expunged; + } + + /** + * Get a new Message suitable for a reply to this message. + * The new Message will have its attributes and headers + * set up appropriately. Note that this new message object + * will be empty, that is, it will <strong>not</strong> have a "content". + * These will have to be suitably filled in by the client. <p> + * + * If <code>replyToAll</code> is set, the new Message will be addressed + * to all recipients of this message. Otherwise, the reply will be + * addressed to only the sender of this message (using the value + * of the <code>getReplyTo</code> method). <p> + * + * The "Subject" field is filled in with the original subject + * prefixed with "Re:" (unless it already starts with "Re:"). <p> + * + * The reply message will use the same session as this message. + * + * @param replyToAll reply should be sent to all recipients + * of this message + * @return the reply Message + * @exception MessagingException for failures + */ + public abstract Message reply(boolean replyToAll) throws MessagingException; + + /** + * Save any changes made to this message into the message-store + * when the containing folder is closed, if the message is contained + * in a folder. (Some implementations may save the changes + * immediately.) Update any header fields to be consistent with the + * changed message contents. If any part of a message's headers or + * contents are changed, saveChanges must be called to ensure that + * those changes are permanent. If saveChanges is not called, any + * such modifications may or may not be saved, depending on the + * message store and folder implementation. <p> + * + * Messages obtained from folders opened READ_ONLY should not be + * modified and saveChanges should not be called on such messages. + * + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values. + * @exception MessagingException for other failures + */ + public abstract void saveChanges() throws MessagingException; + + /** + * Apply the specified Search criterion to this message. + * + * @param term the Search criterion + * @return true if the Message matches this search + * criterion, false otherwise. + * @exception MessagingException for failures + * @see javax.mail.search.SearchTerm + */ + public boolean match(SearchTerm term) throws MessagingException { + return term.match(this); + } +}
diff --git a/mail/src/main/java/javax/mail/MessageAware.java b/mail/src/main/java/javax/mail/MessageAware.java new file mode 100644 index 0000000..7657fcc --- /dev/null +++ b/mail/src/main/java/javax/mail/MessageAware.java
@@ -0,0 +1,36 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +/** + * An interface optionally implemented by <code>DataSources</code> to + * supply information to a <code>DataContentHandler</code> about the + * message context in which the data content object is operating. + * + * @see javax.mail.MessageContext + * @see javax.activation.DataSource + * @see javax.activation.DataContentHandler + * @since JavaMail 1.1 + */ +public interface MessageAware { + /** + * Return the message context. + * + * @return the message context + */ + public MessageContext getMessageContext(); +}
diff --git a/mail/src/main/java/javax/mail/MessageContext.java b/mail/src/main/java/javax/mail/MessageContext.java new file mode 100644 index 0000000..a1365cb --- /dev/null +++ b/mail/src/main/java/javax/mail/MessageContext.java
@@ -0,0 +1,100 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +/** + * The context in which a piece of Message content is contained. A + * <code>MessageContext</code> object is returned by the + * <code>getMessageContext</code> method of the + * <code>MessageAware</code> interface. <code>MessageAware</code> is + * typically implemented by <code>DataSources</code> to allow a + * <code>DataContentHandler</code> to pass on information about the + * context in which a data content object is operating. + * + * @see javax.mail.MessageAware + * @see javax.activation.DataSource + * @see javax.activation.DataContentHandler + * @since JavaMail 1.1 + */ +public class MessageContext { + private Part part; + + /** + * Create a MessageContext object describing the context of the given Part. + * + * @param part the Part + */ + public MessageContext(Part part) { + this.part = part; + } + + /** + * Return the Part that contains the content. + * + * @return the containing Part, or null if not known + */ + public Part getPart() { + return part; + } + + /** + * Return the Message that contains the content. + * Follows the parent chain up through containing Multipart + * objects until it comes to a Message object, or null. + * + * @return the containing Message, or null if not known + */ + public Message getMessage() { + try { + return getMessage(part); + } catch (MessagingException ex) { + return null; + } + } + + /** + * Return the Message containing an arbitrary Part. + * Follows the parent chain up through containing Multipart + * objects until it comes to a Message object, or null. + * + * @return the containing Message, or null if none + * @see javax.mail.BodyPart#getParent + * @see javax.mail.Multipart#getParent + */ + private static Message getMessage(Part p) throws MessagingException { + while (p != null) { + if (p instanceof Message) + return (Message)p; + BodyPart bp = (BodyPart)p; + Multipart mp = bp.getParent(); + if (mp == null) // MimeBodyPart might not be in a MimeMultipart + return null; + p = mp.getParent(); + } + return null; + } + + /** + * Return the Session we're operating in. + * + * @return the Session, or null if not known + */ + public Session getSession() { + Message msg = getMessage(); + return msg != null ? msg.getSession() : null; + } +}
diff --git a/mail/src/main/java/javax/mail/MessageRemovedException.java b/mail/src/main/java/javax/mail/MessageRemovedException.java new file mode 100644 index 0000000..28cde68 --- /dev/null +++ b/mail/src/main/java/javax/mail/MessageRemovedException.java
@@ -0,0 +1,62 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +/** + * The exception thrown when an invalid method is invoked on an expunged + * Message. The only valid methods on an expunged Message are + * <code>isExpunged()</code> and <code>getMessageNumber()</code>. + * + * @see javax.mail.Message#isExpunged() + * @see javax.mail.Message#getMessageNumber() + * @author John Mani + */ + +public class MessageRemovedException extends MessagingException { + + private static final long serialVersionUID = 1951292550679528690L; + + /** + * Constructs a MessageRemovedException with no detail message. + */ + public MessageRemovedException() { + super(); + } + + /** + * Constructs a MessageRemovedException with the specified + * detail message. + * + * @param s The detailed error message + */ + public MessageRemovedException(String s) { + super(s); + } + + /** + * Constructs a MessageRemovedException with the specified + * detail message and embedded exception. The exception is chained + * to this exception. + * + * @param s The detailed error message + * @param e The embedded exception + * @since JavaMail 1.5 + */ + public MessageRemovedException(String s, Exception e) { + super(s, e); + } +}
diff --git a/mail/src/main/java/javax/mail/MessagingException.java b/mail/src/main/java/javax/mail/MessagingException.java new file mode 100644 index 0000000..803a4af --- /dev/null +++ b/mail/src/main/java/javax/mail/MessagingException.java
@@ -0,0 +1,152 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +import java.lang.*; + +/** + * The base class for all exceptions thrown by the Messaging classes + * + * @author John Mani + * @author Bill Shannon + */ + +public class MessagingException extends Exception { + + /** + * The next exception in the chain. + * + * @serial + */ + private Exception next; + + private static final long serialVersionUID = -7569192289819959253L; + + /** + * Constructs a MessagingException with no detail message. + */ + public MessagingException() { + super(); + initCause(null); // prevent anyone else from setting it + } + + /** + * Constructs a MessagingException with the specified detail message. + * + * @param s the detail message + */ + public MessagingException(String s) { + super(s); + initCause(null); // prevent anyone else from setting it + } + + /** + * Constructs a MessagingException with the specified + * Exception and detail message. The specified exception is chained + * to this exception. + * + * @param s the detail message + * @param e the embedded exception + * @see #getNextException + * @see #setNextException + * @see #getCause + */ + public MessagingException(String s, Exception e) { + super(s); + next = e; + initCause(null); // prevent anyone else from setting it + } + + /** + * Get the next exception chained to this one. If the + * next exception is a MessagingException, the chain + * may extend further. + * + * @return next Exception, null if none. + */ + public synchronized Exception getNextException() { + return next; + } + + /** + * Overrides the <code>getCause</code> method of <code>Throwable</code> + * to return the next exception in the chain of nested exceptions. + * + * @return next Exception, null if none. + */ + @Override + public synchronized Throwable getCause() { + return next; + } + + /** + * Add an exception to the end of the chain. If the end + * is <strong>not</strong> a MessagingException, this + * exception cannot be added to the end. + * + * @param ex the new end of the Exception chain + * @return <code>true</code> if this Exception + * was added, <code>false</code> otherwise. + */ + public synchronized boolean setNextException(Exception ex) { + Exception theEnd = this; + while (theEnd instanceof MessagingException && + ((MessagingException)theEnd).next != null) { + theEnd = ((MessagingException)theEnd).next; + } + // If the end is a MessagingException, we can add this + // exception to the chain. + if (theEnd instanceof MessagingException) { + ((MessagingException)theEnd).next = ex; + return true; + } else + return false; + } + + /** + * Override toString method to provide information on + * nested exceptions. + */ + @Override + public synchronized String toString() { + String s = super.toString(); + Exception n = next; + if (n == null) + return s; + StringBuilder sb = new StringBuilder(s == null ? "" : s); + while (n != null) { + sb.append(";\n nested exception is:\n\t"); + if (n instanceof MessagingException) { + MessagingException mex = (MessagingException)n; + sb.append(mex.superToString()); + n = mex.next; + } else { + sb.append(n.toString()); + n = null; + } + } + return sb.toString(); + } + + /** + * Return the "toString" information for this exception, + * without any information on nested exceptions. + */ + private final String superToString() { + return super.toString(); + } +}
diff --git a/mail/src/main/java/javax/mail/MethodNotSupportedException.java b/mail/src/main/java/javax/mail/MethodNotSupportedException.java new file mode 100644 index 0000000..bf7289f --- /dev/null +++ b/mail/src/main/java/javax/mail/MethodNotSupportedException.java
@@ -0,0 +1,60 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + + +/** + * The exception thrown when a method is not supported by the + * implementation + * + * @author John Mani + */ + +public class MethodNotSupportedException extends MessagingException { + + private static final long serialVersionUID = -3757386618726131322L; + + /** + * Constructs a MethodNotSupportedException with no detail message. + */ + public MethodNotSupportedException() { + super(); + } + + /** + * Constructs a MethodNotSupportedException with the specified + * detail message. + * + * @param s The detailed error message + */ + public MethodNotSupportedException(String s) { + super(s); + } + + /** + * Constructs a MethodNotSupportedException with the specified + * detail message and embedded exception. The exception is chained + * to this exception. + * + * @param s The detailed error message + * @param e The embedded exception + * @since JavaMail 1.5 + */ + public MethodNotSupportedException(String s, Exception e) { + super(s, e); + } +}
diff --git a/mail/src/main/java/javax/mail/Multipart.java b/mail/src/main/java/javax/mail/Multipart.java new file mode 100644 index 0000000..d1bcc27 --- /dev/null +++ b/mail/src/main/java/javax/mail/Multipart.java
@@ -0,0 +1,260 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +import java.util.Vector; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.IOException; +import javax.activation.DataSource; + +/** + * Multipart is a container that holds multiple body parts. Multipart + * provides methods to retrieve and set its subparts. <p> + * + * Multipart also acts as the base class for the content object returned + * by most Multipart DataContentHandlers. For example, invoking getContent() + * on a DataHandler whose source is a "multipart/signed" data source may + * return an appropriate subclass of Multipart. <p> + * + * Some messaging systems provide different subtypes of Multiparts. For + * example, MIME specifies a set of subtypes that include "alternative", + * "mixed", "related", "parallel", "signed", etc. <p> + * + * Multipart is an abstract class. Subclasses provide actual implementations. + * + * @author John Mani + */ + +public abstract class Multipart { + + /** + * Vector of BodyPart objects. + */ + protected Vector<BodyPart> parts = new Vector<>(); // Holds BodyParts + + /** + * This field specifies the content-type of this multipart + * object. It defaults to "multipart/mixed". + */ + protected String contentType = "multipart/mixed"; // Content-Type + + /** + * The <code>Part</code> containing this <code>Multipart</code>, + * if known. + * @since JavaMail 1.1 + */ + protected Part parent; + + /** + * Default constructor. An empty Multipart object is created. + */ + protected Multipart() { } + + /** + * Setup this Multipart object from the given MultipartDataSource. <p> + * + * The method adds the MultipartDataSource's BodyPart + * objects into this Multipart. This Multipart's contentType is + * set to that of the MultipartDataSource. <p> + * + * This method is typically used in those cases where one + * has a multipart data source that has already been pre-parsed into + * the individual body parts (for example, an IMAP datasource), but + * needs to create an appropriate Multipart subclass that represents + * a specific multipart subtype. + * + * @param mp Multipart datasource + * @exception MessagingException for failures + */ + protected synchronized void setMultipartDataSource(MultipartDataSource mp) + throws MessagingException { + contentType = mp.getContentType(); + + int count = mp.getCount(); + for (int i = 0; i < count; i++) + addBodyPart(mp.getBodyPart(i)); + } + + /** + * Return the content-type of this Multipart. <p> + * + * This implementation just returns the value of the + * <code>contentType</code> field. + * + * @return content-type + * @see #contentType + */ + public synchronized String getContentType() { + return contentType; + } + + /** + * Return the number of enclosed BodyPart objects. <p> + * + * @return number of parts + * @exception MessagingException for failures + * @see #parts + */ + public synchronized int getCount() throws MessagingException { + if (parts == null) + return 0; + + return parts.size(); + } + + /** + * Get the specified Part. Parts are numbered starting at 0. + * + * @param index the index of the desired Part + * @return the Part + * @exception IndexOutOfBoundsException if the given index + * is out of range. + * @exception MessagingException for other failures + */ + public synchronized BodyPart getBodyPart(int index) + throws MessagingException { + if (parts == null) + throw new IndexOutOfBoundsException("No such BodyPart"); + + return parts.elementAt(index); + } + + /** + * Remove the specified part from the multipart message. + * Shifts all the parts after the removed part down one. + * + * @param part The part to remove + * @return true if part removed, false otherwise + * @exception MessagingException if no such Part exists + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + */ + public synchronized boolean removeBodyPart(BodyPart part) + throws MessagingException { + if (parts == null) + throw new MessagingException("No such body part"); + + boolean ret = parts.removeElement(part); + part.setParent(null); + return ret; + } + + /** + * Remove the part at specified location (starting from 0). + * Shifts all the parts after the removed part down one. + * + * @param index Index of the part to remove + * @exception IndexOutOfBoundsException if the given index + * is out of range. + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception MessagingException for other failures + */ + public synchronized void removeBodyPart(int index) + throws MessagingException { + if (parts == null) + throw new IndexOutOfBoundsException("No such BodyPart"); + + BodyPart part = parts.elementAt(index); + parts.removeElementAt(index); + part.setParent(null); + } + + /** + * Adds a Part to the multipart. The BodyPart is appended to + * the list of existing Parts. + * + * @param part The Part to be appended + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception MessagingException for other failures + */ + public synchronized void addBodyPart(BodyPart part) + throws MessagingException { + if (parts == null) + parts = new Vector<>(); + + parts.addElement(part); + part.setParent(this); + } + + /** + * Adds a BodyPart at position <code>index</code>. + * If <code>index</code> is not the last one in the list, + * the subsequent parts are shifted up. If <code>index</code> + * is larger than the number of parts present, the + * BodyPart is appended to the end. + * + * @param part The BodyPart to be inserted + * @param index Location where to insert the part + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception MessagingException for other failures + */ + public synchronized void addBodyPart(BodyPart part, int index) + throws MessagingException { + if (parts == null) + parts = new Vector<>(); + + parts.insertElementAt(part, index); + part.setParent(this); + } + + /** + * Output an appropriately encoded bytestream to the given + * OutputStream. The implementation subclass decides the + * appropriate encoding algorithm to be used. The bytestream + * is typically used for sending. + * + * @param os the stream to write to + * @exception IOException if an IO related exception occurs + * @exception MessagingException for other failures + */ + public abstract void writeTo(OutputStream os) + throws IOException, MessagingException; + + /** + * Return the <code>Part</code> that contains this <code>Multipart</code> + * object, or <code>null</code> if not known. + * + * @return the parent Part + * @since JavaMail 1.1 + */ + public synchronized Part getParent() { + return parent; + } + + /** + * Set the parent of this <code>Multipart</code> to be the specified + * <code>Part</code>. Normally called by the <code>Message</code> + * or <code>BodyPart</code> <code>setContent(Multipart)</code> method. + * <code>parent</code> may be <code>null</code> if the + * <code>Multipart</code> is being removed from its containing + * <code>Part</code>. + * + * @param parent the parent Part + * @since JavaMail 1.1 + */ + public synchronized void setParent(Part parent) { + this.parent = parent; + } +}
diff --git a/mail/src/main/java/javax/mail/MultipartDataSource.java b/mail/src/main/java/javax/mail/MultipartDataSource.java new file mode 100644 index 0000000..0d68363 --- /dev/null +++ b/mail/src/main/java/javax/mail/MultipartDataSource.java
@@ -0,0 +1,58 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +import javax.activation.DataSource; + +/** + * MultipartDataSource is a <code>DataSource</code> that contains body + * parts. This allows "mail aware" <code>DataContentHandlers</code> to + * be implemented more efficiently by being aware of such + * <code>DataSources</code> and using the appropriate methods to access + * <code>BodyParts</code>. <p> + * + * Note that the data of a MultipartDataSource is also available as + * an input stream. <p> + * + * This interface will typically be implemented by providers that + * preparse multipart bodies, for example an IMAP provider. + * + * @author John Mani + * @see javax.activation.DataSource + */ + +public interface MultipartDataSource extends DataSource { + + /** + * Return the number of enclosed BodyPart objects. + * + * @return number of parts + */ + public int getCount(); + + /** + * Get the specified Part. Parts are numbered starting at 0. + * + * @param index the index of the desired Part + * @return the Part + * @exception IndexOutOfBoundsException if the given index + * is out of range. + * @exception MessagingException for other failures + */ + public BodyPart getBodyPart(int index) throws MessagingException; + +}
diff --git a/mail/src/main/java/javax/mail/NoSuchProviderException.java b/mail/src/main/java/javax/mail/NoSuchProviderException.java new file mode 100644 index 0000000..f94527d --- /dev/null +++ b/mail/src/main/java/javax/mail/NoSuchProviderException.java
@@ -0,0 +1,59 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +/** + * This exception is thrown when Session attempts to instantiate a + * Provider that doesn't exist. + * + * @author Max Spivak + */ + +public class NoSuchProviderException extends MessagingException { + + private static final long serialVersionUID = 8058319293154708827L; + + /** + * Constructs a NoSuchProviderException with no detail message. + */ + public NoSuchProviderException() { + super(); + } + + /** + * Constructs a NoSuchProviderException with the specified + * detail message. + * + * @param message The detailed error message + */ + public NoSuchProviderException(String message) { + super(message); + } + + /** + * Constructs a NoSuchProviderException with the specified + * detail message and embedded exception. The exception is chained + * to this exception. + * + * @param message The detailed error message + * @param e The embedded exception + * @since JavaMail 1.5 + */ + public NoSuchProviderException(String message, Exception e) { + super(message, e); + } +}
diff --git a/mail/src/main/java/javax/mail/Part.java b/mail/src/main/java/javax/mail/Part.java new file mode 100644 index 0000000..c8e5fe3 --- /dev/null +++ b/mail/src/main/java/javax/mail/Part.java
@@ -0,0 +1,451 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +import java.io.*; +import java.util.Enumeration; +import javax.activation.DataHandler; + +/** + * The <code>Part</code> interface is the common base interface for + * Messages and BodyParts. <p> + * + * Part consists of a set of attributes and a "Content".<p> + * + * <strong> Attributes: </strong> <p> + * + * The JavaMail API defines a set of standard Part attributes that are + * considered to be common to most existing Mail systems. These + * attributes have their own settor and gettor methods. Mail systems + * may support other Part attributes as well, these are represented as + * name-value pairs where both the name and value are Strings.<p> + * + * <strong> Content: </strong> <p> + * + * The <strong>data type</strong> of the "content" is returned by + * the <code>getContentType()</code> method. The MIME typing system + * is used to name data types. <p> + * + * The "content" of a Part is available in various formats: + * <ul> + * <li> As a DataHandler - using the <code>getDataHandler()</code> method. + * The "content" of a Part is also available through a + * <code>javax.activation.DataHandler</code> object. The DataHandler + * object allows clients to discover the operations available on the + * content, and to instantiate the appropriate component to perform + * those operations. + * + * <li> As an input stream - using the <code>getInputStream()</code> method. + * Any mail-specific encodings are decoded before this stream is returned. + * + * <li> As a Java object - using the <code>getContent()</code> method. + * This method returns the "content" as a Java object. + * The returned object is of course dependent on the content + * itself. In particular, a "multipart" Part's content is always a + * Multipart or subclass thereof. That is, <code>getContent()</code> on a + * "multipart" type Part will always return a Multipart (or subclass) object. + * </ul> + * + * Part provides the <code>writeTo()</code> method that streams + * out its bytestream in mail-safe form suitable for transmission. + * This bytestream is typically an aggregation of the Part attributes + * and its content's bytestream. <p> + * + * Message and BodyPart implement the Part interface. Note that in + * MIME parlance, Part models an Entity (RFC 2045, Section 2.4). + * + * @author John Mani + */ + +public interface Part { + + /** + * Return the size of the content of this part in bytes. + * Return -1 if the size cannot be determined. <p> + * + * Note that the size may not be an exact measure of the content + * size and may or may not account for any transfer encoding + * of the content. The size is appropriate for display in a + * user interface to give the user a rough idea of the size + * of this part. + * + * @return size of content in bytes + * @exception MessagingException for failures + */ + public int getSize() throws MessagingException; + + /** + * Return the number of lines in the content of this part. + * Return -1 if the number cannot be determined. + * + * Note that this number may not be an exact measure of the + * content length and may or may not account for any transfer + * encoding of the content. + * + * @return number of lines in the content. + * @exception MessagingException for failures + */ + public int getLineCount() throws MessagingException; + + /** + * Returns the Content-Type of the content of this part. + * Returns null if the Content-Type could not be determined. <p> + * + * The MIME typing system is used to name Content-types. + * + * @return The ContentType of this part + * @exception MessagingException for failures + * @see javax.activation.DataHandler + */ + public String getContentType() throws MessagingException; + + /** + * Is this Part of the specified MIME type? This method + * compares <strong>only the <code>primaryType</code> and + * <code>subType</code></strong>. + * The parameters of the content types are ignored. <p> + * + * For example, this method will return <code>true</code> when + * comparing a Part of content type <strong>"text/plain"</strong> + * with <strong>"text/plain; charset=foobar"</strong>. <p> + * + * If the <code>subType</code> of <code>mimeType</code> is the + * special character '*', then the subtype is ignored during the + * comparison. + * + * @param mimeType the MIME type to test + * @return true if this part is of the specified type + * @exception MessagingException for failures + */ + public boolean isMimeType(String mimeType) throws MessagingException; + + /** + * This part should be presented as an attachment. + * @see #getDisposition + * @see #setDisposition + */ + public static final String ATTACHMENT = "attachment"; + + /** + * This part should be presented inline. + * @see #getDisposition + * @see #setDisposition + */ + public static final String INLINE = "inline"; + + /** + * Return the disposition of this part. The disposition + * describes how the part should be presented to the user. + * (See RFC 2183.) The return value should be considered + * without regard to case. For example: + * <blockquote><pre> + * String disp = part.getDisposition(); + * if (disp == null || disp.equalsIgnoreCase(Part.ATTACHMENT)) + * // treat as attachment if not first part + * </pre></blockquote> + * + * @return disposition of this part, or null if unknown + * @exception MessagingException for failures + * @see #ATTACHMENT + * @see #INLINE + * @see #getFileName + */ + public String getDisposition() throws MessagingException; + + /** + * Set the disposition of this part. + * + * @param disposition disposition of this part + * @exception IllegalWriteException if the underlying implementation + * does not support modification of this header + * @exception IllegalStateException if this Part is obtained + * from a READ_ONLY folder + * @exception MessagingException for other failures + * @see #ATTACHMENT + * @see #INLINE + * @see #setFileName + */ + public void setDisposition(String disposition) throws MessagingException; + + /** + * Return a description String for this part. This typically + * associates some descriptive information with this part. + * Returns null if none is available. + * + * @return description of this part + * @exception MessagingException for failures + */ + public String getDescription() throws MessagingException; + + /** + * Set a description String for this part. This typically + * associates some descriptive information with this part. + * + * @param description description of this part + * @exception IllegalWriteException if the underlying implementation + * does not support modification of this header + * @exception IllegalStateException if this Part is obtained + * from a READ_ONLY folder + * @exception MessagingException for other failures + */ + public void setDescription(String description) throws MessagingException; + + /** + * Get the filename associated with this part, if possible. + * Useful if this part represents an "attachment" that was + * loaded from a file. The filename will usually be a simple + * name, not including directory components. + * + * @return Filename to associate with this part + * @exception MessagingException for failures + */ + public String getFileName() throws MessagingException; + + /** + * Set the filename associated with this part, if possible. + * Useful if this part represents an "attachment" that was + * loaded from a file. The filename will usually be a simple + * name, not including directory components. + * + * @param filename Filename to associate with this part + * @exception IllegalWriteException if the underlying implementation + * does not support modification of this header + * @exception IllegalStateException if this Part is obtained + * from a READ_ONLY folder + * @exception MessagingException for other failures + */ + public void setFileName(String filename) throws MessagingException; + + /** + * Return an input stream for this part's "content". Any + * mail-specific transfer encodings will be decoded before the + * input stream is provided. <p> + * + * This is typically a convenience method that just invokes + * the DataHandler's <code>getInputStream()</code> method. + * + * @return an InputStream + * @exception IOException this is typically thrown by the + * DataHandler. Refer to the documentation for + * javax.activation.DataHandler for more details. + * @exception MessagingException for other failures + * @see #getDataHandler + * @see javax.activation.DataHandler#getInputStream + */ + public InputStream getInputStream() + throws IOException, MessagingException; + + /** + * Return a DataHandler for the content within this part. The + * DataHandler allows clients to operate on as well as retrieve + * the content. + * + * @return DataHandler for the content + * @exception MessagingException for failures + */ + public DataHandler getDataHandler() throws MessagingException; + + /** + * Return the content as a Java object. The type of the returned + * object is of course dependent on the content itself. For example, + * the object returned for "text/plain" content is usually a String + * object. The object returned for a "multipart" content is always a + * Multipart subclass. For content-types that are unknown to the + * DataHandler system, an input stream is returned as the content <p> + * + * This is a convenience method that just invokes the DataHandler's + * getContent() method + * + * @return Object + * @exception IOException this is typically thrown by the + * DataHandler. Refer to the documentation for + * javax.activation.DataHandler for more details. + * @exception MessagingException for other failures + * + * @see javax.activation.DataHandler#getContent + */ + public Object getContent() throws IOException, MessagingException; + + /** + * This method provides the mechanism to set this part's content. + * The DataHandler wraps around the actual content. + * + * @param dh The DataHandler for the content. + * @exception IllegalWriteException if the underlying implementation + * does not support modification of existing values + * @exception IllegalStateException if this Part is obtained + * from a READ_ONLY folder + * @exception MessagingException for other failures + */ + public void setDataHandler(DataHandler dh) throws MessagingException; + + /** + * A convenience method for setting this part's content. The part + * internally wraps the content in a DataHandler. <p> + * + * Note that a DataContentHandler class for the specified type should + * be available to the JavaMail implementation for this to work right. + * i.e., to do <code>setContent(foobar, "application/x-foobar")</code>, + * a DataContentHandler for "application/x-foobar" should be installed. + * Refer to the Java Activation Framework for more information. + * + * @param obj A java object. + * @param type MIME type of this object. + * @exception IllegalWriteException if the underlying implementation + * does not support modification of existing values + * @exception IllegalStateException if this Part is obtained + * from a READ_ONLY folder + * @exception MessagingException for other failures + */ + public void setContent(Object obj, String type) + throws MessagingException; + + /** + * A convenience method that sets the given String as this + * part's content with a MIME type of "text/plain". + * + * @param text The text that is the Message's content. + * @exception IllegalWriteException if the underlying + * implementation does not support modification of + * existing values + * @exception IllegalStateException if this Part is obtained + * from a READ_ONLY folder + * @exception MessagingException for other failures + */ + public void setText(String text) throws MessagingException; + + /** + * This method sets the given Multipart object as this message's + * content. + * + * @param mp The multipart object that is the Message's content + * @exception IllegalWriteException if the underlying + * implementation does not support modification of + * existing values + * @exception IllegalStateException if this Part is obtained + * from a READ_ONLY folder + * @exception MessagingException for other failures + */ + public void setContent(Multipart mp) throws MessagingException; + + /** + * Output a bytestream for this Part. This bytestream is + * typically an aggregration of the Part attributes and + * an appropriately encoded bytestream from its 'content'. <p> + * + * Classes that implement the Part interface decide on + * the appropriate encoding algorithm to be used. <p> + * + * The bytestream is typically used for sending. + * + * @param os the stream to write to + * @exception IOException if an error occurs writing to the + * stream or if an error is generated + * by the javax.activation layer. + * @exception MessagingException if an error occurs fetching the + * data to be written + * + * @see javax.activation.DataHandler#writeTo + */ + public void writeTo(OutputStream os) throws IOException, MessagingException; + + /** + * Get all the headers for this header name. Returns <code>null</code> + * if no headers for this header name are available. + * + * @param header_name the name of this header + * @return the value fields for all headers with + * this name + * @exception MessagingException for failures + */ + public String[] getHeader(String header_name) + throws MessagingException; + + /** + * Set the value for this header_name. Replaces all existing + * header values with this new value. + * + * @param header_name the name of this header + * @param header_value the value for this header + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception IllegalStateException if this Part is + * obtained from a READ_ONLY folder + * @exception MessagingException for other failures + */ + public void setHeader(String header_name, String header_value) + throws MessagingException; + /** + * Add this value to the existing values for this header_name. + * + * @param header_name the name of this header + * @param header_value the value for this header + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception IllegalStateException if this Part is + * obtained from a READ_ONLY folder + * @exception MessagingException for other failures + */ + public void addHeader(String header_name, String header_value) + throws MessagingException; + /** + * Remove all headers with this name. + * + * @param header_name the name of this header + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception IllegalStateException if this Part is + * obtained from a READ_ONLY folder + * @exception MessagingException for other failures + */ + public void removeHeader(String header_name) + throws MessagingException; + + /** + * Return all the headers from this part as an Enumeration of + * Header objects. + * + * @return enumeration of Header objects + * @exception MessagingException for failures + */ + public Enumeration<Header> getAllHeaders() throws MessagingException; + + /** + * Return matching headers from this part as an Enumeration of + * Header objects. + * + * @param header_names the headers to match + * @return enumeration of Header objects + * @exception MessagingException for failures + */ + public Enumeration<Header> getMatchingHeaders(String[] header_names) + throws MessagingException; + + /** + * Return non-matching headers from this envelope as an Enumeration + * of Header objects. + * + * @param header_names the headers to not match + * @return enumeration of Header objects + * @exception MessagingException for failures + */ + public Enumeration<Header> getNonMatchingHeaders(String[] header_names) + throws MessagingException; +}
diff --git a/mail/src/main/java/javax/mail/PasswordAuthentication.java b/mail/src/main/java/javax/mail/PasswordAuthentication.java new file mode 100644 index 0000000..8654ee0 --- /dev/null +++ b/mail/src/main/java/javax/mail/PasswordAuthentication.java
@@ -0,0 +1,59 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + + +/** + * The class PasswordAuthentication is a data holder that is used by + * Authenticator. It is simply a repository for a user name and a password. + * + * @see java.net.PasswordAuthentication + * @see javax.mail.Authenticator + * @see javax.mail.Authenticator#getPasswordAuthentication() + * + * @author Bill Foote + */ + +public final class PasswordAuthentication { + + private final String userName; + private final String password; + + /** + * Initialize a new PasswordAuthentication + * @param userName the user name + * @param password The user's password + */ + public PasswordAuthentication(String userName, String password) { + this.userName = userName; + this.password = password; + } + + /** + * @return the user name + */ + public String getUserName() { + return userName; + } + + /** + * @return the password + */ + public String getPassword() { + return password; + } +}
diff --git a/mail/src/main/java/javax/mail/Provider.java b/mail/src/main/java/javax/mail/Provider.java new file mode 100644 index 0000000..6889875 --- /dev/null +++ b/mail/src/main/java/javax/mail/Provider.java
@@ -0,0 +1,138 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +/** + * The Provider is a class that describes a protocol + * implementation. The values typically come from the + * javamail.providers and javamail.default.providers + * resource files. An application may also create and + * register a Provider object to dynamically add support + * for a new provider. + * + * @author Max Spivak + * @author Bill Shannon + */ +public class Provider { + + /** + * This inner class defines the Provider type. + * Currently, STORE and TRANSPORT are the only two provider types + * supported. + */ + + public static class Type { + public static final Type STORE = new Type("STORE"); + public static final Type TRANSPORT = new Type("TRANSPORT"); + + private String type; + + private Type(String type) { + this.type = type; + } + + @Override + public String toString() { + return type; + } + } + + private Type type; + private String protocol, className, vendor, version; + + /** + * Create a new provider of the specified type for the specified + * protocol. The specified class implements the provider. + * + * @param type Type.STORE or Type.TRANSPORT + * @param protocol valid protocol for the type + * @param classname class name that implements this protocol + * @param vendor optional string identifying the vendor (may be null) + * @param version optional implementation version string (may be null) + * @since JavaMail 1.4 + */ + public Provider(Type type, String protocol, String classname, + String vendor, String version) { + this.type = type; + this.protocol = protocol; + this.className = classname; + this.vendor = vendor; + this.version = version; + } + + /** + * Returns the type of this Provider. + * + * @return the provider type + */ + public Type getType() { + return type; + } + + /** + * Returns the protocol supported by this Provider. + * + * @return the protocol + */ + public String getProtocol() { + return protocol; + } + + /** + * Returns the name of the class that implements the protocol. + * + * @return the class name + */ + public String getClassName() { + return className; + } + + /** + * Returns the name of the vendor associated with this implementation + * or null. + * + * @return the vendor + */ + public String getVendor() { + return vendor; + } + + /** + * Returns the version of this implementation or null if no version. + * + * @return the version + */ + public String getVersion() { + return version; + } + + /** Overrides Object.toString() */ + @Override + public String toString() { + String s = "javax.mail.Provider[" + type + "," + + protocol + "," + className; + + if (vendor != null) + s += "," + vendor; + + if (version != null) + s += "," + version; + + s += "]"; + return s; + } +}
diff --git a/mail/src/main/java/javax/mail/Quota.java b/mail/src/main/java/javax/mail/Quota.java new file mode 100644 index 0000000..13da8b9 --- /dev/null +++ b/mail/src/main/java/javax/mail/Quota.java
@@ -0,0 +1,103 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +/** + * This class represents a set of quotas for a given quota root. + * Each quota root has a set of resources, represented by the + * <code>Quota.Resource</code> class. Each resource has a name + * (for example, "STORAGE"), a current usage, and a usage limit. + * See RFC 2087. + * + * @since JavaMail 1.4 + * @author Bill Shannon + */ + +public class Quota { + + /** + * An individual resource in a quota root. + * + * @since JavaMail 1.4 + */ + public static class Resource { + /** The name of the resource. */ + public String name; + /** The current usage of the resource. */ + public long usage; + /** The usage limit for the resource. */ + public long limit; + + /** + * Construct a Resource object with the given name, + * usage, and limit. + * + * @param name the resource name + * @param usage the current usage of the resource + * @param limit the usage limit for the resource + */ + public Resource(String name, long usage, long limit) { + this.name = name; + this.usage = usage; + this.limit = limit; + } + } + + /** + * The name of the quota root. + */ + public String quotaRoot; + + /** + * The set of resources associated with this quota root. + */ + public Quota.Resource[] resources; + + /** + * Create a Quota object for the named quotaroot with no associated + * resources. + * + * @param quotaRoot the name of the quota root + */ + public Quota(String quotaRoot) { + this.quotaRoot = quotaRoot; + } + + /** + * Set a resource limit for this quota root. + * + * @param name the name of the resource + * @param limit the resource limit + */ + public void setResourceLimit(String name, long limit) { + if (resources == null) { + resources = new Quota.Resource[1]; + resources[0] = new Quota.Resource(name, 0, limit); + return; + } + for (int i = 0; i < resources.length; i++) { + if (resources[i].name.equalsIgnoreCase(name)) { + resources[i].limit = limit; + return; + } + } + Quota.Resource[] ra = new Quota.Resource[resources.length + 1]; + System.arraycopy(resources, 0, ra, 0, resources.length); + ra[ra.length - 1] = new Quota.Resource(name, 0, limit); + resources = ra; + } +}
diff --git a/mail/src/main/java/javax/mail/QuotaAwareStore.java b/mail/src/main/java/javax/mail/QuotaAwareStore.java new file mode 100644 index 0000000..a6f566d --- /dev/null +++ b/mail/src/main/java/javax/mail/QuotaAwareStore.java
@@ -0,0 +1,57 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +/** + * An interface implemented by Stores that support quotas. + * The {@link #getQuota getQuota} and {@link #setQuota setQuota} methods + * support the quota model defined by the IMAP QUOTA extension. + * Refer to <A HREF="http://www.ietf.org/rfc/rfc2087.txt">RFC 2087</A> + * for more information. <p> + * + * @since JavaMail 1.4 + */ +public interface QuotaAwareStore { + /** + * Get the quotas for the named folder. + * Quotas are controlled on the basis of a quota root, not + * (necessarily) a folder. The relationship between folders + * and quota roots depends on the server. Some servers + * might implement a single quota root for all folders owned by + * a user. Other servers might implement a separate quota root + * for each folder. A single folder can even have multiple + * quota roots, perhaps controlling quotas for different + * resources. + * + * @param folder the name of the folder + * @return array of Quota objects + * @exception MessagingException if the server doesn't support the + * QUOTA extension + */ + Quota[] getQuota(String folder) throws MessagingException; + + /** + * Set the quotas for the quota root specified in the quota argument. + * Typically this will be one of the quota roots obtained from the + * <code>getQuota</code> method, but it need not be. + * + * @param quota the quota to set + * @exception MessagingException if the server doesn't support the + * QUOTA extension + */ + void setQuota(Quota quota) throws MessagingException; +}
diff --git a/mail/src/main/java/javax/mail/ReadOnlyFolderException.java b/mail/src/main/java/javax/mail/ReadOnlyFolderException.java new file mode 100644 index 0000000..f658b5c --- /dev/null +++ b/mail/src/main/java/javax/mail/ReadOnlyFolderException.java
@@ -0,0 +1,82 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +/** + * This exception is thrown when an attempt is made to open a folder + * read-write access when the folder is marked read-only. <p> + * + * The getMessage() method returns more detailed information about the + * error that caused this exception. <p> + * + * @author Jim Glennon + */ + +public class ReadOnlyFolderException extends MessagingException { + transient private Folder folder; + + private static final long serialVersionUID = 5711829372799039325L; + + /** + * Constructs a ReadOnlyFolderException with the specified + * folder and no detail message. + * + * @param folder the Folder + * @since JavaMail 1.2 + */ + public ReadOnlyFolderException(Folder folder) { + this(folder, null); + } + + /** + * Constructs a ReadOnlyFolderException with the specified + * detail message. + * + * @param folder The Folder + * @param message The detailed error message + * @since JavaMail 1.2 + */ + public ReadOnlyFolderException(Folder folder, String message) { + super(message); + this.folder = folder; + } + + /** + * Constructs a ReadOnlyFolderException with the specified + * detail message and embedded exception. The exception is chained + * to this exception. + * + * @param folder The Folder + * @param message The detailed error message + * @param e The embedded exception + * @since JavaMail 1.5 + */ + public ReadOnlyFolderException(Folder folder, String message, Exception e) { + super(message, e); + this.folder = folder; + } + + /** + * Returns the Folder object. + * + * @return the Folder + * @since JavaMail 1.2 + */ + public Folder getFolder() { + return folder; + } +}
diff --git a/mail/src/main/java/javax/mail/SendFailedException.java b/mail/src/main/java/javax/mail/SendFailedException.java new file mode 100644 index 0000000..ed73480 --- /dev/null +++ b/mail/src/main/java/javax/mail/SendFailedException.java
@@ -0,0 +1,116 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +/** + * This exception is thrown when the message cannot be sent.<p> + * + * The exception includes those addresses to which the message could not be + * sent as well as the valid addresses to which the message was sent and + * valid addresses to which the message was not sent. + * + * @see javax.mail.Transport#send + * @see javax.mail.Transport#sendMessage + * @see javax.mail.event.TransportEvent + * + * @author John Mani + * @author Max Spivak + */ + +public class SendFailedException extends MessagingException { + transient protected Address[] invalid; + transient protected Address[] validSent; + transient protected Address[] validUnsent; + + private static final long serialVersionUID = -6457531621682372913L; + + /** + * Constructs a SendFailedException with no detail message. + */ + public SendFailedException() { + super(); + } + + /** + * Constructs a SendFailedException with the specified detail message. + * @param s the detail message + */ + public SendFailedException(String s) { + super(s); + } + + /** + * Constructs a SendFailedException with the specified + * Exception and detail message. The specified exception is chained + * to this exception. + * @param s the detail message + * @param e the embedded exception + * @see #getNextException + * @see #setNextException + */ + public SendFailedException(String s, Exception e) { + super(s, e); + } + + + /** + * Constructs a SendFailedException with the specified string + * and the specified address objects. + * + * @param msg the detail message + * @param ex the embedded exception + * @param validSent valid addresses to which message was sent + * @param validUnsent valid addresses to which message was not sent + * @param invalid the invalid addresses + * @see #getNextException + * @see #setNextException + */ + public SendFailedException(String msg, Exception ex, Address[] validSent, + Address[] validUnsent, Address[] invalid) { + super(msg, ex); + this.validSent = validSent; + this.validUnsent = validUnsent; + this.invalid = invalid; + } + + /** + * Return the addresses to which this message was sent succesfully. + * @return Addresses to which the message was sent successfully or null + */ + public Address[] getValidSentAddresses() { + return validSent; + } + + /** + * Return the addresses that are valid but to which this message + * was not sent. + * @return Addresses that are valid but to which the message was + * not sent successfully or null + */ + public Address[] getValidUnsentAddresses() { + return validUnsent; + } + + /** + * Return the addresses to which this message could not be sent. + * + * @return Addresses to which the message sending failed or null; + */ + public Address[] getInvalidAddresses() { + return invalid; + } +}
diff --git a/mail/src/main/java/javax/mail/Service.java b/mail/src/main/java/javax/mail/Service.java new file mode 100644 index 0000000..5c9e20c --- /dev/null +++ b/mail/src/main/java/javax/mail/Service.java
@@ -0,0 +1,657 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +import java.io.*; +import java.net.*; +import java.util.*; +import java.util.concurrent.Executor; +import javax.mail.event.*; + +/** + * An abstract class that contains the functionality + * common to messaging services, such as stores and transports. <p> + * A messaging service is created from a <code>Session</code> and is + * named using a <code>URLName</code>. A service must be connected + * before it can be used. Connection events are sent to reflect + * its connection status. + * + * @author Christopher Cotton + * @author Bill Shannon + * @author Kanwar Oberoi + */ + +public abstract class Service implements AutoCloseable { + + /** + * The session from which this service was created. + */ + protected Session session; + + /** + * The <code>URLName</code> of this service. + */ + protected volatile URLName url = null; + + /** + * Debug flag for this service. Set from the session's debug + * flag when this service is created. + */ + protected boolean debug = false; + + private boolean connected = false; + + /* + * connectionListeners is a Vector, initialized here, + * because we depend on it always existing and depend + * on the synchronization that Vector provides. + * (Sychronizing on the Service object itself can cause + * deadlocks when notifying listeners.) + */ + private final Vector<ConnectionListener> connectionListeners + = new Vector<>(); + + /** + * The queue of events to be delivered. + */ + private final EventQueue q; + + /** + * Constructor. + * + * @param session Session object for this service + * @param urlname URLName object to be used for this service + */ + protected Service(Session session, URLName urlname) { + this.session = session; + debug = session.getDebug(); + url = urlname; + + /* + * Initialize the URLName with default values. + * The URLName will be updated when connect is called. + */ + String protocol = null; + String host = null; + int port = -1; + String user = null; + String password = null; + String file = null; + + // get whatever information we can from the URL + // XXX - url should always be non-null here, Session + // passes it into the constructor + if (url != null) { + protocol = url.getProtocol(); + host = url.getHost(); + port = url.getPort(); + user = url.getUsername(); + password = url.getPassword(); + file = url.getFile(); + } + + // try to get protocol-specific default properties + if (protocol != null) { + if (host == null) + host = session.getProperty("mail." + protocol + ".host"); + if (user == null) + user = session.getProperty("mail." + protocol + ".user"); + } + + // try to get mail-wide default properties + if (host == null) + host = session.getProperty("mail.host"); + + if (user == null) + user = session.getProperty("mail.user"); + + // try using the system username + if (user == null) { + try { + user = System.getProperty("user.name"); + } catch (SecurityException sex) { + // XXX - it's not worth creating a MailLogger just for this + //logger.log(Level.CONFIG, "Can't get user.name property", sex); + } + } + + url = new URLName(protocol, host, port, file, user, password); + + // create or choose the appropriate event queue + String scope = + session.getProperties().getProperty("mail.event.scope", "folder"); + Executor executor = + (Executor)session.getProperties().get("mail.event.executor"); + if (scope.equalsIgnoreCase("application")) + q = EventQueue.getApplicationEventQueue(executor); + else if (scope.equalsIgnoreCase("session")) + q = session.getEventQueue(); + else // if (scope.equalsIgnoreCase("store") || + // scope.equalsIgnoreCase("folder")) + q = new EventQueue(executor); + } + + /** + * A generic connect method that takes no parameters. Subclasses + * can implement the appropriate authentication schemes. Subclasses + * that need additional information might want to use some properties + * or might get it interactively using a popup window. <p> + * + * If the connection is successful, an "open" <code>ConnectionEvent</code> + * is delivered to any <code>ConnectionListeners</code> on this service. <p> + * + * Most clients should just call this method to connect to the service.<p> + * + * It is an error to connect to an already connected service. <p> + * + * The implementation provided here simply calls the following + * <code>connect(String, String, String)</code> method with nulls. + * + * @exception AuthenticationFailedException for authentication failures + * @exception MessagingException for other failures + * @exception IllegalStateException if the service is already connected + * + * @see javax.mail.event.ConnectionEvent + */ + public void connect() throws MessagingException { + connect(null, null, null); + } + + /** + * Connect to the specified address. This method provides a simple + * authentication scheme that requires a username and password. <p> + * + * If the connection is successful, an "open" <code>ConnectionEvent</code> + * is delivered to any <code>ConnectionListeners</code> on this service. <p> + * + * It is an error to connect to an already connected service. <p> + * + * The implementation in the Service class will collect defaults + * for the host, user, and password from the session, from the + * <code>URLName</code> for this service, and from the supplied + * parameters and then call the <code>protocolConnect</code> method. + * If the <code>protocolConnect</code> method returns <code>false</code>, + * the user will be prompted for any missing information and the + * <code>protocolConnect</code> method will be called again. The + * subclass should override the <code>protocolConnect</code> method. + * The subclass should also implement the <code>getURLName</code> + * method, or use the implementation in this class. <p> + * + * On a successful connection, the <code>setURLName</code> method is + * called with a URLName that includes the information used to make + * the connection, including the password. <p> + * + * If the username passed in is null, a default value will be chosen + * as described above. + * + * If the password passed in is null and this is the first successful + * connection to this service, the user name and the password + * collected from the user will be saved as defaults for subsequent + * connection attempts to this same service when using other Service object + * instances (the connection information is typically always saved within + * a particular Service object instance). The password is saved using the + * Session method <code>setPasswordAuthentication</code>. If the + * password passed in is not null, it is not saved, on the assumption + * that the application is managing passwords explicitly. + * + * @param host the host to connect to + * @param user the user name + * @param password this user's password + * @exception AuthenticationFailedException for authentication failures + * @exception MessagingException for other failures + * @exception IllegalStateException if the service is already connected + * @see javax.mail.event.ConnectionEvent + * @see javax.mail.Session#setPasswordAuthentication + */ + public void connect(String host, String user, String password) + throws MessagingException { + connect(host, -1, user, password); + } + + /** + * Connect to the current host using the specified username + * and password. This method is equivalent to calling the + * <code>connect(host, user, password)</code> method with null + * for the host name. + * + * @param user the user name + * @param password this user's password + * @exception AuthenticationFailedException for authentication failures + * @exception MessagingException for other failures + * @exception IllegalStateException if the service is already connected + * @see javax.mail.event.ConnectionEvent + * @see javax.mail.Session#setPasswordAuthentication + * @see #connect(java.lang.String, java.lang.String, java.lang.String) + * @since JavaMail 1.4 + */ + public void connect(String user, String password) + throws MessagingException { + connect(null, user, password); + } + + /** + * Similar to connect(host, user, password) except a specific port + * can be specified. + * + * @param host the host to connect to + * @param port the port to connect to (-1 means the default port) + * @param user the user name + * @param password this user's password + * @exception AuthenticationFailedException for authentication failures + * @exception MessagingException for other failures + * @exception IllegalStateException if the service is already connected + * @see #connect(java.lang.String, java.lang.String, java.lang.String) + * @see javax.mail.event.ConnectionEvent + */ + public synchronized void connect(String host, int port, + String user, String password) throws MessagingException { + + // see if the service is already connected + if (isConnected()) + throw new IllegalStateException("already connected"); + + PasswordAuthentication pw; + boolean connected = false; + boolean save = false; + String protocol = null; + String file = null; + + // get whatever information we can from the URL + // XXX - url should always be non-null here, Session + // passes it into the constructor + if (url != null) { + protocol = url.getProtocol(); + if (host == null) + host = url.getHost(); + if (port == -1) + port = url.getPort(); + + if (user == null) { + user = url.getUsername(); + if (password == null) // get password too if we need it + password = url.getPassword(); + } else { + if (password == null && user.equals(url.getUsername())) + // only get the password if it matches the username + password = url.getPassword(); + } + + file = url.getFile(); + } + + // try to get protocol-specific default properties + if (protocol != null) { + if (host == null) + host = session.getProperty("mail." + protocol + ".host"); + if (user == null) + user = session.getProperty("mail." + protocol + ".user"); + } + + // try to get mail-wide default properties + if (host == null) + host = session.getProperty("mail.host"); + + if (user == null) + user = session.getProperty("mail.user"); + + // try using the system username + if (user == null) { + try { + user = System.getProperty("user.name"); + } catch (SecurityException sex) { + // XXX - it's not worth creating a MailLogger just for this + //logger.log(Level.CONFIG, "Can't get user.name property", sex); + } + } + + // if we don't have a password, look for saved authentication info + if (password == null && url != null) { + // canonicalize the URLName + setURLName(new URLName(protocol, host, port, file, user, null)); + pw = session.getPasswordAuthentication(getURLName()); + if (pw != null) { + if (user == null) { + user = pw.getUserName(); + password = pw.getPassword(); + } else if (user.equals(pw.getUserName())) { + password = pw.getPassword(); + } + } else + save = true; + } + + // try connecting, if the protocol needs some missing + // information (user, password) it will not connect. + // if it tries to connect and fails, remember why for later. + AuthenticationFailedException authEx = null; + try { + connected = protocolConnect(host, port, user, password); + } catch (AuthenticationFailedException ex) { + authEx = ex; + } + + // if not connected, ask the user and try again + if (!connected) { + InetAddress addr; + try { + addr = InetAddress.getByName(host); + } catch (UnknownHostException e) { + addr = null; + } + pw = session.requestPasswordAuthentication( + addr, port, + protocol, + null, user); + if (pw != null) { + user = pw.getUserName(); + password = pw.getPassword(); + + // have the service connect again + connected = protocolConnect(host, port, user, password); + } + } + + // if we're not connected by now, we give up + if (!connected) { + if (authEx != null) + throw authEx; + else if (user == null) + throw new AuthenticationFailedException( + "failed to connect, no user name specified?"); + else if (password == null) + throw new AuthenticationFailedException( + "failed to connect, no password specified?"); + else + throw new AuthenticationFailedException("failed to connect"); + } + + setURLName(new URLName(protocol, host, port, file, user, password)); + + if (save) + session.setPasswordAuthentication(getURLName(), + new PasswordAuthentication(user, password)); + + // set our connected state + setConnected(true); + + // finally, deliver the connection event + notifyConnectionListeners(ConnectionEvent.OPENED); + } + + + /** + * The service implementation should override this method to + * perform the actual protocol-specific connection attempt. + * The default implementation of the <code>connect</code> method + * calls this method as needed. <p> + * + * The <code>protocolConnect</code> method should return + * <code>false</code> if a user name or password is required + * for authentication but the corresponding parameter is null; + * the <code>connect</code> method will prompt the user when + * needed to supply missing information. This method may + * also return <code>false</code> if authentication fails for + * the supplied user name or password. Alternatively, this method + * may throw an AuthenticationFailedException when authentication + * fails. This exception may include a String message with more + * detail about the failure. <p> + * + * The <code>protocolConnect</code> method should throw an + * exception to report failures not related to authentication, + * such as an invalid host name or port number, loss of a + * connection during the authentication process, unavailability + * of the server, etc. + * + * @param host the name of the host to connect to + * @param port the port to use (-1 means use default port) + * @param user the name of the user to login as + * @param password the user's password + * @return true if connection successful, false if authentication failed + * @exception AuthenticationFailedException for authentication failures + * @exception MessagingException for non-authentication failures + */ + protected boolean protocolConnect(String host, int port, String user, + String password) throws MessagingException { + return false; + } + + /** + * Is this service currently connected? <p> + * + * This implementation uses a private boolean field to + * store the connection state. This method returns the value + * of that field. <p> + * + * Subclasses may want to override this method to verify that any + * connection to the message store is still alive. + * + * @return true if the service is connected, false if it is not connected + */ + public synchronized boolean isConnected() { + return connected; + } + + /** + * Set the connection state of this service. The connection state + * will automatically be set by the service implementation during the + * <code>connect</code> and <code>close</code> methods. + * Subclasses will need to call this method to set the state + * if the service was automatically disconnected. <p> + * + * The implementation in this class merely sets the private field + * returned by the <code>isConnected</code> method. + * + * @param connected true if the service is connected, + * false if it is not connected + */ + protected synchronized void setConnected(boolean connected) { + this.connected = connected; + } + + /** + * Close this service and terminate its connection. A close + * ConnectionEvent is delivered to any ConnectionListeners. Any + * Messaging components (Folders, Messages, etc.) belonging to this + * service are invalid after this service is closed. Note that the service + * is closed even if this method terminates abnormally by throwing + * a MessagingException. <p> + * + * This implementation uses <code>setConnected(false)</code> to set + * this service's connected state to <code>false</code>. It will then + * send a close ConnectionEvent to any registered ConnectionListeners. + * Subclasses overriding this method to do implementation specific + * cleanup should call this method as a last step to insure event + * notification, probably by including a call to <code>super.close()</code> + * in a <code>finally</code> clause. + * + * @see javax.mail.event.ConnectionEvent + * @throws MessagingException for errors while closing + */ + public synchronized void close() throws MessagingException { + setConnected(false); + notifyConnectionListeners(ConnectionEvent.CLOSED); + } + + /** + * Return a URLName representing this service. The returned URLName + * does <em>not</em> include the password field. <p> + * + * Subclasses should only override this method if their + * URLName does not follow the standard format. <p> + * + * The implementation in the Service class returns (usually a copy of) + * the <code>url</code> field with the password and file information + * stripped out. + * + * @return the URLName representing this service + * @see URLName + */ + public URLName getURLName() { + URLName url = this.url; // snapshot + if (url != null && (url.getPassword() != null || url.getFile() != null)) + return new URLName(url.getProtocol(), url.getHost(), + url.getPort(), null /* no file */, + url.getUsername(), null /* no password */); + else + return url; + } + + /** + * Set the URLName representing this service. + * Normally used to update the <code>url</code> field + * after a service has successfully connected. <p> + * + * Subclasses should only override this method if their + * URL does not follow the standard format. In particular, + * subclasses should override this method if their URL + * does not require all the possible fields supported by + * <code>URLName</code>; a new <code>URLName</code> should + * be constructed with any unneeded fields removed. <p> + * + * The implementation in the Service class simply sets the + * <code>url</code> field. + * + * @param url the URLName + * @see URLName + */ + protected void setURLName(URLName url) { + this.url = url; + } + + /** + * Add a listener for Connection events on this service. <p> + * + * The default implementation provided here adds this listener + * to an internal list of ConnectionListeners. + * + * @param l the Listener for Connection events + * @see javax.mail.event.ConnectionEvent + */ + public void addConnectionListener(ConnectionListener l) { + connectionListeners.addElement(l); + } + + /** + * Remove a Connection event listener. <p> + * + * The default implementation provided here removes this listener + * from the internal list of ConnectionListeners. + * + * @param l the listener + * @see #addConnectionListener + */ + public void removeConnectionListener(ConnectionListener l) { + connectionListeners.removeElement(l); + } + + /** + * Notify all ConnectionListeners. Service implementations are + * expected to use this method to broadcast connection events. <p> + * + * The provided default implementation queues the event into + * an internal event queue. An event dispatcher thread dequeues + * events from the queue and dispatches them to the registered + * ConnectionListeners. Note that the event dispatching occurs + * in a separate thread, thus avoiding potential deadlock problems. + * + * @param type the ConnectionEvent type + */ + protected void notifyConnectionListeners(int type) { + /* + * Don't bother queuing an event if there's no listeners. + * Yes, listeners could be removed after checking, which + * just makes this an expensive no-op. + */ + if (connectionListeners.size() > 0) { + ConnectionEvent e = new ConnectionEvent(this, type); + queueEvent(e, connectionListeners); + } + + /* Fix for broken JDK1.1.x Garbage collector : + * The 'conservative' GC in JDK1.1.x occasionally fails to + * garbage-collect Threads which are in the wait state. + * This would result in thread (and consequently memory) leaks. + * + * We attempt to fix this by sending a 'terminator' event + * to the queue, after we've sent the CLOSED event. The + * terminator event causes the event-dispatching thread to + * self destruct. + */ + if (type == ConnectionEvent.CLOSED) + q.terminateQueue(); + } + + /** + * Return <code>getURLName.toString()</code> if this service has a URLName, + * otherwise it will return the default <code>toString</code>. + */ + @Override + public String toString() { + URLName url = getURLName(); + if (url != null) + return url.toString(); + else + return super.toString(); + } + + /** + * Add the event and vector of listeners to the queue to be delivered. + * + * @param event the event + * @param vector the vector of listeners + */ + protected void queueEvent(MailEvent event, + Vector<? extends EventListener> vector) { + /* + * Copy the vector in order to freeze the state of the set + * of EventListeners the event should be delivered to prior + * to delivery. This ensures that any changes made to the + * Vector from a target listener's method during the delivery + * of this event will not take effect until after the event is + * delivered. + */ + @SuppressWarnings("unchecked") + Vector<? extends EventListener> v = (Vector)vector.clone(); + q.enqueue(event, v); + } + + /** + * Stop the event dispatcher thread so the queue can be garbage collected. + */ + @Override + protected void finalize() throws Throwable { + try { + q.terminateQueue(); + } finally { + super.finalize(); + } + } + + /** + * Package private method to allow Folder to get the Session for a Store. + */ + Session getSession() { + return session; + } + + /** + * Package private method to allow Folder to get the EventQueue for a Store. + */ + EventQueue getEventQueue() { + return q; + } +}
diff --git a/mail/src/main/java/javax/mail/Session.java b/mail/src/main/java/javax/mail/Session.java new file mode 100644 index 0000000..534143e --- /dev/null +++ b/mail/src/main/java/javax/mail/Session.java
@@ -0,0 +1,1421 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +import java.lang.reflect.*; +import java.io.*; +import java.net.*; +import java.security.*; +import java.util.Collections; +import java.util.Hashtable; +import java.util.Map; +import java.util.HashMap; +import java.util.List; +import java.util.ArrayList; +import java.util.Properties; +import java.util.StringTokenizer; +import java.util.ServiceLoader; +import java.util.logging.Level; +import java.util.concurrent.Executor; + +import com.sun.mail.util.LineInputStream; +import com.sun.mail.util.MailLogger; + +/** + * The Session class represents a mail session and is not subclassed. + * It collects together properties and defaults used by the mail API's. + * A single default session can be shared by multiple applications on the + * desktop. Unshared sessions can also be created. <p> + * + * The Session class provides access to the protocol providers that + * implement the <code>Store</code>, <code>Transport</code>, and related + * classes. The protocol providers are configured using the following files: + * <ul> + * <li> <code>javamail.providers</code> and + * <code>javamail.default.providers</code> </li> + * <li> <code>javamail.address.map</code> and + * <code>javamail.default.address.map</code> </li> + * </ul> + * <p> + * Each <code>javamail.</code><i>X</i> resource file is searched for using + * three methods in the following order: + * <ol> + * <li> <code><i>java.home</i>/<i>conf</i>/javamail.</code><i>X</i> </li> + * <li> <code>META-INF/javamail.</code><i>X</i> </li> + * <li> <code>META-INF/javamail.default.</code><i>X</i> </li> + * </ol> + * <p> + * (Where <i>java.home</i> is the value of the "java.home" System property + * and <i>conf</i> is the directory named "conf" if it exists, + * otherwise the directory named "lib"; the "conf" directory was + * introduced in JDK 1.9.) + * <p> + * The first method allows the user to include their own version of the + * resource file by placing it in the <i>conf</i> directory where the + * <code>java.home</code> property points. The second method allows an + * application that uses the JavaMail APIs to include their own resource + * files in their application's or jar file's <code>META-INF</code> + * directory. The <code>javamail.default.</code><i>X</i> default files + * are part of the JavaMail <code>mail.jar</code> file and should not be + * supplied by users. <p> + * + * File location depends upon how the <code>ClassLoader</code> method + * <code>getResource</code> is implemented. Usually, the + * <code>getResource</code> method searches through CLASSPATH until it + * finds the requested file and then stops. <p> + * + * The ordering of entries in the resource files matters. If multiple + * entries exist, the first entries take precedence over the later + * entries. For example, the first IMAP provider found will be set as the + * default IMAP implementation until explicitly changed by the + * application. The user- or system-supplied resource files augment, they + * do not override, the default files included with the JavaMail APIs. + * This means that all entries in all files loaded will be available. <p> + * + * <b><code>javamail.providers</code></b> and + * <b><code>javamail.default.providers</code></b><p> + * + * These resource files specify the stores and transports that are + * available on the system, allowing an application to "discover" what + * store and transport implementations are available. The protocol + * implementations are listed one per line. The file format defines four + * attributes that describe a protocol implementation. Each attribute is + * an "="-separated name-value pair with the name in lowercase. Each + * name-value pair is semi-colon (";") separated. The following names + * are defined. + * + * <table border=1> + * <caption> + * Attribute Names in Providers Files + * </caption> + * <tr> + * <th>Name</th><th>Description</th> + * </tr> + * <tr> + * <td>protocol</td> + * <td>Name assigned to protocol. + * For example, <code>smtp</code> for Transport.</td> + * </tr> + * <tr> + * <td>type</td> + * <td>Valid entries are <code>store</code> and <code>transport</code>.</td> + * </tr> + * <tr> + * <td>class</td> + * <td>Class name that implements this protocol.</td> + * </tr> + * <tr> + * <td>vendor</td> + * <td>Optional string identifying the vendor.</td> + * </tr> + * <tr> + * <td>version</td> + * <td>Optional string identifying the version.</td> + * </tr> + * </table><p> + * + * Here's an example of <code>META-INF/javamail.default.providers</code> + * file contents: + * <pre> + * protocol=imap; type=store; class=com.sun.mail.imap.IMAPStore; vendor=Oracle; + * protocol=smtp; type=transport; class=com.sun.mail.smtp.SMTPTransport; vendor=Oracle; + * </pre><p> + * + * The current implementation also supports configuring providers using + * the Java SE {@link java.util.ServiceLoader ServiceLoader} mechanism. + * When creating your own provider, create a {@link Provider} subclass, + * for example: + * <pre> + * package com.example; + * + * import javax.mail.Provider; + * + * public class MyProvider extends Provider { + * public MyProvider() { + * super(Provider.Type.STORE, "myprot", MyStore.class.getName(), + * "Example", null); + * } + * } + * </pre> + * Then include a file named <code>META-INF/services/javax.mail.Provider</code> + * in your jar file that lists the name of your Provider class: + * <pre> + * com.example.MyProvider + * </pre> + * <p> + * + * <b><code>javamail.address.map</code></b> and + * <b><code>javamail.default.address.map</code></b><p> + * + * These resource files map transport address types to the transport + * protocol. The <code>getType</code> method of + * <code>javax.mail.Address</code> returns the address type. The + * <code>javamail.address.map</code> file maps the transport type to the + * protocol. The file format is a series of name-value pairs. Each key + * name should correspond to an address type that is currently installed + * on the system; there should also be an entry for each + * <code>javax.mail.Address</code> implementation that is present if it is + * to be used. For example, the + * <code>javax.mail.internet.InternetAddress</code> method + * <code>getType</code> returns "rfc822". Each referenced protocol should + * be installed on the system. For the case of <code>news</code>, below, + * the client should install a Transport provider supporting the nntp + * protocol. <p> + * + * Here are the typical contents of a <code>javamail.address.map</code> file: + * <pre> + * rfc822=smtp + * news=nntp + * </pre> + * + * @author John Mani + * @author Bill Shannon + * @author Max Spivak + */ + +public final class Session { + + private final Properties props; + private final Authenticator authenticator; + private final Hashtable<URLName, PasswordAuthentication> authTable + = new Hashtable<>(); + private boolean debug = false; + private PrintStream out; // debug output stream + private MailLogger logger; + private List<Provider> providers; + private final Map<String, Provider> providersByProtocol = new HashMap<>(); + private final Map<String, Provider> providersByClassName = new HashMap<>(); + private final Properties addressMap = new Properties(); + // maps type to protocol + private boolean loadedProviders; // javamail.[default.]providers loaded? + // the queue of events to be delivered, if mail.event.scope===session + private final EventQueue q; + + // The default session. + private static Session defaultSession = null; + + private static final String confDir; + + static { + String dir = null; + try { + dir = AccessController.doPrivileged( + new PrivilegedAction<String>() { + @Override + public String run() { + String home = System.getProperty("java.home"); + String newdir = home + File.separator + "conf"; + File conf = new File(newdir); + if (conf.exists()) + return newdir + File.separator; + else + return home + File.separator + + "lib" + File.separator; + } + }); + } catch (Exception ex) { + // ignore any exceptions + } + confDir = dir; + } + + // Constructor is not public + private Session(Properties props, Authenticator authenticator) { + this.props = props; + this.authenticator = authenticator; + + if (Boolean.valueOf(props.getProperty("mail.debug")).booleanValue()) + debug = true; + + initLogger(); + logger.log(Level.CONFIG, "JavaMail version {0}", Version.version); + + // get the Class associated with the Authenticator + Class<?> cl; + if (authenticator != null) + cl = authenticator.getClass(); + else + cl = this.getClass(); + // load the resources + loadAddressMap(cl); + q = new EventQueue((Executor)props.get("mail.event.executor")); + } + + private final synchronized void initLogger() { + logger = new MailLogger(this.getClass(), "DEBUG", debug, getDebugOut()); + } + + /** + * Get a new Session object. + * + * @param props Properties object that hold relevant properties.<br> + * It is expected that the client supplies values + * for the properties listed in Appendix A of the + * JavaMail spec (particularly mail.store.protocol, + * mail.transport.protocol, mail.host, mail.user, + * and mail.from) as the defaults are unlikely to + * work in all cases. + * @param authenticator Authenticator object used to call back to + * the application when a user name and password is + * needed. + * @return a new Session object + * @see javax.mail.Authenticator + */ + public static Session getInstance(Properties props, + Authenticator authenticator) { + return new Session(props, authenticator); + } + + /** + * Get a new Session object. + * + * @param props Properties object that hold relevant properties.<br> + * It is expected that the client supplies values + * for the properties listed in Appendix A of the + * JavaMail spec (particularly mail.store.protocol, + * mail.transport.protocol, mail.host, mail.user, + * and mail.from) as the defaults are unlikely to + * work in all cases. + * @return a new Session object + * @since JavaMail 1.2 + */ + public static Session getInstance(Properties props) { + return new Session(props, null); + } + + /** + * Get the default Session object. If a default has not yet been + * setup, a new Session object is created and installed as the + * default. <p> + * + * Since the default session is potentially available to all + * code executing in the same Java virtual machine, and the session + * can contain security sensitive information such as user names + * and passwords, access to the default session is restricted. + * The Authenticator object, which must be created by the caller, + * is used indirectly to check access permission. The Authenticator + * object passed in when the session is created is compared with + * the Authenticator object passed in to subsequent requests to + * get the default session. If both objects are the same, or are + * from the same ClassLoader, the request is allowed. Otherwise, + * it is denied. <p> + * + * Note that if the Authenticator object used to create the session + * is null, anyone can get the default session by passing in null. <p> + * + * Note also that the Properties object is used only the first time + * this method is called, when a new Session object is created. + * Subsequent calls return the Session object that was created by the + * first call, and ignore the passed Properties object. Use the + * <code>getInstance</code> method to get a new Session object every + * time the method is called. <p> + * + * Additional security Permission objects may be used to + * control access to the default session. <p> + * + * In the current implementation, if a SecurityManager is set, the + * caller must have the <code>RuntimePermission("setFactory")</code> + * permission. + * + * @param props Properties object. Used only if a new Session + * object is created.<br> + * It is expected that the client supplies values + * for the properties listed in Appendix A of the + * JavaMail spec (particularly mail.store.protocol, + * mail.transport.protocol, mail.host, mail.user, + * and mail.from) as the defaults are unlikely to + * work in all cases. + * @param authenticator Authenticator object. Used only if a + * new Session object is created. Otherwise, + * it must match the Authenticator used to create + * the Session. + * @return the default Session object + */ + public static synchronized Session getDefaultInstance(Properties props, + Authenticator authenticator) { + if (defaultSession == null) { + SecurityManager security = System.getSecurityManager(); + if (security != null) + security.checkSetFactory(); + defaultSession = new Session(props, authenticator); + } else { + // have to check whether caller is allowed to see default session + if (defaultSession.authenticator == authenticator) + ; // either same object or both null, either way OK + else if (defaultSession.authenticator != null && + authenticator != null && + defaultSession.authenticator.getClass().getClassLoader() == + authenticator.getClass().getClassLoader()) + ; // both objects came from the same class loader, OK + else + // anything else is not allowed + throw new SecurityException("Access to default session denied"); + } + + return defaultSession; + } + + /** + * Get the default Session object. If a default has not yet been + * setup, a new Session object is created and installed as the + * default. <p> + * + * Note that a default session created with no Authenticator is + * available to all code executing in the same Java virtual + * machine, and the session can contain security sensitive + * information such as user names and passwords. + * + * @param props Properties object. Used only if a new Session + * object is created.<br> + * It is expected that the client supplies values + * for the properties listed in Appendix A of the + * JavaMail spec (particularly mail.store.protocol, + * mail.transport.protocol, mail.host, mail.user, + * and mail.from) as the defaults are unlikely to + * work in all cases. + * @return the default Session object + * @since JavaMail 1.2 + */ + public static Session getDefaultInstance(Properties props) { + return getDefaultInstance(props, null); + } + + /** + * Set the debug setting for this Session. + * <p> + * Since the debug setting can be turned on only after the Session + * has been created, to turn on debugging in the Session + * constructor, set the property <code>mail.debug</code> in the + * Properties object passed in to the constructor to true. The + * value of the <code>mail.debug</code> property is used to + * initialize the per-Session debugging flag. Subsequent calls to + * the <code>setDebug</code> method manipulate the per-Session + * debugging flag and have no affect on the <code>mail.debug</code> + * property. + * + * @param debug Debug setting + */ + public synchronized void setDebug(boolean debug) { + this.debug = debug; + initLogger(); + logger.log(Level.CONFIG, "setDebug: JavaMail version {0}", + Version.version); + } + + /** + * Get the debug setting for this Session. + * + * @return current debug setting + */ + public synchronized boolean getDebug() { + return debug; + } + + /** + * Set the stream to be used for debugging output for this session. + * If <code>out</code> is null, <code>System.out</code> will be used. + * Note that debugging output that occurs before any session is created, + * as a result of setting the <code>mail.debug</code> system property, + * will always be sent to <code>System.out</code>. + * + * @param out the PrintStream to use for debugging output + * @since JavaMail 1.3 + */ + public synchronized void setDebugOut(PrintStream out) { + this.out = out; + initLogger(); + } + + /** + * Returns the stream to be used for debugging output. If no stream + * has been set, <code>System.out</code> is returned. + * + * @return the PrintStream to use for debugging output + * @since JavaMail 1.3 + */ + public synchronized PrintStream getDebugOut() { + if (out == null) + return System.out; + else + return out; + } + + /** + * This method returns an array of all the implementations installed + * via the javamail.[default.]providers files that can + * be loaded using the ClassLoader available to this application. + * + * @return Array of configured providers + */ + public synchronized Provider[] getProviders() { + List<Provider> plist = new ArrayList<Provider>(); + boolean needFallback = true; + // first, add all the services + ServiceLoader<Provider> loader = ServiceLoader.load(Provider.class); + for (Provider p : loader) { + plist.add(p); + needFallback = false; + } + // then, add all the providers from config files + if (!loadedProviders) + loadProviders(needFallback); + if (providers != null) + plist.addAll(providers); + Provider[] _providers = new Provider[plist.size()]; + plist.toArray(_providers); + return _providers; + } + + /** + * Returns the default Provider for the protocol + * specified. Checks mail.<protocol>.class property + * first and if it exists, returns the Provider + * associated with this implementation. If it doesn't exist, + * returns the Provider that appeared first in the + * configuration files. If an implementation for the protocol + * isn't found, throws NoSuchProviderException + * + * @param protocol Configured protocol (i.e. smtp, imap, etc) + * @return Currently configured Provider for the specified protocol + * @exception NoSuchProviderException If a provider for the given + * protocol is not found. + */ + public synchronized Provider getProvider(String protocol) + throws NoSuchProviderException { + + if (protocol == null || protocol.length() <= 0) { + throw new NoSuchProviderException("Invalid protocol: null"); + } + + Provider _provider = null; + + // check if the mail.<protocol>.class property exists + String _className = props.getProperty("mail."+protocol+".class"); + if (_className != null) { + if (logger.isLoggable(Level.FINE)) { + logger.fine("mail."+protocol+ + ".class property exists and points to " + + _className); + } + _provider = getProviderByClassName(_className); + } + + if (_provider == null) + _provider = getProviderByProtocol(protocol); + + if (_provider == null) { + throw new NoSuchProviderException("No provider for " + protocol); + } else { + if (logger.isLoggable(Level.FINE)) { + logger.fine("getProvider() returning " + _provider.toString()); + } + return _provider; + } + } + + /** + * Set the passed Provider to be the default implementation + * for the protocol in Provider.protocol overriding any previous values. + * + * @param provider Currently configured Provider which will be + * set as the default for the protocol + * @exception NoSuchProviderException If the provider passed in + * is invalid. + */ + public synchronized void setProvider(Provider provider) + throws NoSuchProviderException { + if (provider == null) { + throw new NoSuchProviderException("Can't set null provider"); + } + providersByProtocol.put(provider.getProtocol(), provider); + providersByClassName.put(provider.getClassName(), provider); + props.put("mail." + provider.getProtocol() + ".class", + provider.getClassName()); + } + + + /** + * Get a Store object that implements this user's desired Store + * protocol. The <code>mail.store.protocol</code> property specifies the + * desired protocol. If an appropriate Store object is not obtained, + * NoSuchProviderException is thrown + * + * @return a Store object + * @exception NoSuchProviderException If a provider for the given + * protocol is not found. + */ + public Store getStore() throws NoSuchProviderException { + return getStore(getProperty("mail.store.protocol")); + } + + /** + * Get a Store object that implements the specified protocol. If an + * appropriate Store object cannot be obtained, + * NoSuchProviderException is thrown. + * + * @param protocol the Store protocol + * @return a Store object + * @exception NoSuchProviderException If a provider for the given + * protocol is not found. + */ + public Store getStore(String protocol) throws NoSuchProviderException { + return getStore(new URLName(protocol, null, -1, null, null, null)); + } + + + /** + * Get a Store object for the given URLName. If the requested Store + * object cannot be obtained, NoSuchProviderException is thrown. + * + * The "scheme" part of the URL string (Refer RFC 1738) is used + * to locate the Store protocol. <p> + * + * @param url URLName that represents the desired Store + * @return a closed Store object + * @see #getFolder(URLName) + * @see javax.mail.URLName + * @exception NoSuchProviderException If a provider for the given + * URLName is not found. + */ + public Store getStore(URLName url) throws NoSuchProviderException { + String protocol = url.getProtocol(); + Provider p = getProvider(protocol); + return getStore(p, url); + } + + /** + * Get an instance of the store specified by Provider. Instantiates + * the store and returns it. + * + * @param provider Store Provider that will be instantiated + * @return Instantiated Store + * @exception NoSuchProviderException If a provider for the given + * Provider is not found. + */ + public Store getStore(Provider provider) throws NoSuchProviderException { + return getStore(provider, null); + } + + + /** + * Get an instance of the store specified by Provider. If the URLName + * is not null, uses it, otherwise creates a new one. Instantiates + * the store and returns it. This is a private method used by + * getStore(Provider) and getStore(URLName) + * + * @param provider Store Provider that will be instantiated + * @param url URLName used to instantiate the Store + * @return Instantiated Store + * @exception NoSuchProviderException If a provider for the given + * Provider/URLName is not found. + */ + private Store getStore(Provider provider, URLName url) + throws NoSuchProviderException { + + // make sure we have the correct type of provider + if (provider == null || provider.getType() != Provider.Type.STORE ) { + throw new NoSuchProviderException("invalid provider"); + } + + return getService(provider, url, Store.class); + } + + /** + * Get a closed Folder object for the given URLName. If the requested + * Folder object cannot be obtained, null is returned. <p> + * + * The "scheme" part of the URL string (Refer RFC 1738) is used + * to locate the Store protocol. The rest of the URL string (that is, + * the "schemepart", as per RFC 1738) is used by that Store + * in a protocol dependent manner to locate and instantiate the + * appropriate Folder object. <p> + * + * Note that RFC 1738 also specifies the syntax for the + * "schemepart" for IP-based protocols (IMAP4, POP3, etc.). + * Providers of IP-based mail Stores should implement that + * syntax for referring to Folders. <p> + * + * @param url URLName that represents the desired folder + * @return Folder + * @see #getStore(URLName) + * @see javax.mail.URLName + * @exception NoSuchProviderException If a provider for the given + * URLName is not found. + * @exception MessagingException if the Folder could not be + * located or created. + */ + public Folder getFolder(URLName url) + throws MessagingException { + // First get the Store + Store store = getStore(url); + store.connect(); + return store.getFolder(url); + } + + /** + * Get a Transport object that implements this user's desired + * Transport protcol. The <code>mail.transport.protocol</code> property + * specifies the desired protocol. If an appropriate Transport + * object cannot be obtained, MessagingException is thrown. + * + * @return a Transport object + * @exception NoSuchProviderException If the provider is not found. + */ + public Transport getTransport() throws NoSuchProviderException { + String prot = getProperty("mail.transport.protocol"); + if (prot != null) + return getTransport(prot); + // if the property isn't set, use the protocol for "rfc822" + prot = (String)addressMap.get("rfc822"); + if (prot != null) + return getTransport(prot); + return getTransport("smtp"); // if all else fails + } + + /** + * Get a Transport object that implements the specified protocol. + * If an appropriate Transport object cannot be obtained, null is + * returned. + * + * @param protocol the Transport protocol + * @return a Transport object + * @exception NoSuchProviderException If provider for the given + * protocol is not found. + */ + public Transport getTransport(String protocol) + throws NoSuchProviderException { + return getTransport(new URLName(protocol, null, -1, null, null, null)); + } + + + /** + * Get a Transport object for the given URLName. If the requested + * Transport object cannot be obtained, NoSuchProviderException is thrown. + * + * The "scheme" part of the URL string (Refer RFC 1738) is used + * to locate the Transport protocol. <p> + * + * @param url URLName that represents the desired Transport + * @return a closed Transport object + * @see javax.mail.URLName + * @exception NoSuchProviderException If a provider for the given + * URLName is not found. + */ + public Transport getTransport(URLName url) throws NoSuchProviderException { + String protocol = url.getProtocol(); + Provider p = getProvider(protocol); + return getTransport(p, url); + } + + /** + * Get an instance of the transport specified in the Provider. Instantiates + * the transport and returns it. + * + * @param provider Transport Provider that will be instantiated + * @return Instantiated Transport + * @exception NoSuchProviderException If provider for the given + * provider is not found. + */ + public Transport getTransport(Provider provider) + throws NoSuchProviderException { + return getTransport(provider, null); + } + + /** + * Get a Transport object that can transport a Message of the + * specified address type. + * + * @param address an address for which a Transport is needed + * @return A Transport object + * @see javax.mail.Address + * @exception NoSuchProviderException If provider for the + * Address type is not found + */ + public Transport getTransport(Address address) + throws NoSuchProviderException { + + String transportProtocol; + transportProtocol = + getProperty("mail.transport.protocol." + address.getType()); + if (transportProtocol != null) + return getTransport(transportProtocol); + transportProtocol = (String)addressMap.get(address.getType()); + if (transportProtocol != null) + return getTransport(transportProtocol); + throw new NoSuchProviderException("No provider for Address type: "+ + address.getType()); + } + + /** + * Get a Transport object using the given provider and urlname. + * + * @param provider the provider to use + * @param url urlname to use (can be null) + * @return A Transport object + * @exception NoSuchProviderException If no provider or the provider + * was the wrong class. + */ + + private Transport getTransport(Provider provider, URLName url) + throws NoSuchProviderException { + // make sure we have the correct type of provider + if (provider == null || provider.getType() != Provider.Type.TRANSPORT) { + throw new NoSuchProviderException("invalid provider"); + } + + return getService(provider, url, Transport.class); + } + + /** + * Get a Service object. Needs a provider object, but will + * create a URLName if needed. It attempts to instantiate + * the correct class. + * + * @param provider which provider to use + * @param url which URLName to use (can be null) + * @param type the service type (class) + * @exception NoSuchProviderException thrown when the class cannot be + * found or when it does not have the correct constructor + * (Session, URLName), or if it is not derived from + * Service. + */ + private <T extends Service> T getService(Provider provider, URLName url, + Class<T> type) + throws NoSuchProviderException { + // need a provider and url + if (provider == null) { + throw new NoSuchProviderException("null"); + } + + // create a url if needed + if (url == null) { + url = new URLName(provider.getProtocol(), null, -1, + null, null, null); + } + + Object service = null; + + // get the ClassLoader associated with the Authenticator + ClassLoader cl; + if (authenticator != null) + cl = authenticator.getClass().getClassLoader(); + else + cl = this.getClass().getClassLoader(); + + // now load the class + Class<?> serviceClass = null; + try { + // First try the "application's" class loader. + ClassLoader ccl = getContextClassLoader(); + if (ccl != null) + try { + serviceClass = + Class.forName(provider.getClassName(), false, ccl); + } catch (ClassNotFoundException ex) { + // ignore it + } + if (serviceClass == null || !type.isAssignableFrom(serviceClass)) + serviceClass = + Class.forName(provider.getClassName(), false, cl); + + if (!type.isAssignableFrom(serviceClass)) + throw new ClassCastException( + type.getName() + " " + serviceClass.getName()); + } catch (Exception ex1) { + // That didn't work, now try the "system" class loader. + // (Need both of these because JDK 1.1 class loaders + // may not delegate to their parent class loader.) + try { + serviceClass = Class.forName(provider.getClassName()); + if (!type.isAssignableFrom(serviceClass)) + throw new ClassCastException( + type.getName() + " " + serviceClass.getName()); + } catch (Exception ex) { + // Nothing worked, give up. + logger.log(Level.FINE, "Exception loading provider", ex); + throw new NoSuchProviderException(provider.getProtocol()); + } + } + + // construct an instance of the class + try { + Class<?>[] c = {javax.mail.Session.class, javax.mail.URLName.class}; + Constructor<?> cons = serviceClass.getConstructor(c); + + Object[] o = {this, url}; + service = cons.newInstance(o); + + } catch (Exception ex) { + logger.log(Level.FINE, "Exception loading provider", ex); + throw new NoSuchProviderException(provider.getProtocol()); + } + + return type.cast(service); + } + + /** + * Save a PasswordAuthentication for this (store or transport) URLName. + * If pw is null the entry corresponding to the URLName is removed. + * <p> + * This is normally used only by the store or transport implementations + * to allow authentication information to be shared among multiple + * uses of a session. + * + * @param url the URLName + * @param pw the PasswordAuthentication to save + */ + public void setPasswordAuthentication(URLName url, + PasswordAuthentication pw) { + if (pw == null) + authTable.remove(url); + else + authTable.put(url, pw); + } + + /** + * Return any saved PasswordAuthentication for this (store or transport) + * URLName. Normally used only by store or transport implementations. + * + * @param url the URLName + * @return the PasswordAuthentication corresponding to the URLName + */ + public PasswordAuthentication getPasswordAuthentication(URLName url) { + return authTable.get(url); + } + + /** + * Call back to the application to get the needed user name and password. + * The application should put up a dialog something like: + * <pre> + * Connecting to <protocol> mail service on host <addr>, port <port>. + * <prompt> + * + * User Name: <defaultUserName> + * Password: + * </pre> + * + * @param addr InetAddress of the host. may be null. + * @param port the port on the host + * @param protocol protocol scheme (e.g. imap, pop3, etc.) + * @param prompt any additional String to show as part of + * the prompt; may be null. + * @param defaultUserName the default username. may be null. + * @return the authentication which was collected by the authenticator; + * may be null. + */ + public PasswordAuthentication requestPasswordAuthentication( + InetAddress addr, int port, + String protocol, String prompt, String defaultUserName) { + + if (authenticator != null) { + return authenticator.requestPasswordAuthentication( + addr, port, protocol, prompt, defaultUserName); + } else { + return null; + } + } + + /** + * Returns the Properties object associated with this Session + * + * @return Properties object + */ + public Properties getProperties() { + return props; + } + + /** + * Returns the value of the specified property. Returns null + * if this property does not exist. + * + * @param name the property name + * @return String that is the property value + */ + public String getProperty(String name) { + return props.getProperty(name); + } + + /** + * Get the Provider that uses the specified class name. + * + * @param className the class name + * @return the Provider + */ + private Provider getProviderByClassName(String className) { + // first, try our local list of providers + Provider p = providersByClassName.get(className); + if (p != null) + return p; + + // now, try services + ServiceLoader<Provider> loader = ServiceLoader.load(Provider.class); + for (Provider pp : loader) { + if (className.equals(pp.getClassName())) + return pp; + } + + // finally, if we haven't loaded our config, load it and try again + if (!loadedProviders) { + loadProviders(true); + p = providersByClassName.get(className); + } + return p; + } + + /** + * Get the Provider for the specified protocol. + * + * @param protocol the protocol + * @return the Provider + */ + private Provider getProviderByProtocol(String protocol) { + // first, try our local list of providers + Provider p = providersByProtocol.get(protocol); + if (p != null) + return p; + + // now, try services + ServiceLoader<Provider> loader = ServiceLoader.load(Provider.class); + for (Provider pp : loader) { + if (protocol.equals(pp.getProtocol())) + return pp; + } + + // finally, if we haven't loaded our config, load it and try again + if (!loadedProviders) { + loadProviders(true); + p = providersByProtocol.get(protocol); + } + return p; + } + + /** + * Load the protocol providers config files. + * If fallback is true, provide built in defaults if nothing is loaded. + */ + private void loadProviders(boolean fallback) { + StreamLoader loader = new StreamLoader() { + @Override + public void load(InputStream is) throws IOException { + loadProvidersFromStream(is); + } + }; + + // load system-wide javamail.providers from the + // <java.home>/{conf,lib} directory + try { + if (confDir != null) + loadFile(confDir + "javamail.providers", loader); + } catch (SecurityException ex) {} + + // get the Class associated with the Authenticator + Class<?> cl; + if (authenticator != null) + cl = authenticator.getClass(); + else + cl = this.getClass(); + + // load the META-INF/javamail.providers file supplied by an application + loadAllResources("META-INF/javamail.providers", cl, loader); + + // load default META-INF/javamail.default.providers from mail.jar file + loadResource("/META-INF/javamail.default.providers", cl, loader, false); + + /* + * If we haven't loaded any providers and the fallback configuration + * is needed, fake it. + */ + if ((providers == null || providers.size() == 0) && fallback) { + logger.config("failed to load any providers, using defaults"); + // failed to load any providers, initialize with our defaults + addProvider(new Provider(Provider.Type.STORE, + "imap", "com.sun.mail.imap.IMAPStore", + "Oracle", Version.version)); + addProvider(new Provider(Provider.Type.STORE, + "imaps", "com.sun.mail.imap.IMAPSSLStore", + "Oracle", Version.version)); + addProvider(new Provider(Provider.Type.STORE, + "pop3", "com.sun.mail.pop3.POP3Store", + "Oracle", Version.version)); + addProvider(new Provider(Provider.Type.STORE, + "pop3s", "com.sun.mail.pop3.POP3SSLStore", + "Oracle", Version.version)); + addProvider(new Provider(Provider.Type.TRANSPORT, + "smtp", "com.sun.mail.smtp.SMTPTransport", + "Oracle", Version.version)); + addProvider(new Provider(Provider.Type.TRANSPORT, + "smtps", "com.sun.mail.smtp.SMTPSSLTransport", + "Oracle", Version.version)); + } + + if (logger.isLoggable(Level.CONFIG)) { + // dump the output of the tables for debugging + logger.config("Tables of loaded providers from javamail.providers"); + logger.config("Providers Listed By Class Name: " + + providersByClassName.toString()); + logger.config("Providers Listed By Protocol: " + + providersByProtocol.toString()); + } + loadedProviders = true; + } + + private void loadProvidersFromStream(InputStream is) + throws IOException { + if (is != null) { + LineInputStream lis = new LineInputStream(is); + String currLine; + + // load and process one line at a time using LineInputStream + while ((currLine = lis.readLine()) != null) { + + if (currLine.startsWith("#")) + continue; + if (currLine.trim().length() == 0) + continue; // skip blank line + Provider.Type type = null; + String protocol = null, className = null; + String vendor = null, version = null; + + // separate line into key-value tuples + StringTokenizer tuples = new StringTokenizer(currLine,";"); + while (tuples.hasMoreTokens()) { + String currTuple = tuples.nextToken().trim(); + + // set the value of each attribute based on its key + int sep = currTuple.indexOf("="); + if (currTuple.startsWith("protocol=")) { + protocol = currTuple.substring(sep+1); + } else if (currTuple.startsWith("type=")) { + String strType = currTuple.substring(sep+1); + if (strType.equalsIgnoreCase("store")) { + type = Provider.Type.STORE; + } else if (strType.equalsIgnoreCase("transport")) { + type = Provider.Type.TRANSPORT; + } + } else if (currTuple.startsWith("class=")) { + className = currTuple.substring(sep+1); + } else if (currTuple.startsWith("vendor=")) { + vendor = currTuple.substring(sep+1); + } else if (currTuple.startsWith("version=")) { + version = currTuple.substring(sep+1); + } + } + + // check if a valid Provider; else, continue + if (type == null || protocol == null || className == null + || protocol.length() <= 0 || className.length() <= 0) { + + logger.log(Level.CONFIG, "Bad provider entry: {0}", + currLine); + continue; + } + Provider provider = new Provider(type, protocol, className, + vendor, version); + + // add the newly-created Provider to the lookup tables + addProvider(provider); + } + } + } + + /** + * Add a provider to the session. + * + * @param provider the provider to add + * @since JavaMail 1.4 + */ + public synchronized void addProvider(Provider provider) { + if (providers == null) + providers = new ArrayList<Provider>(); + providers.add(provider); + providersByClassName.put(provider.getClassName(), provider); + if (!providersByProtocol.containsKey(provider.getProtocol())) + providersByProtocol.put(provider.getProtocol(), provider); + } + + // load maps in reverse order of preference so that the preferred + // map is loaded last since its entries will override the previous ones + private void loadAddressMap(Class<?> cl) { + StreamLoader loader = new StreamLoader() { + @Override + public void load(InputStream is) throws IOException { + addressMap.load(is); + } + }; + + // load default META-INF/javamail.default.address.map from mail.jar + loadResource("/META-INF/javamail.default.address.map", cl, loader, true); + + // load the META-INF/javamail.address.map file supplied by an app + loadAllResources("META-INF/javamail.address.map", cl, loader); + + // load system-wide javamail.address.map from the + // <java.home>/{conf,lib} directory + try { + if (confDir != null) + loadFile(confDir + "javamail.address.map", loader); + } catch (SecurityException ex) {} + + if (addressMap.isEmpty()) { + logger.config("failed to load address map, using defaults"); + addressMap.put("rfc822", "smtp"); + } + } + + /** + * Set the default transport protocol to use for addresses of + * the specified type. Normally the default is set by the + * <code>javamail.default.address.map</code> or + * <code>javamail.address.map</code> files or resources. + * + * @param addresstype type of address + * @param protocol name of protocol + * @see #getTransport(Address) + * @since JavaMail 1.4 + */ + public synchronized void setProtocolForAddress(String addresstype, + String protocol) { + if (protocol == null) + addressMap.remove(addresstype); + else + addressMap.put(addresstype, protocol); + } + + /** + * Load from the named file. + */ + private void loadFile(String name, StreamLoader loader) { + InputStream clis = null; + try { + clis = new BufferedInputStream(new FileInputStream(name)); + loader.load(clis); + logger.log(Level.CONFIG, "successfully loaded file: {0}", name); + } catch (FileNotFoundException fex) { + // ignore it + } catch (IOException e) { + if (logger.isLoggable(Level.CONFIG)) + logger.log(Level.CONFIG, "not loading file: " + name, e); + } catch (SecurityException sex) { + if (logger.isLoggable(Level.CONFIG)) + logger.log(Level.CONFIG, "not loading file: " + name, sex); + } finally { + try { + if (clis != null) + clis.close(); + } catch (IOException ex) { } // ignore it + } + } + + /** + * Load from the named resource. + */ + private void loadResource(String name, Class<?> cl, StreamLoader loader, + boolean expected) { + InputStream clis = null; + try { + clis = getResourceAsStream(cl, name); + if (clis != null) { + loader.load(clis); + logger.log(Level.CONFIG, "successfully loaded resource: {0}", + name); + } else { + if (expected) + logger.log(Level.WARNING, + "expected resource not found: {0}", name); + } + } catch (IOException e) { + logger.log(Level.CONFIG, "Exception loading resource", e); + } catch (SecurityException sex) { + logger.log(Level.CONFIG, "Exception loading resource", sex); + } finally { + try { + if (clis != null) + clis.close(); + } catch (IOException ex) { } // ignore it + } + } + + /** + * Load all of the named resource. + */ + private void loadAllResources(String name, Class<?> cl, + StreamLoader loader) { + boolean anyLoaded = false; + try { + URL[] urls; + ClassLoader cld = null; + // First try the "application's" class loader. + cld = getContextClassLoader(); + if (cld == null) + cld = cl.getClassLoader(); + if (cld != null) + urls = getResources(cld, name); + else + urls = getSystemResources(name); + if (urls != null) { + for (int i = 0; i < urls.length; i++) { + URL url = urls[i]; + InputStream clis = null; + logger.log(Level.CONFIG, "URL {0}", url); + try { + clis = openStream(url); + if (clis != null) { + loader.load(clis); + anyLoaded = true; + logger.log(Level.CONFIG, + "successfully loaded resource: {0}", url); + } else { + logger.log(Level.CONFIG, + "not loading resource: {0}", url); + } + } catch (FileNotFoundException fex) { + // ignore it + } catch (IOException ioex) { + logger.log(Level.CONFIG, "Exception loading resource", + ioex); + } catch (SecurityException sex) { + logger.log(Level.CONFIG, "Exception loading resource", + sex); + } finally { + try { + if (clis != null) + clis.close(); + } catch (IOException cex) { } + } + } + } + } catch (Exception ex) { + logger.log(Level.CONFIG, "Exception loading resource", ex); + } + + // if failed to load anything, fall back to old technique, just in case + if (!anyLoaded) { + /* + logger.config("!anyLoaded"); + */ + loadResource("/" + name, cl, loader, false); + } + } + + /* + * Following are security related methods that work on JDK 1.2 or newer. + */ + + static ClassLoader getContextClassLoader() { + return AccessController.doPrivileged( + new PrivilegedAction<ClassLoader>() { + @Override + public ClassLoader run() { + ClassLoader cl = null; + try { + cl = Thread.currentThread().getContextClassLoader(); + } catch (SecurityException ex) { + } + return cl; + } + } + ); + } + + private static InputStream getResourceAsStream(final Class<?> c, + final String name) throws IOException { + try { + return AccessController.doPrivileged( + new PrivilegedExceptionAction<InputStream>() { + @Override + public InputStream run() throws IOException { + try { + return c.getResourceAsStream(name); + } catch (RuntimeException e) { + // gracefully handle ClassLoader bugs (Tomcat) + IOException ioex = new IOException( + "ClassLoader.getResourceAsStream failed"); + ioex.initCause(e); + throw ioex; + } + } + } + ); + } catch (PrivilegedActionException e) { + throw (IOException)e.getException(); + } + } + + private static URL[] getResources(final ClassLoader cl, final String name) { + return AccessController.doPrivileged(new PrivilegedAction<URL[]>() { + @Override + public URL[] run() { + URL[] ret = null; + try { + List<URL> v = Collections.list(cl.getResources(name)); + if (!v.isEmpty()) { + ret = new URL[v.size()]; + v.toArray(ret); + } + } catch (IOException ioex) { + } catch (SecurityException ex) { } + return ret; + } + }); + } + + private static URL[] getSystemResources(final String name) { + return AccessController.doPrivileged(new PrivilegedAction<URL[]>() { + @Override + public URL[] run() { + URL[] ret = null; + try { + List<URL> v = Collections.list( + ClassLoader.getSystemResources(name)); + if (!v.isEmpty()) { + ret = new URL[v.size()]; + v.toArray(ret); + } + } catch (IOException ioex) { + } catch (SecurityException ex) { } + return ret; + } + }); + } + + private static InputStream openStream(final URL url) throws IOException { + try { + return AccessController.doPrivileged( + new PrivilegedExceptionAction<InputStream>() { + @Override + public InputStream run() throws IOException { + return url.openStream(); + } + } + ); + } catch (PrivilegedActionException e) { + throw (IOException)e.getException(); + } + } + + EventQueue getEventQueue() { + return q; + } +} + +/** + * Support interface to generalize + * code that loads resources from stream. + */ +interface StreamLoader { + public void load(InputStream is) throws IOException; +}
diff --git a/mail/src/main/java/javax/mail/Store.java b/mail/src/main/java/javax/mail/Store.java new file mode 100644 index 0000000..2678a09 --- /dev/null +++ b/mail/src/main/java/javax/mail/Store.java
@@ -0,0 +1,301 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +import java.io.*; +import java.net.*; +import java.util.*; +import javax.mail.event.*; + +/** + * An abstract class that models a message store and its + * access protocol, for storing and retrieving messages. + * Subclasses provide actual implementations. <p> + * + * Note that <code>Store</code> extends the <code>Service</code> + * class, which provides many common methods for naming stores, + * connecting to stores, and listening to connection events. + * + * @author John Mani + * @author Bill Shannon + * + * @see javax.mail.Service + * @see javax.mail.event.ConnectionEvent + * @see javax.mail.event.StoreEvent + */ + +public abstract class Store extends Service { + + /** + * Constructor. + * + * @param session Session object for this Store. + * @param urlname URLName object to be used for this Store + */ + protected Store(Session session, URLName urlname) { + super(session, urlname); + } + + /** + * Returns a Folder object that represents the 'root' of + * the default namespace presented to the user by the Store. + * + * @return the root Folder + * @exception IllegalStateException if this Store is not connected. + * @exception MessagingException for other failures + */ + public abstract Folder getDefaultFolder() throws MessagingException; + + /** + * Return the Folder object corresponding to the given name. Note + * that a Folder object is returned even if the named folder does + * not physically exist on the Store. The <code>exists()</code> + * method on the folder object indicates whether this folder really + * exists. <p> + * + * Folder objects are not cached by the Store, so invoking this + * method on the same name multiple times will return that many + * distinct Folder objects. + * + * @param name The name of the Folder. In some Stores, name can + * be an absolute path if it starts with the + * hierarchy delimiter. Else it is interpreted + * relative to the 'root' of this namespace. + * @return Folder object + * @exception IllegalStateException if this Store is not connected. + * @exception MessagingException for other failures + * @see Folder#exists + * @see Folder#create + */ + public abstract Folder getFolder(String name) + throws MessagingException; + + /** + * Return a closed Folder object, corresponding to the given + * URLName. The store specified in the given URLName should + * refer to this Store object. <p> + * + * Implementations of this method may obtain the name of the + * actual folder using the <code>getFile()</code> method on + * URLName, and use that name to create the folder. + * + * @param url URLName that denotes a folder + * @return Folder object + * @exception IllegalStateException if this Store is not connected. + * @exception MessagingException for other failures + * @see URLName + */ + public abstract Folder getFolder(URLName url) + throws MessagingException; + + /** + * Return a set of folders representing the <i>personal</i> namespaces + * for the current user. A personal namespace is a set of names that + * is considered within the personal scope of the authenticated user. + * Typically, only the authenticated user has access to mail folders + * in their personal namespace. If an INBOX exists for a user, it + * must appear within the user's personal namespace. In the + * typical case, there should be only one personal namespace for each + * user in each Store. <p> + * + * This implementation returns an array with a single entry containing + * the return value of the <code>getDefaultFolder</code> method. + * Subclasses should override this method to return appropriate information. + * + * @return array of Folder objects + * @exception IllegalStateException if this Store is not connected. + * @exception MessagingException for other failures + * @since JavaMail 1.2 + */ + public Folder[] getPersonalNamespaces() throws MessagingException { + return new Folder[] { getDefaultFolder() }; + } + + /** + * Return a set of folders representing the namespaces for + * <code>user</code>. The namespaces returned represent the + * personal namespaces for the user. To access mail folders in the + * other user's namespace, the currently authenticated user must be + * explicitly granted access rights. For example, it is common for + * a manager to grant to their secretary access rights to their + * mail folders. <p> + * + * This implementation returns an empty array. Subclasses should + * override this method to return appropriate information. + * + * @param user the user name + * @return array of Folder objects + * @exception IllegalStateException if this Store is not connected. + * @exception MessagingException for other failures + * @since JavaMail 1.2 + */ + public Folder[] getUserNamespaces(String user) + throws MessagingException { + return new Folder[0]; + } + + /** + * Return a set of folders representing the <i>shared</i> namespaces. + * A shared namespace is a namespace that consists of mail folders + * that are intended to be shared amongst users and do not exist + * within a user's personal namespace. <p> + * + * This implementation returns an empty array. Subclasses should + * override this method to return appropriate information. + * + * @exception IllegalStateException if this Store is not connected. + * @exception MessagingException for other failures + * @return array of Folder objects + * @since JavaMail 1.2 + */ + public Folder[] getSharedNamespaces() throws MessagingException { + return new Folder[0]; + } + + // Vector of Store listeners + private volatile Vector<StoreListener> storeListeners = null; + + /** + * Add a listener for StoreEvents on this Store. <p> + * + * The default implementation provided here adds this listener + * to an internal list of StoreListeners. + * + * @param l the Listener for Store events + * @see javax.mail.event.StoreEvent + */ + public synchronized void addStoreListener(StoreListener l) { + if (storeListeners == null) + storeListeners = new Vector<>(); + storeListeners.addElement(l); + } + + /** + * Remove a listener for Store events. <p> + * + * The default implementation provided here removes this listener + * from the internal list of StoreListeners. + * + * @param l the listener + * @see #addStoreListener + */ + public synchronized void removeStoreListener(StoreListener l) { + if (storeListeners != null) + storeListeners.removeElement(l); + } + + /** + * Notify all StoreListeners. Store implementations are + * expected to use this method to broadcast StoreEvents. <p> + * + * The provided default implementation queues the event into + * an internal event queue. An event dispatcher thread dequeues + * events from the queue and dispatches them to the registered + * StoreListeners. Note that the event dispatching occurs + * in a separate thread, thus avoiding potential deadlock problems. + * + * @param type the StoreEvent type + * @param message a message for the StoreEvent + */ + protected void notifyStoreListeners(int type, String message) { + if (storeListeners == null) + return; + + StoreEvent e = new StoreEvent(this, type, message); + queueEvent(e, storeListeners); + } + + // Vector of folder listeners + private volatile Vector<FolderListener> folderListeners = null; + + /** + * Add a listener for Folder events on any Folder object + * obtained from this Store. FolderEvents are delivered to + * FolderListeners on the affected Folder as well as to + * FolderListeners on the containing Store. <p> + * + * The default implementation provided here adds this listener + * to an internal list of FolderListeners. + * + * @param l the Listener for Folder events + * @see javax.mail.event.FolderEvent + */ + public synchronized void addFolderListener(FolderListener l) { + if (folderListeners == null) + folderListeners = new Vector<>(); + folderListeners.addElement(l); + } + + /** + * Remove a listener for Folder events. <p> + * + * The default implementation provided here removes this listener + * from the internal list of FolderListeners. + * + * @param l the listener + * @see #addFolderListener + */ + public synchronized void removeFolderListener(FolderListener l) { + if (folderListeners != null) + folderListeners.removeElement(l); + } + + /** + * Notify all FolderListeners. Store implementations are + * expected to use this method to broadcast Folder events. <p> + * + * The provided default implementation queues the event into + * an internal event queue. An event dispatcher thread dequeues + * events from the queue and dispatches them to the registered + * FolderListeners. Note that the event dispatching occurs + * in a separate thread, thus avoiding potential deadlock problems. + * + * @param type type of FolderEvent + * @param folder affected Folder + * @see #notifyFolderRenamedListeners + */ + protected void notifyFolderListeners(int type, Folder folder) { + if (folderListeners == null) + return; + + FolderEvent e = new FolderEvent(this, folder, type); + queueEvent(e, folderListeners); + } + + /** + * Notify all FolderListeners about the renaming of a folder. + * Store implementations are expected to use this method to broadcast + * Folder events indicating the renaming of folders. <p> + * + * The provided default implementation queues the event into + * an internal event queue. An event dispatcher thread dequeues + * events from the queue and dispatches them to the registered + * FolderListeners. Note that the event dispatching occurs + * in a separate thread, thus avoiding potential deadlock problems. + * + * @param oldF the folder being renamed + * @param newF the folder representing the new name. + * @since JavaMail 1.1 + */ + protected void notifyFolderRenamedListeners(Folder oldF, Folder newF) { + if (folderListeners == null) + return; + + FolderEvent e = new FolderEvent(this, oldF, newF,FolderEvent.RENAMED); + queueEvent(e, folderListeners); + } +}
diff --git a/mail/src/main/java/javax/mail/StoreClosedException.java b/mail/src/main/java/javax/mail/StoreClosedException.java new file mode 100644 index 0000000..29a0bbb --- /dev/null +++ b/mail/src/main/java/javax/mail/StoreClosedException.java
@@ -0,0 +1,83 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +/** + * This exception is thrown when a method is invoked on a Messaging object + * and the Store that owns that object has died due to some reason. + * This exception should be treated as a fatal error; in particular any + * messaging object belonging to that Store must be considered invalid. <p> + * + * The connect method may be invoked on the dead Store object to + * revive it. <p> + * + * The getMessage() method returns more detailed information about the + * error that caused this exception. <p> + * + * @author John Mani + */ + +public class StoreClosedException extends MessagingException { + transient private Store store; + + private static final long serialVersionUID = -3145392336120082655L; + + /** + * Constructs a StoreClosedException with no detail message. + * + * @param store The dead Store object + */ + public StoreClosedException(Store store) { + this(store, null); + } + + /** + * Constructs a StoreClosedException with the specified + * detail message. + * + * @param store The dead Store object + * @param message The detailed error message + */ + public StoreClosedException(Store store, String message) { + super(message); + this.store = store; + } + + /** + * Constructs a StoreClosedException with the specified + * detail message and embedded exception. The exception is chained + * to this exception. + * + * @param store The dead Store object + * @param message The detailed error message + * @param e The embedded exception + * @since JavaMail 1.5 + */ + public StoreClosedException(Store store, String message, Exception e) { + super(message, e); + this.store = store; + } + + /** + * Returns the dead Store object. + * + * @return the dead Store object + */ + public Store getStore() { + return store; + } +}
diff --git a/mail/src/main/java/javax/mail/Transport.java b/mail/src/main/java/javax/mail/Transport.java new file mode 100644 index 0000000..e9b3029 --- /dev/null +++ b/mail/src/main/java/javax/mail/Transport.java
@@ -0,0 +1,400 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Vector; +import javax.mail.event.*; + +/** + * An abstract class that models a message transport. + * Subclasses provide actual implementations. <p> + * + * Note that <code>Transport</code> extends the <code>Service</code> + * class, which provides many common methods for naming transports, + * connecting to transports, and listening to connection events. + * + * @author John Mani + * @author Max Spivak + * @author Bill Shannon + * + * @see javax.mail.Service + * @see javax.mail.event.ConnectionEvent + * @see javax.mail.event.TransportEvent + */ + +public abstract class Transport extends Service { + + /** + * Constructor. + * + * @param session Session object for this Transport. + * @param urlname URLName object to be used for this Transport + */ + public Transport(Session session, URLName urlname) { + super(session, urlname); + } + + /** + * Send a message. The message will be sent to all recipient + * addresses specified in the message (as returned from the + * <code>Message</code> method <code>getAllRecipients</code>), + * using message transports appropriate to each address. The + * <code>send</code> method calls the <code>saveChanges</code> + * method on the message before sending it. <p> + * + * If any of the recipient addresses is detected to be invalid by + * the Transport during message submission, a SendFailedException + * is thrown. Clients can get more detail about the failure by examining + * the exception. Whether or not the message is still sent successfully + * to any valid addresses depends on the Transport implementation. See + * SendFailedException for more details. Note also that success does + * not imply that the message was delivered to the ultimate recipient, + * as failures may occur in later stages of delivery. Once a Transport + * accepts a message for delivery to a recipient, failures that occur later + * should be reported to the user via another mechanism, such as + * returning the undeliverable message. <p> + * + * In typical usage, a SendFailedException reflects an error detected + * by the server. The details of the SendFailedException will usually + * contain the error message from the server (such as an SMTP error + * message). An address may be detected as invalid for a variety of + * reasons - the address may not exist, the address may have invalid + * syntax, the address may have exceeded its quota, etc. <p> + * + * Note that <code>send</code> is a static method that creates and + * manages its own connection. Any connection associated with any + * Transport instance used to invoke this method is ignored and not + * used. This method should only be invoked using the form + * <code>Transport.send(msg);</code>, and should never be invoked + * using an instance variable. + * + * @param msg the message to send + * @exception SendFailedException if the message could not + * be sent to some or any of the recipients. + * @exception MessagingException for other failures + * @see Message#saveChanges + * @see Message#getAllRecipients + * @see #send(Message, Address[]) + * @see javax.mail.SendFailedException + */ + public static void send(Message msg) throws MessagingException { + msg.saveChanges(); // do this first + send0(msg, msg.getAllRecipients(), null, null); + } + + /** + * Send the message to the specified addresses, ignoring any + * recipients specified in the message itself. The + * <code>send</code> method calls the <code>saveChanges</code> + * method on the message before sending it. <p> + * + * @param msg the message to send + * @param addresses the addresses to which to send the message + * @exception SendFailedException if the message could not + * be sent to some or any of the recipients. + * @exception MessagingException for other failures + * @see Message#saveChanges + * @see #send(Message) + * @see javax.mail.SendFailedException + */ + public static void send(Message msg, Address[] addresses) + throws MessagingException { + + msg.saveChanges(); + send0(msg, addresses, null, null); + } + + /** + * Send a message. The message will be sent to all recipient + * addresses specified in the message (as returned from the + * <code>Message</code> method <code>getAllRecipients</code>). + * The <code>send</code> method calls the <code>saveChanges</code> + * method on the message before sending it. <p> + * + * Use the specified user name and password to authenticate to + * the mail server. + * + * @param msg the message to send + * @param user the user name + * @param password this user's password + * @exception SendFailedException if the message could not + * be sent to some or any of the recipients. + * @exception MessagingException for other failures + * @see Message#saveChanges + * @see #send(Message) + * @see javax.mail.SendFailedException + * @since JavaMail 1.5 + */ + public static void send(Message msg, + String user, String password) throws MessagingException { + + msg.saveChanges(); + send0(msg, msg.getAllRecipients(), user, password); + } + + /** + * Send the message to the specified addresses, ignoring any + * recipients specified in the message itself. The + * <code>send</code> method calls the <code>saveChanges</code> + * method on the message before sending it. <p> + * + * Use the specified user name and password to authenticate to + * the mail server. + * + * @param msg the message to send + * @param addresses the addresses to which to send the message + * @param user the user name + * @param password this user's password + * @exception SendFailedException if the message could not + * be sent to some or any of the recipients. + * @exception MessagingException for other failures + * @see Message#saveChanges + * @see #send(Message) + * @see javax.mail.SendFailedException + * @since JavaMail 1.5 + */ + public static void send(Message msg, Address[] addresses, + String user, String password) throws MessagingException { + + msg.saveChanges(); + send0(msg, addresses, user, password); + } + + // send, but without the saveChanges + private static void send0(Message msg, Address[] addresses, + String user, String password) throws MessagingException { + + if (addresses == null || addresses.length == 0) + throw new SendFailedException("No recipient addresses"); + + /* + * protocols is a map containing the addresses + * indexed by address type + */ + Map<String, List<Address>> protocols + = new HashMap<>(); + + // Lists of addresses + List<Address> invalid = new ArrayList<>(); + List<Address> validSent = new ArrayList<>(); + List<Address> validUnsent = new ArrayList<>(); + + for (int i = 0; i < addresses.length; i++) { + // is this address type already in the map? + if (protocols.containsKey(addresses[i].getType())) { + List<Address> v = protocols.get(addresses[i].getType()); + v.add(addresses[i]); + } else { + // need to add a new protocol + List<Address> w = new ArrayList<>(); + w.add(addresses[i]); + protocols.put(addresses[i].getType(), w); + } + } + + int dsize = protocols.size(); + if (dsize == 0) + throw new SendFailedException("No recipient addresses"); + + Session s = (msg.session != null) ? msg.session : + Session.getDefaultInstance(System.getProperties(), null); + Transport transport; + + /* + * Optimize the case of a single protocol. + */ + if (dsize == 1) { + transport = s.getTransport(addresses[0]); + try { + if (user != null) + transport.connect(user, password); + else + transport.connect(); + transport.sendMessage(msg, addresses); + } finally { + transport.close(); + } + return; + } + + /* + * More than one protocol. Have to do them one at a time + * and collect addresses and chain exceptions. + */ + MessagingException chainedEx = null; + boolean sendFailed = false; + + for(List<Address> v : protocols.values()) { + Address[] protaddresses = new Address[v.size()]; + v.toArray(protaddresses); + + // Get a Transport that can handle this address type. + if ((transport = s.getTransport(protaddresses[0])) == null) { + // Could not find an appropriate Transport .. + // Mark these addresses invalid. + for (int j = 0; j < protaddresses.length; j++) + invalid.add(protaddresses[j]); + continue; + } + try { + transport.connect(); + transport.sendMessage(msg, protaddresses); + } catch (SendFailedException sex) { + sendFailed = true; + // chain the exception we're catching to any previous ones + if (chainedEx == null) + chainedEx = sex; + else + chainedEx.setNextException(sex); + + // retrieve invalid addresses + Address[] a = sex.getInvalidAddresses(); + if (a != null) + for (int j = 0; j < a.length; j++) + invalid.add(a[j]); + + // retrieve validSent addresses + a = sex.getValidSentAddresses(); + if (a != null) + for (int k = 0; k < a.length; k++) + validSent.add(a[k]); + + // retrieve validUnsent addresses + Address[] c = sex.getValidUnsentAddresses(); + if (c != null) + for (int l = 0; l < c.length; l++) + validUnsent.add(c[l]); + } catch (MessagingException mex) { + sendFailed = true; + // chain the exception we're catching to any previous ones + if (chainedEx == null) + chainedEx = mex; + else + chainedEx.setNextException(mex); + } finally { + transport.close(); + } + } + + // done with all protocols. throw exception if something failed + if (sendFailed || invalid.size() != 0 || validUnsent.size() != 0) { + Address[] a = null, b = null, c = null; + + // copy address lists into arrays + if (validSent.size() > 0) { + a = new Address[validSent.size()]; + validSent.toArray(a); + } + if (validUnsent.size() > 0) { + b = new Address[validUnsent.size()]; + validUnsent.toArray(b); + } + if (invalid.size() > 0) { + c = new Address[invalid.size()]; + invalid.toArray(c); + } + throw new SendFailedException("Sending failed", chainedEx, + a, b, c); + } + } + + /** + * Send the Message to the specified list of addresses. An appropriate + * TransportEvent indicating the delivery status is delivered to any + * TransportListener registered on this Transport. Also, if any of + * the addresses is invalid, a SendFailedException is thrown. + * Whether or not the message is still sent succesfully to + * any valid addresses depends on the Transport implementation. <p> + * + * Unlike the static <code>send</code> method, the <code>sendMessage</code> + * method does <em>not</em> call the <code>saveChanges</code> method on + * the message; the caller should do so. + * + * @param msg The Message to be sent + * @param addresses array of addresses to send this message to + * @see javax.mail.event.TransportEvent + * @exception SendFailedException if the send failed because of + * invalid addresses. + * @exception MessagingException if the connection is dead or not in the + * connected state + */ + public abstract void sendMessage(Message msg, Address[] addresses) + throws MessagingException; + + // Vector of Transport listeners + private volatile Vector<TransportListener> transportListeners = null; + + /** + * Add a listener for Transport events. <p> + * + * The default implementation provided here adds this listener + * to an internal list of TransportListeners. + * + * @param l the Listener for Transport events + * @see javax.mail.event.TransportEvent + */ + public synchronized void addTransportListener(TransportListener l) { + if (transportListeners == null) + transportListeners = new Vector<>(); + transportListeners.addElement(l); + } + + /** + * Remove a listener for Transport events. <p> + * + * The default implementation provided here removes this listener + * from the internal list of TransportListeners. + * + * @param l the listener + * @see #addTransportListener + */ + public synchronized void removeTransportListener(TransportListener l) { + if (transportListeners != null) + transportListeners.removeElement(l); + } + + /** + * Notify all TransportListeners. Transport implementations are + * expected to use this method to broadcast TransportEvents.<p> + * + * The provided default implementation queues the event into + * an internal event queue. An event dispatcher thread dequeues + * events from the queue and dispatches them to the registered + * TransportListeners. Note that the event dispatching occurs + * in a separate thread, thus avoiding potential deadlock problems. + * + * @param type the TransportEvent type + * @param validSent valid addresses to which message was sent + * @param validUnsent valid addresses to which message was not sent + * @param invalid the invalid addresses + * @param msg the message + */ + protected void notifyTransportListeners(int type, Address[] validSent, + Address[] validUnsent, + Address[] invalid, Message msg) { + if (transportListeners == null) + return; + + TransportEvent e = new TransportEvent(this, type, validSent, + validUnsent, invalid, msg); + queueEvent(e, transportListeners); + } +}
diff --git a/mail/src/main/java/javax/mail/UIDFolder.java b/mail/src/main/java/javax/mail/UIDFolder.java new file mode 100644 index 0000000..2a8b97c --- /dev/null +++ b/mail/src/main/java/javax/mail/UIDFolder.java
@@ -0,0 +1,206 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +import java.util.NoSuchElementException; + +/** + * The <code>UIDFolder</code> interface is implemented by Folders + * that can support the "disconnected" mode of operation, by providing + * unique-ids for messages in the folder. This interface is based on + * the IMAP model for supporting disconnected operation. <p> + * + * A Unique identifier (UID) is a positive long value, assigned to + * each message in a specific folder. Unique identifiers are assigned + * in a strictly <strong>ascending</strong> fashion in the mailbox. + * That is, as each message is added to the mailbox it is assigned a + * higher UID than the message(s) which were added previously. Unique + * identifiers persist across sessions. This permits a client to + * resynchronize its state from a previous session with the server. <p> + * + * Associated with every mailbox is a unique identifier validity value. + * If unique identifiers from an earlier session fail to persist to + * this session, the unique identifier validity value + * <strong>must</strong> be greater than the one used in the earlier + * session. <p> + * + * Refer to <A HREF="http://www.ietf.org/rfc/rfc2060.txt">RFC 2060</A> + * for more information. + * + * All the Folder objects returned by the default IMAP provider implement + * the UIDFolder interface. Use it as follows: + * <blockquote><pre> + * + * Folder f = store.getFolder("whatever"); + * UIDFolder uf = (UIDFolder)f; + * long uid = uf.getUID(msg); + * + * </pre></blockquote><p> + * + * @author Bill Shannon + * @author John Mani + */ + +public interface UIDFolder { + + /** + * A fetch profile item for fetching UIDs. + * This inner class extends the <code>FetchProfile.Item</code> + * class to add new FetchProfile item types, specific to UIDFolders. + * The only item currently defined here is the <code>UID</code> item. + * + * @see FetchProfile + */ + public static class FetchProfileItem extends FetchProfile.Item { + protected FetchProfileItem(String name) { + super(name); + } + + /** + * UID is a fetch profile item that can be included in a + * <code>FetchProfile</code> during a fetch request to a Folder. + * This item indicates that the UIDs for messages in the specified + * range are desired to be prefetched. <p> + * + * An example of how a client uses this is below: + * <blockquote><pre> + * + * FetchProfile fp = new FetchProfile(); + * fp.add(UIDFolder.FetchProfileItem.UID); + * folder.fetch(msgs, fp); + * + * </pre></blockquote> + */ + public static final FetchProfileItem UID = + new FetchProfileItem("UID"); + } + + /** + * This is a special value that can be used as the <code>end</code> + * parameter in <code>getMessagesByUID(start, end)</code>, to denote the + * UID of the last message in the folder. + * + * @see #getMessagesByUID + */ + public static final long LASTUID = -1; + + /** + * The largest value possible for a UID, a 32-bit unsigned integer. + * This can be used to fetch all new messages by keeping track of the + * last UID that was seen and using: + * <blockquote><pre> + * + * Folder f = store.getFolder("whatever"); + * UIDFolder uf = (UIDFolder)f; + * Message[] newMsgs = + * uf.getMessagesByUID(lastSeenUID + 1, UIDFolder.MAXUID); + * + * </pre></blockquote><p> + * + * @since JavaMail 1.6 + */ + public static final long MAXUID = 0xffffffffL; // max 32-bit unsigned int + + /** + * Returns the UIDValidity value associated with this folder. <p> + * + * Clients typically compare this value against a UIDValidity + * value saved from a previous session to insure that any cached + * UIDs are not stale. + * + * @return UIDValidity + * @exception MessagingException for failures + */ + public long getUIDValidity() throws MessagingException; + + /** + * Get the Message corresponding to the given UID. If no such + * message exists, <code>null</code> is returned. + * + * @param uid UID for the desired message + * @return the Message object. <code>null</code> is returned + * if no message corresponding to this UID is obtained. + * @exception MessagingException for failures + */ + public Message getMessageByUID(long uid) throws MessagingException; + + /** + * Get the Messages specified by the given range. The special + * value LASTUID can be used for the <code>end</code> parameter + * to indicate the UID of the last message in the folder. <p> + * + * Note that <code>end</code> need not be greater than <code>start</code>; + * the order of the range doesn't matter. + * Note also that, unless the folder is empty, use of LASTUID ensures + * that at least one message will be returned - the last message in the + * folder. + * + * @param start start UID + * @param end end UID + * @return array of Message objects + * @exception MessagingException for failures + * @see #LASTUID + */ + public Message[] getMessagesByUID(long start, long end) + throws MessagingException; + + /** + * Get the Messages specified by the given array of UIDs. If any UID is + * invalid, <code>null</code> is returned for that entry. <p> + * + * Note that the returned array will be of the same size as the specified + * array of UIDs, and <code>null</code> entries may be present in the + * array to indicate invalid UIDs. + * + * @param uids array of UIDs + * @return array of Message objects + * @exception MessagingException for failures + */ + public Message[] getMessagesByUID(long[] uids) + throws MessagingException; + + /** + * Get the UID for the specified message. Note that the message + * <strong>must</strong> belong to this folder. Otherwise + * java.util.NoSuchElementException is thrown. + * + * @param message Message from this folder + * @return UID for this message + * @exception NoSuchElementException if the given Message + * is not in this Folder. + * @exception MessagingException for other failures + */ + public long getUID(Message message) throws MessagingException; + + /** + * Returns the predicted UID that will be assigned to the + * next message that is appended to this folder. + * Messages might be appended to the folder after this value + * is retrieved, causing this value to be out of date. + * This value might only be updated when a folder is first opened. + * Note that messages may have been appended to the folder + * while it was open and thus this value may be out of + * date. <p> + * + * If the value is unknown, -1 is returned. <p> + * + * @return the UIDNEXT value, or -1 if unknown + * @exception MessagingException for failures + * @since JavaMail 1.6 + */ + public long getUIDNext() throws MessagingException; +}
diff --git a/mail/src/main/java/javax/mail/URLName.java b/mail/src/main/java/javax/mail/URLName.java new file mode 100644 index 0000000..e26955c --- /dev/null +++ b/mail/src/main/java/javax/mail/URLName.java
@@ -0,0 +1,786 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +import java.net.*; + +import java.io.ByteArrayOutputStream; +import java.io.OutputStreamWriter; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.util.BitSet; +import java.util.Locale; + + +/** + * The name of a URL. This class represents a URL name and also + * provides the basic parsing functionality to parse most internet + * standard URL schemes. <p> + * + * Note that this class differs from <code>java.net.URL</code> + * in that this class just represents the name of a URL, it does + * not model the connection to a URL. + * + * @author Christopher Cotton + * @author Bill Shannon + */ + +public class URLName { + + /** + * The full version of the URL + */ + protected String fullURL; + + /** + * The protocol to use (ftp, http, nntp, imap, pop3 ... etc.) . + */ + private String protocol; + + /** + * The username to use when connecting + */ + private String username; + + /** + * The password to use when connecting. + */ + private String password; + + /** + * The host name to which to connect. + */ + private String host; + + /** + * The host's IP address, used in equals and hashCode. + * Computed on demand. + */ + private InetAddress hostAddress; + private boolean hostAddressKnown = false; + + /** + * The protocol port to connect to. + */ + private int port = -1; + + /** + * The specified file name on that host. + */ + private String file; + + /** + * # reference. + */ + private String ref; + + /** + * Our hash code. + */ + private int hashCode = 0; + + /** + * A way to turn off encoding, just in case... + */ + private static boolean doEncode = true; + + static { + try { + doEncode = !Boolean.getBoolean("mail.URLName.dontencode"); + } catch (Exception ex) { + // ignore any errors + } + } + + /** + * Creates a URLName object from the specified protocol, + * host, port number, file, username, and password. Specifying a port + * number of -1 indicates that the URL should use the default port for + * the protocol. + * + * @param protocol the protocol + * @param host the host name + * @param port the port number + * @param file the file + * @param username the user name + * @param password the password + */ + public URLName( + String protocol, + String host, + int port, + String file, + String username, + String password + ) + { + this.protocol = protocol; + this.host = host; + this.port = port; + int refStart; + if (file != null && (refStart = file.indexOf('#')) != -1) { + this.file = file.substring(0, refStart); + this.ref = file.substring(refStart + 1); + } else { + this.file = file; + this.ref = null; + } + this.username = doEncode ? encode(username) : username; + this.password = doEncode ? encode(password) : password; + } + + /** + * Construct a URLName from a java.net.URL object. + * + * @param url the URL + */ + public URLName(URL url) { + this(url.toString()); + } + + /** + * Construct a URLName from the string. Parses out all the possible + * information (protocol, host, port, file, username, password). + * + * @param url the URL string + */ + public URLName(String url) { + parseString(url); + } + + /** + * Constructs a string representation of this URLName. + */ + @Override + public String toString() { + if (fullURL == null) { + // add the "protocol:" + StringBuilder tempURL = new StringBuilder(); + if (protocol != null) { + tempURL.append(protocol); + tempURL.append(":"); + } + + if (username != null || host != null) { + // add the "//" + tempURL.append("//"); + + // add the user:password@ + // XXX - can you just have a password? without a username? + if (username != null) { + tempURL.append(username); + + if (password != null){ + tempURL.append(":"); + tempURL.append(password); + } + + tempURL.append("@"); + } + + // add host + if (host != null) { + tempURL.append(host); + } + + // add port (if needed) + if (port != -1) { + tempURL.append(":"); + tempURL.append(Integer.toString(port)); + } + if (file != null) + tempURL.append("/"); + } + + // add the file + if (file != null) { + tempURL.append(file); + } + + // add the ref + if (ref != null) { + tempURL.append("#"); + tempURL.append(ref); + } + + // create the fullURL now + fullURL = tempURL.toString(); + } + + return fullURL; + } + + /** + * Method which does all of the work of parsing the string. + * + * @param url the URL string to parse + */ + protected void parseString(String url) { + // initialize everything in case called from subclass + // (URLName really should be a final class) + protocol = file = ref = host = username = password = null; + port = -1; + + int len = url.length(); + + // find the protocol + // XXX - should check for only legal characters before the colon + // (legal: a-z, A-Z, 0-9, "+", ".", "-") + int protocolEnd = url.indexOf(':'); + if (protocolEnd != -1) + protocol = url.substring(0, protocolEnd); + + // is this an Internet standard URL that contains a host name? + if (url.regionMatches(protocolEnd + 1, "//", 0, 2)) { + // find where the file starts + String fullhost = null; + int fileStart = url.indexOf('/', protocolEnd + 3); + if (fileStart != -1) { + fullhost = url.substring(protocolEnd + 3, fileStart); + if (fileStart + 1 < len) + file = url.substring(fileStart + 1); + else + file = ""; + } else + fullhost = url.substring(protocolEnd + 3); + + // examine the fullhost, for username password etc. + int i = fullhost.indexOf('@'); + if (i != -1) { + String fulluserpass = fullhost.substring(0, i); + fullhost = fullhost.substring(i + 1); + + // get user and password + int passindex = fulluserpass.indexOf(':'); + if (passindex != -1) { + username = fulluserpass.substring(0, passindex); + password = fulluserpass.substring(passindex + 1); + } else { + username = fulluserpass; + } + } + + // get the port (if there) + int portindex; + if (fullhost.length() > 0 && fullhost.charAt(0) == '[') { + // an IPv6 address? + portindex = fullhost.indexOf(':', fullhost.indexOf(']')); + } else { + portindex = fullhost.indexOf(':'); + } + if (portindex != -1) { + String portstring = fullhost.substring(portindex + 1); + if (portstring.length() > 0) { + try { + port = Integer.parseInt(portstring); + } catch (NumberFormatException nfex) { + port = -1; + } + } + + host = fullhost.substring(0, portindex); + } else { + host = fullhost; + } + } else { + if (protocolEnd + 1 < len) + file = url.substring(protocolEnd + 1); + } + + // extract the reference from the file name, if any + int refStart; + if (file != null && (refStart = file.indexOf('#')) != -1) { + ref = file.substring(refStart + 1); + file = file.substring(0, refStart); + } + } + + /** + * Returns the port number of this URLName. + * Returns -1 if the port is not set. + * + * @return the port number + */ + public int getPort() { + return port; + } + + /** + * Returns the protocol of this URLName. + * Returns null if this URLName has no protocol. + * + * @return the protocol + */ + public String getProtocol() { + return protocol; + } + + /** + * Returns the file name of this URLName. + * Returns null if this URLName has no file name. + * + * @return the file name of this URLName + */ + public String getFile() { + return file; + } + + /** + * Returns the reference of this URLName. + * Returns null if this URLName has no reference. + * + * @return the reference part of the URLName + */ + public String getRef() { + return ref; + } + + /** + * Returns the host of this URLName. + * Returns null if this URLName has no host. + * + * @return the host name + */ + public String getHost() { + return host; + } + + /** + * Returns the user name of this URLName. + * Returns null if this URLName has no user name. + * + * @return the user name + */ + public String getUsername() { + return doEncode ? decode(username) : username; + } + + /** + * Returns the password of this URLName. + * Returns null if this URLName has no password. + * + * @return the password + */ + public String getPassword() { + return doEncode ? decode(password) : password; + } + + /** + * Constructs a URL from the URLName. + * + * @return the URL + * @exception MalformedURLException if the URL is malformed + */ + public URL getURL() throws MalformedURLException { + // URL expects the file to include the separating "/" + String f = getFile(); + if (f == null) + f = ""; + else + f = "/" + f; + return new URL(getProtocol(), getHost(), getPort(), f); + } + + /** + * Compares two URLNames. The result is true if and only if the + * argument is not null and is a URLName object that represents the + * same URLName as this object. Two URLName objects are equal if + * they have the same protocol and the same host, + * the same port number on the host, the same username, + * and the same file on the host. The fields (host, username, + * file) are also considered the same if they are both + * null. <p> + * + * Hosts are considered equal if the names are equal (case independent) + * or if host name lookups for them both succeed and they both reference + * the same IP address. <p> + * + * Note that URLName has no knowledge of default port numbers for + * particular protocols, so "imap://host" and "imap://host:143" + * would not compare as equal. <p> + * + * Note also that the password field is not included in the comparison, + * nor is any reference field appended to the filename. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof URLName)) + return false; + URLName u2 = (URLName)obj; + + // compare protocols + if (!(protocol == u2.protocol || + (protocol != null && protocol.equals(u2.protocol)))) + return false; + + // compare hosts + InetAddress a1 = getHostAddress(), a2 = u2.getHostAddress(); + // if we have internet address for both, and they're not the same, fail + if (a1 != null && a2 != null) { + if (!a1.equals(a2)) + return false; + // else, if we have host names for both, and they're not the same, fail + } else if (host != null && u2.host != null) { + if (!host.equalsIgnoreCase(u2.host)) + return false; + // else, if not both null + } else if (host != u2.host) { + return false; + } + // at this point, hosts match + + // compare usernames + if (!(username == u2.username || + (username != null && username.equals(u2.username)))) + return false; + + // Forget about password since it doesn't + // really denote a different store. + + // compare files + String f1 = file == null ? "" : file; + String f2 = u2.file == null ? "" : u2.file; + + if (!f1.equals(f2)) + return false; + + // compare ports + if (port != u2.port) + return false; + + // all comparisons succeeded, they're equal + return true; + } + + /** + * Compute the hash code for this URLName. + */ + @Override + public int hashCode() { + if (hashCode != 0) + return hashCode; + if (protocol != null) + hashCode += protocol.hashCode(); + InetAddress addr = getHostAddress(); + if (addr != null) + hashCode += addr.hashCode(); + else if (host != null) + hashCode += host.toLowerCase(Locale.ENGLISH).hashCode(); + if (username != null) + hashCode += username.hashCode(); + if (file != null) + hashCode += file.hashCode(); + hashCode += port; + return hashCode; + } + + /** + * Get the IP address of our host. Look up the + * name the first time and remember that we've done + * so, whether the lookup fails or not. + */ + private synchronized InetAddress getHostAddress() { + if (hostAddressKnown) + return hostAddress; + if (host == null) + return null; + try { + hostAddress = InetAddress.getByName(host); + } catch (UnknownHostException ex) { + hostAddress = null; + } + hostAddressKnown = true; + return hostAddress; + } + + /** + * The class contains a utility method for converting a + * <code>String</code> into a MIME format called + * "<code>x-www-form-urlencoded</code>" format. + * <p> + * To convert a <code>String</code>, each character is examined in turn: + * <ul> + * <li>The ASCII characters '<code>a</code>' through '<code>z</code>', + * '<code>A</code>' through '<code>Z</code>', '<code>0</code>' + * through '<code>9</code>', and ".", "-", + * "*", "_" remain the same. + * <li>The space character '<code> </code>' is converted into a + * plus sign '<code>+</code>'. + * <li>All other characters are converted into the 3-character string + * "<code>%<i>xy</i></code>", where <i>xy</i> is the two-digit + * hexadecimal representation of the lower 8-bits of the character. + * </ul> + * + * @author Herb Jellinek + * @since JDK1.0 + */ + static BitSet dontNeedEncoding; + static final int caseDiff = ('a' - 'A'); + + /* The list of characters that are not encoded have been determined by + referencing O'Reilly's "HTML: The Definitive Guide" (page 164). */ + + static { + dontNeedEncoding = new BitSet(256); + int i; + for (i = 'a'; i <= 'z'; i++) { + dontNeedEncoding.set(i); + } + for (i = 'A'; i <= 'Z'; i++) { + dontNeedEncoding.set(i); + } + for (i = '0'; i <= '9'; i++) { + dontNeedEncoding.set(i); + } + /* encoding a space to a + is done in the encode() method */ + dontNeedEncoding.set(' '); + dontNeedEncoding.set('-'); + dontNeedEncoding.set('_'); + dontNeedEncoding.set('.'); + dontNeedEncoding.set('*'); + } + + /** + * Translates a string into <code>x-www-form-urlencoded</code> format. + * + * @param s <code>String</code> to be translated. + * @return the translated <code>String</code>. + */ + static String encode(String s) { + if (s == null) + return null; + // the common case is no encoding is needed + for (int i = 0; i < s.length(); i++) { + int c = (int)s.charAt(i); + if (c == ' ' || !dontNeedEncoding.get(c)) + return _encode(s); + } + return s; + } + + private static String _encode(String s) { + int maxBytesPerChar = 10; + StringBuilder out = new StringBuilder(s.length()); + ByteArrayOutputStream buf = new ByteArrayOutputStream(maxBytesPerChar); + OutputStreamWriter writer = new OutputStreamWriter(buf); + + for (int i = 0; i < s.length(); i++) { + int c = (int)s.charAt(i); + if (dontNeedEncoding.get(c)) { + if (c == ' ') { + c = '+'; + } + out.append((char)c); + } else { + // convert to external encoding before hex conversion + try { + writer.write(c); + writer.flush(); + } catch(IOException e) { + buf.reset(); + continue; + } + byte[] ba = buf.toByteArray(); + for (int j = 0; j < ba.length; j++) { + out.append('%'); + char ch = Character.forDigit((ba[j] >> 4) & 0xF, 16); + // converting to use uppercase letter as part of + // the hex value if ch is a letter. + if (Character.isLetter(ch)) { + ch -= caseDiff; + } + out.append(ch); + ch = Character.forDigit(ba[j] & 0xF, 16); + if (Character.isLetter(ch)) { + ch -= caseDiff; + } + out.append(ch); + } + buf.reset(); + } + } + + return out.toString(); + } + + + /** + * The class contains a utility method for converting from + * a MIME format called "<code>x-www-form-urlencoded</code>" + * to a <code>String</code> + * <p> + * To convert to a <code>String</code>, each character is examined in turn: + * <ul> + * <li>The ASCII characters '<code>a</code>' through '<code>z</code>', + * '<code>A</code>' through '<code>Z</code>', and '<code>0</code>' + * through '<code>9</code>' remain the same. + * <li>The plus sign '<code>+</code>'is converted into a + * space character '<code> </code>'. + * <li>The remaining characters are represented by 3-character + * strings which begin with the percent sign, + * "<code>%<i>xy</i></code>", where <i>xy</i> is the two-digit + * hexadecimal representation of the lower 8-bits of the character. + * </ul> + * + * @author Mark Chamness + * @author Michael McCloskey + * @since 1.2 + */ + + /** + * Decodes a "x-www-form-urlencoded" + * to a <tt>String</tt>. + * @param s the <code>String</code> to decode + * @return the newly decoded <code>String</code> + */ + static String decode(String s) { + if (s == null) + return null; + if (indexOfAny(s, "+%") == -1) + return s; // the common case + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + switch (c) { + case '+': + sb.append(' '); + break; + case '%': + try { + sb.append((char)Integer.parseInt( + s.substring(i+1,i+3),16)); + } catch (NumberFormatException e) { + throw new IllegalArgumentException( + "Illegal URL encoded value: " + + s.substring(i,i+3)); + } + i += 2; + break; + default: + sb.append(c); + break; + } + } + // Undo conversion to external encoding + String result = sb.toString(); + try { + byte[] inputBytes = result.getBytes("8859_1"); + result = new String(inputBytes); + } catch (UnsupportedEncodingException e) { + // The system should always have 8859_1 + } + return result; + } + + /** + * Return the first index of any of the characters in "any" in "s", + * or -1 if none are found. + * + * This should be a method on String. + */ + private static int indexOfAny(String s, String any) { + return indexOfAny(s, any, 0); + } + + private static int indexOfAny(String s, String any, int start) { + try { + int len = s.length(); + for (int i = start; i < len; i++) { + if (any.indexOf(s.charAt(i)) >= 0) + return i; + } + return -1; + } catch (StringIndexOutOfBoundsException e) { + return -1; + } + } + + /* + // Do not remove, this is needed when testing new URL cases + public static void main(String[] argv) { + String [] testURLNames = { + "protocol://userid:password@host:119/file", + "http://funny/folder/file.html", + "http://funny/folder/file.html#ref", + "http://funny/folder/file.html#", + "http://funny/#ref", + "imap://jmr:secret@labyrinth//var/mail/jmr", + "nntp://fred@labyrinth:143/save/it/now.mbox", + "imap://jmr@labyrinth/INBOX", + "imap://labryrinth", + "imap://labryrinth/", + "file:", + "file:INBOX", + "file:/home/shannon/mail/foo", + "/tmp/foo", + "//host/tmp/foo", + ":/tmp/foo", + "/really/weird:/tmp/foo#bar", + "" + }; + + URLName url = + new URLName("protocol", "host", 119, "file", "userid", "password"); + System.out.println("Test URL: " + url.toString()); + if (argv.length == 0) { + for (int i = 0; i < testURLNames.length; i++) { + print(testURLNames[i]); + System.out.println(); + } + } else { + for (int i = 0; i < argv.length; i++) { + print(argv[i]); + System.out.println(); + } + if (argv.length == 2) { + URLName u1 = new URLName(argv[0]); + URLName u2 = new URLName(argv[1]); + System.out.println("URL1 hash code: " + u1.hashCode()); + System.out.println("URL2 hash code: " + u2.hashCode()); + if (u1.equals(u2)) + System.out.println("success, equal"); + else + System.out.println("fail, not equal"); + if (u2.equals(u1)) + System.out.println("success, equal"); + else + System.out.println("fail, not equal"); + if (u1.hashCode() == u2.hashCode()) + System.out.println("success, hashCodes equal"); + else + System.out.println("fail, hashCodes not equal"); + } + } + } + + private static void print(String name) { + URLName url = new URLName(name); + System.out.println("Original URL: " + name); + System.out.println("The fullUrl : " + url.toString()); + if (!name.equals(url.toString())) + System.out.println(" : NOT EQUAL!"); + System.out.println("The protocol is: " + url.getProtocol()); + System.out.println("The host is: " + url.getHost()); + System.out.println("The port is: " + url.getPort()); + System.out.println("The user is: " + url.getUsername()); + System.out.println("The password is: " + url.getPassword()); + System.out.println("The file is: " + url.getFile()); + System.out.println("The ref is: " + url.getRef()); + } + */ +}
diff --git a/mail/src/main/java/javax/mail/event/ConnectionAdapter.java b/mail/src/main/java/javax/mail/event/ConnectionAdapter.java new file mode 100644 index 0000000..a07e151 --- /dev/null +++ b/mail/src/main/java/javax/mail/event/ConnectionAdapter.java
@@ -0,0 +1,34 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.event; + +/** + * The adapter which receives connection events. + * The methods in this class are empty; this class is provided as a + * convenience for easily creating listeners by extending this class + * and overriding only the methods of interest. + * + * @author John Mani + */ +public abstract class ConnectionAdapter implements ConnectionListener { + @Override + public void opened(ConnectionEvent e) {} + @Override + public void disconnected(ConnectionEvent e) {} + @Override + public void closed(ConnectionEvent e) {} +}
diff --git a/mail/src/main/java/javax/mail/event/ConnectionEvent.java b/mail/src/main/java/javax/mail/event/ConnectionEvent.java new file mode 100644 index 0000000..c89a003 --- /dev/null +++ b/mail/src/main/java/javax/mail/event/ConnectionEvent.java
@@ -0,0 +1,77 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.event; + +import java.util.*; +import javax.mail.*; + +/** + * This class models Connection events. + * + * @author John Mani + */ + +public class ConnectionEvent extends MailEvent { + + /** A connection was opened. */ + public static final int OPENED = 1; + /** A connection was disconnected (not currently used). */ + public static final int DISCONNECTED = 2; + /** A connection was closed. */ + public static final int CLOSED = 3; + + /** + * The event type. + * + * @serial + */ + protected int type; + + private static final long serialVersionUID = -1855480171284792957L; + + /** + * Construct a ConnectionEvent. + * + * @param source The source object + * @param type the event type + */ + public ConnectionEvent(Object source, int type) { + super(source); + this.type = type; + } + + /** + * Return the type of this event + * @return type + */ + public int getType() { + return type; + } + + /** + * Invokes the appropriate ConnectionListener method + */ + @Override + public void dispatch(Object listener) { + if (type == OPENED) + ((ConnectionListener)listener).opened(this); + else if (type == DISCONNECTED) + ((ConnectionListener)listener).disconnected(this); + else if (type == CLOSED) + ((ConnectionListener)listener).closed(this); + } +}
diff --git a/mail/src/main/java/javax/mail/event/ConnectionListener.java b/mail/src/main/java/javax/mail/event/ConnectionListener.java new file mode 100644 index 0000000..64721df --- /dev/null +++ b/mail/src/main/java/javax/mail/event/ConnectionListener.java
@@ -0,0 +1,50 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.event; + +import java.util.*; + +/** + * This is the Listener interface for Connection events. + * + * @author John Mani + */ + +public interface ConnectionListener extends java.util.EventListener { + + /** + * Invoked when a Store/Folder/Transport is opened. + * + * @param e the ConnectionEvent + */ + public void opened(ConnectionEvent e); + + /** + * Invoked when a Store is disconnected. Note that a folder + * cannot be disconnected, so a folder will not fire this event + * + * @param e the ConnectionEvent + */ + public void disconnected(ConnectionEvent e); + + /** + * Invoked when a Store/Folder/Transport is closed. + * + * @param e the ConnectionEvent + */ + public void closed(ConnectionEvent e); +}
diff --git a/mail/src/main/java/javax/mail/event/FolderAdapter.java b/mail/src/main/java/javax/mail/event/FolderAdapter.java new file mode 100644 index 0000000..7bdd009 --- /dev/null +++ b/mail/src/main/java/javax/mail/event/FolderAdapter.java
@@ -0,0 +1,34 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.event; + +/** + * The adapter which receives Folder events. + * The methods in this class are empty; this class is provided as a + * convenience for easily creating listeners by extending this class + * and overriding only the methods of interest. + * + * @author John Mani + */ +public abstract class FolderAdapter implements FolderListener { + @Override + public void folderCreated(FolderEvent e) {} + @Override + public void folderRenamed(FolderEvent e) {} + @Override + public void folderDeleted(FolderEvent e) {} +}
diff --git a/mail/src/main/java/javax/mail/event/FolderEvent.java b/mail/src/main/java/javax/mail/event/FolderEvent.java new file mode 100644 index 0000000..4c89ae6 --- /dev/null +++ b/mail/src/main/java/javax/mail/event/FolderEvent.java
@@ -0,0 +1,144 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.event; + +import java.util.*; +import javax.mail.*; + +/** + * This class models Folder <em>existence</em> events. FolderEvents are + * delivered to FolderListeners registered on the affected Folder as + * well as the containing Store. <p> + * + * Service providers vary widely in their ability to notify clients of + * these events. At a minimum, service providers must notify listeners + * registered on the same Store or Folder object on which the operation + * occurs. Service providers may also notify listeners when changes + * are made through operations on other objects in the same virtual + * machine, or by other clients in the same or other hosts. Such + * notifications are not required and are typically not supported + * by mail protocols (including IMAP). + * + * @author John Mani + * @author Bill Shannon + */ + +public class FolderEvent extends MailEvent { + + /** The folder was created. */ + public static final int CREATED = 1; + /** The folder was deleted. */ + public static final int DELETED = 2; + /** The folder was renamed. */ + public static final int RENAMED = 3; + + /** + * The event type. + * + * @serial + */ + protected int type; + + /** + * The folder the event occurred on. + */ + transient protected Folder folder; + + /** + * The folder that represents the new name, in case of a RENAMED event. + * + * @since JavaMail 1.1 + */ + transient protected Folder newFolder; + + private static final long serialVersionUID = 5278131310563694307L; + + /** + * Constructor. <p> + * + * @param source The source of the event + * @param folder The affected folder + * @param type The event type + */ + public FolderEvent(Object source, Folder folder, int type) { + this(source, folder, folder, type); + } + + /** + * Constructor. Use for RENAMED events. + * + * @param source The source of the event + * @param oldFolder The folder that is renamed + * @param newFolder The folder that represents the new name + * @param type The event type + * @since JavaMail 1.1 + */ + public FolderEvent(Object source, Folder oldFolder, + Folder newFolder, int type) { + super(source); + this.folder = oldFolder; + this.newFolder = newFolder; + this.type = type; + } + + /** + * Return the type of this event. + * + * @return type + */ + public int getType() { + return type; + } + + /** + * Return the affected folder. + * + * @return the affected folder + * @see #getNewFolder + */ + public Folder getFolder() { + return folder; + } + + /** + * If this event indicates that a folder is renamed, (i.e, the event type + * is RENAMED), then this method returns the Folder object representing the + * new name. <p> + * + * The <code>getFolder()</code> method returns the folder that is renamed. + * + * @return Folder representing the new name. + * @see #getFolder + * @since JavaMail 1.1 + */ + public Folder getNewFolder() { + return newFolder; + } + + /** + * Invokes the appropriate FolderListener method + */ + @Override + public void dispatch(Object listener) { + if (type == CREATED) + ((FolderListener)listener).folderCreated(this); + else if (type == DELETED) + ((FolderListener)listener).folderDeleted(this); + else if (type == RENAMED) + ((FolderListener)listener).folderRenamed(this); + } +}
diff --git a/mail/src/main/java/javax/mail/event/FolderListener.java b/mail/src/main/java/javax/mail/event/FolderListener.java new file mode 100644 index 0000000..5c652a3 --- /dev/null +++ b/mail/src/main/java/javax/mail/event/FolderListener.java
@@ -0,0 +1,48 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.event; + +import java.util.*; + +/** + * This is the Listener interface for Folder events. + * + * @author John Mani + */ + +public interface FolderListener extends java.util.EventListener { + /** + * Invoked when a Folder is created. + * + * @param e the FolderEvent + */ + public void folderCreated(FolderEvent e); + + /** + * Invoked when a folder is deleted. + * + * @param e the FolderEvent + */ + public void folderDeleted(FolderEvent e); + + /** + * Invoked when a folder is renamed. + * + * @param e the FolderEvent + */ + public void folderRenamed(FolderEvent e); +}
diff --git a/mail/src/main/java/javax/mail/event/MailEvent.java b/mail/src/main/java/javax/mail/event/MailEvent.java new file mode 100644 index 0000000..97e4191 --- /dev/null +++ b/mail/src/main/java/javax/mail/event/MailEvent.java
@@ -0,0 +1,46 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.event; + +import java.util.EventObject; + +/** + * Common base class for mail events, defining the dispatch method. + * + * @author Bill Shannon + */ + +public abstract class MailEvent extends EventObject { + private static final long serialVersionUID = 1846275636325456631L; + + /** + * Construct a MailEvent referring to the given source. + * + * @param source the source of the event + */ + public MailEvent(Object source) { + super(source); + } + + /** + * This method invokes the appropriate method on a listener for + * this event. Subclasses provide the implementation. + * + * @param listener the listener to invoke on + */ + public abstract void dispatch(Object listener); +}
diff --git a/mail/src/main/java/javax/mail/event/MessageChangedEvent.java b/mail/src/main/java/javax/mail/event/MessageChangedEvent.java new file mode 100644 index 0000000..0099592 --- /dev/null +++ b/mail/src/main/java/javax/mail/event/MessageChangedEvent.java
@@ -0,0 +1,84 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.event; + +import java.util.*; +import javax.mail.*; + +/** + * This class models Message change events. + * + * @author John Mani + */ + +public class MessageChangedEvent extends MailEvent { + + /** The message's flags changed. */ + public static final int FLAGS_CHANGED = 1; + /** The message's envelope (headers, but not body) changed. */ + public static final int ENVELOPE_CHANGED = 2; + + /** + * The event type. + * + * @serial + */ + protected int type; + + /** + * The message that changed. + */ + transient protected Message msg; + + private static final long serialVersionUID = -4974972972105535108L; + + /** + * Constructor. + * @param source The folder that owns the message + * @param type The change type + * @param msg The changed message + */ + public MessageChangedEvent(Object source, int type, Message msg) { + super(source); + this.msg = msg; + this.type = type; + } + + /** + * Return the type of this event. + * @return type + */ + public int getMessageChangeType() { + return type; + } + + /** + * Return the changed Message. + * @return the message + */ + public Message getMessage() { + return msg; + } + + /** + * Invokes the appropriate MessageChangedListener method. + */ + @Override + public void dispatch(Object listener) { + ((MessageChangedListener)listener).messageChanged(this); + } +}
diff --git a/mail/src/main/java/javax/mail/event/MessageChangedListener.java b/mail/src/main/java/javax/mail/event/MessageChangedListener.java new file mode 100644 index 0000000..c4eac48 --- /dev/null +++ b/mail/src/main/java/javax/mail/event/MessageChangedListener.java
@@ -0,0 +1,37 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.event; + +import java.util.*; + +/** + * This is the Listener interface for MessageChanged events + * + * @author John Mani + */ + +public interface MessageChangedListener extends java.util.EventListener { + /** + * Invoked when a message is changed. The change-type specifies + * what changed. + * + * @param e the MessageChangedEvent + * @see MessageChangedEvent#FLAGS_CHANGED + * @see MessageChangedEvent#ENVELOPE_CHANGED + */ + public void messageChanged(MessageChangedEvent e); +}
diff --git a/mail/src/main/java/javax/mail/event/MessageCountAdapter.java b/mail/src/main/java/javax/mail/event/MessageCountAdapter.java new file mode 100644 index 0000000..694069f --- /dev/null +++ b/mail/src/main/java/javax/mail/event/MessageCountAdapter.java
@@ -0,0 +1,32 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.event; + +/** + * The adapter which receives MessageCount events. + * The methods in this class are empty; this class is provided as a + * convenience for easily creating listeners by extending this class + * and overriding only the methods of interest. + * + * @author John Mani + */ +public abstract class MessageCountAdapter implements MessageCountListener { + @Override + public void messagesAdded(MessageCountEvent e) {} + @Override + public void messagesRemoved(MessageCountEvent e) {} +}
diff --git a/mail/src/main/java/javax/mail/event/MessageCountEvent.java b/mail/src/main/java/javax/mail/event/MessageCountEvent.java new file mode 100644 index 0000000..24d38a8 --- /dev/null +++ b/mail/src/main/java/javax/mail/event/MessageCountEvent.java
@@ -0,0 +1,134 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.event; + +import java.util.*; +import javax.mail.*; + +/** + * This class notifies changes in the number of messages in a folder. <p> + * + * Note that some folder types may only deliver MessageCountEvents at + * certain times or after certain operations. IMAP in particular will + * only notify the client of MessageCountEvents when a client issues a + * new command. Refer to + * <A HREF="http://www.ietf.org/rfc/rfc3501.txt" TARGET="_top">RFC 3501</A> + * for details. + * A client may want to "poll" the folder by occasionally calling the + * {@link javax.mail.Folder#getMessageCount getMessageCount} or + * {@link javax.mail.Folder#isOpen isOpen} methods + * to solicit any such notifications. + * + * @author John Mani + */ + +public class MessageCountEvent extends MailEvent { + + /** The messages were added to their folder */ + public static final int ADDED = 1; + /** The messages were removed from their folder */ + public static final int REMOVED = 2; + + /** + * The event type. + * + * @serial + */ + protected int type; + + /** + * If true, this event is the result of an explicit + * expunge by this client, and the messages in this + * folder have been renumbered to account for this. + * If false, this event is the result of an expunge + * by external sources. + * + * @serial + */ + protected boolean removed; + + /** + * The messages. + */ + transient protected Message[] msgs; + + private static final long serialVersionUID = -7447022340837897369L; + + /** + * Constructor. + * @param folder The containing folder + * @param type The event type + * @param removed If true, this event is the result of an explicit + * expunge by this client, and the messages in this + * folder have been renumbered to account for this. + * If false, this event is the result of an expunge + * by external sources. + * + * @param msgs The messages added/removed + */ + public MessageCountEvent(Folder folder, int type, + boolean removed, Message[] msgs) { + super(folder); + this.type = type; + this.removed = removed; + this.msgs = msgs; + } + + /** + * Return the type of this event. + * @return type + */ + public int getType() { + return type; + } + + /** + * Indicates whether this event is the result of an explicit + * expunge by this client, or due to an expunge from external + * sources. If <code>true</code>, this event is due to an + * explicit expunge and hence all remaining messages in this + * folder have been renumbered. If <code>false</code>, this event + * is due to an external expunge. <p> + * + * Note that this method is valid only if the type of this event + * is <code>REMOVED</code> + * + * @return true if the message has been removed + */ + public boolean isRemoved() { + return removed; + } + + /** + * Return the array of messages added or removed. + * @return array of messages + */ + public Message[] getMessages() { + return msgs; + } + + /** + * Invokes the appropriate MessageCountListener method. + */ + @Override + public void dispatch(Object listener) { + if (type == ADDED) + ((MessageCountListener)listener).messagesAdded(this); + else // REMOVED + ((MessageCountListener)listener).messagesRemoved(this); + } +}
diff --git a/mail/src/main/java/javax/mail/event/MessageCountListener.java b/mail/src/main/java/javax/mail/event/MessageCountListener.java new file mode 100644 index 0000000..5793fe0 --- /dev/null +++ b/mail/src/main/java/javax/mail/event/MessageCountListener.java
@@ -0,0 +1,41 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.event; + +import java.util.*; + +/** + * This is the Listener interface for MessageCount events. + * + * @author John Mani + */ + +public interface MessageCountListener extends java.util.EventListener { + /** + * Invoked when messages are added into a folder. + * + * @param e the MessageCountEvent + */ + public void messagesAdded(MessageCountEvent e); + + /** + * Invoked when messages are removed (expunged) from a folder. + * + * @param e the MessageCountEvent + */ + public void messagesRemoved(MessageCountEvent e); +}
diff --git a/mail/src/main/java/javax/mail/event/StoreEvent.java b/mail/src/main/java/javax/mail/event/StoreEvent.java new file mode 100644 index 0000000..b433283 --- /dev/null +++ b/mail/src/main/java/javax/mail/event/StoreEvent.java
@@ -0,0 +1,99 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.event; + +import java.util.*; +import javax.mail.*; + +/** + * This class models notifications from the Store connection. These + * notifications can be ALERTS or NOTICES. ALERTS must be presented + * to the user in a fashion that calls the user's attention to the + * message. + * + * @author John Mani + */ + +public class StoreEvent extends MailEvent { + + /** + * Indicates that this message is an ALERT. + */ + public static final int ALERT = 1; + + /** + * Indicates that this message is a NOTICE. + */ + public static final int NOTICE = 2; + + /** + * The event type. + * + * @serial + */ + protected int type; + + /** + * The message text to be presented to the user. + * + * @serial + */ + protected String message; + + private static final long serialVersionUID = 1938704919992515330L; + + /** + * Construct a StoreEvent. + * + * @param store the source Store + * @param type the event type + * @param message a message assoicated with the event + */ + public StoreEvent(Store store, int type, String message) { + super(store); + this.type = type; + this.message = message; + } + + /** + * Return the type of this event. + * + * @return type + * @see #ALERT + * @see #NOTICE + */ + public int getMessageType() { + return type; + } + + /** + * Get the message from the Store. + * + * @return message from the Store + */ + public String getMessage() { + return message; + } + + /** + * Invokes the appropriate StoreListener method. + */ + @Override + public void dispatch(Object listener) { + ((StoreListener)listener).notification(this); + } +}
diff --git a/mail/src/main/java/javax/mail/event/StoreListener.java b/mail/src/main/java/javax/mail/event/StoreListener.java new file mode 100644 index 0000000..3c00ab8 --- /dev/null +++ b/mail/src/main/java/javax/mail/event/StoreListener.java
@@ -0,0 +1,37 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.event; + +import java.util.*; + +/** + * This is the Listener interface for Store Notifications. + * + * @author John Mani + */ + +public interface StoreListener extends java.util.EventListener { + + /** + * Invoked when the Store generates a notification event. + * + * @param e the StoreEvent + * @see StoreEvent#ALERT + * @see StoreEvent#NOTICE + */ + public void notification(StoreEvent e); +}
diff --git a/mail/src/main/java/javax/mail/event/TransportAdapter.java b/mail/src/main/java/javax/mail/event/TransportAdapter.java new file mode 100644 index 0000000..798cef7 --- /dev/null +++ b/mail/src/main/java/javax/mail/event/TransportAdapter.java
@@ -0,0 +1,34 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.event; + +/** + * The adapter which receives Transport events. + * The methods in this class are empty; this class is provided as a + * convenience for easily creating listeners by extending this class + * and overriding only the methods of interest. + * + * @author John Mani + */ +public abstract class TransportAdapter implements TransportListener { + @Override + public void messageDelivered(TransportEvent e) {} + @Override + public void messageNotDelivered(TransportEvent e) {} + @Override + public void messagePartiallyDelivered(TransportEvent e) {} +}
diff --git a/mail/src/main/java/javax/mail/event/TransportEvent.java b/mail/src/main/java/javax/mail/event/TransportEvent.java new file mode 100644 index 0000000..2f754ca --- /dev/null +++ b/mail/src/main/java/javax/mail/event/TransportEvent.java
@@ -0,0 +1,154 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.event; + +import java.util.*; +import javax.mail.*; + +/** + * This class models Transport events. + * + * @author John Mani + * @author Max Spivak + * + * @see javax.mail.Transport + * @see javax.mail.event.TransportListener + */ + +public class TransportEvent extends MailEvent { + + /** + * Message has been successfully delivered to all recipients by the + * transport firing this event. validSent[] contains all the addresses + * this transport sent to successfully. validUnsent[] and invalid[] + * should be null, + */ + public static final int MESSAGE_DELIVERED = 1; + + /** + * Message was not sent for some reason. validSent[] should be null. + * validUnsent[] may have addresses that are valid (but the message + * wasn't sent to them). invalid[] should likely contain invalid addresses. + */ + public static final int MESSAGE_NOT_DELIVERED = 2; + + /** + * Message was successfully sent to some recipients but not to all. + * validSent[] holds addresses of recipients to whom the message was sent. + * validUnsent[] holds valid addresses to which the message was not sent. + * invalid[] holds invalid addresses, if any. + */ + public static final int MESSAGE_PARTIALLY_DELIVERED = 3; + + + /** + * The event type. + * + * @serial + */ + protected int type; + + /** The valid address to which the message was sent. */ + transient protected Address[] validSent; + /** The valid address to which the message was not sent. */ + transient protected Address[] validUnsent; + /** The invalid addresses. */ + transient protected Address[] invalid; + /** The Message to which this event applies. */ + transient protected Message msg; + + private static final long serialVersionUID = -4729852364684273073L; + + /** + * Constructor. + * + * @param transport The Transport object + * @param type the event type (MESSAGE_DELIVERED, etc.) + * @param validSent the valid addresses to which the message was sent + * @param validUnsent the valid addresses to which the message was + * not sent + * @param invalid the invalid addresses + * @param msg the message being sent + */ + public TransportEvent(Transport transport, int type, Address[] validSent, + Address[] validUnsent, Address[] invalid, + Message msg) { + super(transport); + this.type = type; + this.validSent = validSent; + this.validUnsent = validUnsent; + this.invalid = invalid; + this.msg = msg; + } + + /** + * Return the type of this event. + * @return type + */ + public int getType() { + return type; + } + + /** + * Return the addresses to which this message was sent succesfully. + * @return Addresses to which the message was sent successfully or null + */ + public Address[] getValidSentAddresses() { + return validSent; + } + + /** + * Return the addresses that are valid but to which this message + * was not sent. + * @return Addresses that are valid but to which the message was + * not sent successfully or null + */ + public Address[] getValidUnsentAddresses() { + return validUnsent; + } + + /** + * Return the addresses to which this message could not be sent. + * @return Addresses to which the message sending failed or null + */ + public Address[] getInvalidAddresses() { + return invalid; + } + + /** + * Get the Message object associated with this Transport Event. + * + * @return the Message object + * @since JavaMail 1.2 + */ + public Message getMessage() { + return msg; + } + + /** + * Invokes the appropriate TransportListener method. + */ + @Override + public void dispatch(Object listener) { + if (type == MESSAGE_DELIVERED) + ((TransportListener)listener).messageDelivered(this); + else if (type == MESSAGE_NOT_DELIVERED) + ((TransportListener)listener).messageNotDelivered(this); + else // MESSAGE_PARTIALLY_DELIVERED + ((TransportListener)listener).messagePartiallyDelivered(this); + } +}
diff --git a/mail/src/main/java/javax/mail/event/TransportListener.java b/mail/src/main/java/javax/mail/event/TransportListener.java new file mode 100644 index 0000000..3fc030d --- /dev/null +++ b/mail/src/main/java/javax/mail/event/TransportListener.java
@@ -0,0 +1,52 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.event; + +import java.util.*; + +/** + * This is the Listener interface for Transport events + * + * @author John Mani + * @author Max Spivak + * + * @see javax.mail.Transport + * @see javax.mail.event.TransportEvent + */ + +public interface TransportListener extends java.util.EventListener { + + /** + * Invoked when a Message is succesfully delivered. + * @param e TransportEvent + */ + public void messageDelivered(TransportEvent e); + + /** + * Invoked when a Message is not delivered. + * @param e TransportEvent + * @see TransportEvent + */ + public void messageNotDelivered(TransportEvent e); + + /** + * Invoked when a Message is partially delivered. + * @param e TransportEvent + * @see TransportEvent + */ + public void messagePartiallyDelivered(TransportEvent e); +}
diff --git a/mail/src/main/java/javax/mail/event/package.html b/mail/src/main/java/javax/mail/event/package.html new file mode 100644 index 0000000..d22c00b --- /dev/null +++ b/mail/src/main/java/javax/mail/event/package.html
@@ -0,0 +1,31 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> +<HTML> +<HEAD> +<meta http-equiv="Content-Type" content="text/html; charset=utf-8"> +<!-- + + Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +</HEAD> +<BODY BGCOLOR="white"> + +Listeners and events for the JavaMail API. +This package defines listener classes and event classes used by the classes +defined in the <code>javax.mail</code> package. + +</BODY> +</HTML>
diff --git a/mail/src/main/java/javax/mail/internet/AddressException.java b/mail/src/main/java/javax/mail/internet/AddressException.java new file mode 100644 index 0000000..b91e1ce --- /dev/null +++ b/mail/src/main/java/javax/mail/internet/AddressException.java
@@ -0,0 +1,114 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +/** + * The exception thrown when a wrongly formatted address is encountered. + * + * @author Bill Shannon + * @author Max Spivak + */ + +public class AddressException extends ParseException { + /** + * The string being parsed. + * + * @serial + */ + protected String ref = null; + + /** + * The index in the string where the error occurred, or -1 if not known. + * + * @serial + */ + protected int pos = -1; + + private static final long serialVersionUID = 9134583443539323120L; + + /** + * Constructs an AddressException with no detail message. + */ + public AddressException() { + super(); + } + + /** + * Constructs an AddressException with the specified detail message. + * @param s the detail message + */ + public AddressException(String s) { + super(s); + } + + /** + * Constructs an AddressException with the specified detail message + * and reference info. + * + * @param s the detail message + * @param ref the string being parsed + */ + public AddressException(String s, String ref) { + super(s); + this.ref = ref; + } + + /** + * Constructs an AddressException with the specified detail message + * and reference info. + * + * @param s the detail message + * @param ref the string being parsed + * @param pos the position of the error + */ + public AddressException(String s, String ref, int pos) { + super(s); + this.ref = ref; + this.pos = pos; + } + + /** + * Get the string that was being parsed when the error was detected + * (null if not relevant). + * + * @return the string that was being parsed + */ + public String getRef() { + return ref; + } + + /** + * Get the position with the reference string where the error was + * detected (-1 if not relevant). + * + * @return the position within the string of the error + */ + public int getPos() { + return pos; + } + + @Override + public String toString() { + String s = super.toString(); + if (ref == null) + return s; + s += " in string ``" + ref + "''"; + if (pos < 0) + return s; + return s + " at position " + pos; + } +}
diff --git a/mail/src/main/java/javax/mail/internet/ContentDisposition.java b/mail/src/main/java/javax/mail/internet/ContentDisposition.java new file mode 100644 index 0000000..123a1ef --- /dev/null +++ b/mail/src/main/java/javax/mail/internet/ContentDisposition.java
@@ -0,0 +1,186 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import javax.mail.*; +import java.util.*; +import java.io.*; +import com.sun.mail.util.PropUtil; + +/** + * This class represents a MIME ContentDisposition value. It provides + * methods to parse a ContentDisposition string into individual components + * and to generate a MIME style ContentDisposition string. + * + * @author John Mani + */ + +public class ContentDisposition { + + private static final boolean contentDispositionStrict = + PropUtil.getBooleanSystemProperty("mail.mime.contentdisposition.strict", true); + + private String disposition; // disposition + private ParameterList list; // parameter list + + /** + * No-arg Constructor. + */ + public ContentDisposition() { } + + /** + * Constructor. + * + * @param disposition disposition + * @param list ParameterList + * @since JavaMail 1.2 + */ + public ContentDisposition(String disposition, ParameterList list) { + this.disposition = disposition; + this.list = list; + } + + /** + * Constructor that takes a ContentDisposition string. The String + * is parsed into its constituents: dispostion and parameters. + * A ParseException is thrown if the parse fails. + * + * @param s the ContentDisposition string. + * @exception ParseException if the parse fails. + * @since JavaMail 1.2 + */ + public ContentDisposition(String s) throws ParseException { + HeaderTokenizer h = new HeaderTokenizer(s, HeaderTokenizer.MIME); + HeaderTokenizer.Token tk; + + // First "disposition" .. + tk = h.next(); + if (tk.getType() != HeaderTokenizer.Token.ATOM) { + if (contentDispositionStrict) { + throw new ParseException("Expected disposition, got " + + tk.getValue()); + } + } else { + disposition = tk.getValue(); + } + + // Then parameters .. + String rem = h.getRemainder(); + if (rem != null) { + try { + list = new ParameterList(rem); + } catch (ParseException px) { + if (contentDispositionStrict) { + throw px; + } + } + } + } + + /** + * Return the disposition value. + * @return the disposition + * @since JavaMail 1.2 + */ + public String getDisposition() { + return disposition; + } + + /** + * Return the specified parameter value. Returns <code>null</code> + * if this parameter is absent. + * + * @param name the parameter name + * @return parameter value + * @since JavaMail 1.2 + */ + public String getParameter(String name) { + if (list == null) + return null; + + return list.get(name); + } + + /** + * Return a ParameterList object that holds all the available + * parameters. Returns null if no parameters are available. + * + * @return ParameterList + * @since JavaMail 1.2 + */ + public ParameterList getParameterList() { + return list; + } + + /** + * Set the disposition. Replaces the existing disposition. + * @param disposition the disposition + * @since JavaMail 1.2 + */ + public void setDisposition(String disposition) { + this.disposition = disposition; + } + + /** + * Set the specified parameter. If this parameter already exists, + * it is replaced by this new value. + * + * @param name parameter name + * @param value parameter value + * @since JavaMail 1.2 + */ + public void setParameter(String name, String value) { + if (list == null) + list = new ParameterList(); + + list.set(name, value); + } + + /** + * Set a new ParameterList. + * @param list ParameterList + * @since JavaMail 1.2 + */ + public void setParameterList(ParameterList list) { + this.list = list; + } + + /** + * Retrieve a RFC2045 style string representation of + * this ContentDisposition. Returns an empty string if + * the conversion failed. + * + * @return RFC2045 style string + * @since JavaMail 1.2 + */ + @Override + public String toString() { + if (disposition == null) + return ""; + + if (list == null) + return disposition; + + StringBuilder sb = new StringBuilder(disposition); + + // append the parameter list + // use the length of the string buffer + the length of + // the header name formatted as follows "Content-Disposition: " + sb.append(list.toString(sb.length() + 21)); + return sb.toString(); + } +}
diff --git a/mail/src/main/java/javax/mail/internet/ContentType.java b/mail/src/main/java/javax/mail/internet/ContentType.java new file mode 100644 index 0000000..2005d2f --- /dev/null +++ b/mail/src/main/java/javax/mail/internet/ContentType.java
@@ -0,0 +1,274 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import javax.mail.*; +import java.util.*; +import java.io.*; + +/** + * This class represents a MIME Content-Type value. It provides + * methods to parse a Content-Type string into individual components + * and to generate a MIME style Content-Type string. + * + * @author John Mani + */ + +public class ContentType { + + private String primaryType; // primary type + private String subType; // subtype + private ParameterList list; // parameter list + + /** + * No-arg Constructor. + */ + public ContentType() { } + + /** + * Constructor. + * + * @param primaryType primary type + * @param subType subType + * @param list ParameterList + */ + public ContentType(String primaryType, String subType, + ParameterList list) { + this.primaryType = primaryType; + this.subType = subType; + this.list = list; + } + + /** + * Constructor that takes a Content-Type string. The String + * is parsed into its constituents: primaryType, subType + * and parameters. A ParseException is thrown if the parse fails. + * + * @param s the Content-Type string. + * @exception ParseException if the parse fails. + */ + public ContentType(String s) throws ParseException { + HeaderTokenizer h = new HeaderTokenizer(s, HeaderTokenizer.MIME); + HeaderTokenizer.Token tk; + + // First "type" .. + tk = h.next(); + if (tk.getType() != HeaderTokenizer.Token.ATOM) + throw new ParseException("In Content-Type string <" + s + ">" + + ", expected MIME type, got " + + tk.getValue()); + primaryType = tk.getValue(); + + // The '/' separator .. + tk = h.next(); + if ((char)tk.getType() != '/') + throw new ParseException("In Content-Type string <" + s + ">" + + ", expected '/', got " + tk.getValue()); + + // Then "subType" .. + tk = h.next(); + if (tk.getType() != HeaderTokenizer.Token.ATOM) + throw new ParseException("In Content-Type string <" + s + ">" + + ", expected MIME subtype, got " + + tk.getValue()); + subType = tk.getValue(); + + // Finally parameters .. + String rem = h.getRemainder(); + if (rem != null) + list = new ParameterList(rem); + } + + /** + * Return the primary type. + * @return the primary type + */ + public String getPrimaryType() { + return primaryType; + } + + /** + * Return the subType. + * @return the subType + */ + public String getSubType() { + return subType; + } + + /** + * Return the MIME type string, without the parameters. + * The returned value is basically the concatenation of + * the primaryType, the '/' character and the secondaryType. + * + * @return the type + */ + public String getBaseType() { + if (primaryType == null || subType == null) + return ""; + return primaryType + '/' + subType; + } + + /** + * Return the specified parameter value. Returns <code>null</code> + * if this parameter is absent. + * + * @param name the parameter name + * @return parameter value + */ + public String getParameter(String name) { + if (list == null) + return null; + + return list.get(name); + } + + /** + * Return a ParameterList object that holds all the available + * parameters. Returns null if no parameters are available. + * + * @return ParameterList + */ + public ParameterList getParameterList() { + return list; + } + + /** + * Set the primary type. Overrides existing primary type. + * @param primaryType primary type + */ + public void setPrimaryType(String primaryType) { + this.primaryType = primaryType; + } + + /** + * Set the subType. Replaces the existing subType. + * @param subType the subType + */ + public void setSubType(String subType) { + this.subType = subType; + } + + /** + * Set the specified parameter. If this parameter already exists, + * it is replaced by this new value. + * + * @param name parameter name + * @param value parameter value + */ + public void setParameter(String name, String value) { + if (list == null) + list = new ParameterList(); + + list.set(name, value); + } + + /** + * Set a new ParameterList. + * @param list ParameterList + */ + public void setParameterList(ParameterList list) { + this.list = list; + } + + /** + * Retrieve a RFC2045 style string representation of + * this Content-Type. Returns an empty string if + * the conversion failed. + * + * @return RFC2045 style string + */ + @Override + public String toString() { + if (primaryType == null || subType == null) // need both + return ""; + + StringBuilder sb = new StringBuilder(); + sb.append(primaryType).append('/').append(subType); + if (list != null) + // append the parameter list + // use the length of the string buffer + the length of + // the header name formatted as follows "Content-Type: " + sb.append(list.toString(sb.length() + 14)); + + return sb.toString(); + } + + /** + * Match with the specified ContentType object. This method + * compares <strong>only the <code>primaryType</code> and + * <code>subType</code> </strong>. The parameters of both operands + * are ignored. <p> + * + * For example, this method will return <code>true</code> when + * comparing the ContentTypes for <strong>"text/plain"</strong> + * and <strong>"text/plain; charset=foobar"</strong>. + * + * If the <code>subType</code> of either operand is the special + * character '*', then the subtype is ignored during the match. + * For example, this method will return <code>true</code> when + * comparing the ContentTypes for <strong>"text/plain"</strong> + * and <strong>"text/*" </strong> + * + * @param cType ContentType to compare this against + * @return true if it matches + */ + public boolean match(ContentType cType) { + // Match primaryType + if (!((primaryType == null && cType.getPrimaryType() == null) || + (primaryType != null && + primaryType.equalsIgnoreCase(cType.getPrimaryType())))) + return false; + + String sType = cType.getSubType(); + + // If either one of the subTypes is wildcarded, return true + if ((subType != null && subType.startsWith("*")) || + (sType != null && sType.startsWith("*"))) + return true; + + // Match subType + return (subType == null && sType == null) || + (subType != null && subType.equalsIgnoreCase(sType)); + } + + /** + * Match with the specified content-type string. This method + * compares <strong>only the <code>primaryType</code> and + * <code>subType</code> </strong>. + * The parameters of both operands are ignored. <p> + * + * For example, this method will return <code>true</code> when + * comparing the ContentType for <strong>"text/plain"</strong> + * with <strong>"text/plain; charset=foobar"</strong>. + * + * If the <code>subType</code> of either operand is the special + * character '*', then the subtype is ignored during the match. + * For example, this method will return <code>true</code> when + * comparing the ContentType for <strong>"text/plain"</strong> + * with <strong>"text/*" </strong> + * + * @param s the content-type string to match + * @return true if it matches + */ + public boolean match(String s) { + try { + return match(new ContentType(s)); + } catch (ParseException pex) { + return false; + } + } +}
diff --git a/mail/src/main/java/javax/mail/internet/HeaderTokenizer.java b/mail/src/main/java/javax/mail/internet/HeaderTokenizer.java new file mode 100644 index 0000000..e5bafe3 --- /dev/null +++ b/mail/src/main/java/javax/mail/internet/HeaderTokenizer.java
@@ -0,0 +1,471 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import java.util.*; + +/** + * This class tokenizes RFC822 and MIME headers into the basic + * symbols specified by RFC822 and MIME. <p> + * + * This class handles folded headers (ie headers with embedded + * CRLF SPACE sequences). The folds are removed in the returned + * tokens. + * + * @author John Mani + * @author Bill Shannon + */ + +public class HeaderTokenizer { + + /** + * The Token class represents tokens returned by the + * HeaderTokenizer. + */ + public static class Token { + + private int type; + private String value; + + /** + * Token type indicating an ATOM. + */ + public static final int ATOM = -1; + + /** + * Token type indicating a quoted string. The value + * field contains the string without the quotes. + */ + public static final int QUOTEDSTRING = -2; + + /** + * Token type indicating a comment. The value field + * contains the comment string without the comment + * start and end symbols. + */ + public static final int COMMENT = -3; + + /** + * Token type indicating end of input. + */ + public static final int EOF = -4; + + /** + * Constructor. + * @param type Token type + * @param value Token value + */ + public Token(int type, String value) { + this.type = type; + this.value = value; + } + + /** + * Return the type of the token. If the token represents a + * delimiter or a control character, the type is that character + * itself, converted to an integer. Otherwise, it's value is + * one of the following: + * <ul> + * <li><code>ATOM</code> A sequence of ASCII characters + * delimited by either SPACE, CTL, "(", <"> or the + * specified SPECIALS + * <li><code>QUOTEDSTRING</code> A sequence of ASCII characters + * within quotes + * <li><code>COMMENT</code> A sequence of ASCII characters + * within "(" and ")". + * <li><code>EOF</code> End of header + * </ul> + * + * @return the token type + */ + public int getType() { + return type; + } + + /** + * Returns the value of the token just read. When the current + * token is a quoted string, this field contains the body of the + * string, without the quotes. When the current token is a comment, + * this field contains the body of the comment. + * + * @return token value + */ + public String getValue() { + return value; + } + } + + private String string; // the string to be tokenized + private boolean skipComments; // should comments be skipped ? + private String delimiters; // delimiter string + private int currentPos; // current parse position + private int maxPos; // string length + private int nextPos; // track start of next Token for next() + private int peekPos; // track start of next Token for peek() + + /** + * RFC822 specials + */ + public final static String RFC822 = "()<>@,;:\\\"\t .[]"; + + /** + * MIME specials + */ + public final static String MIME = "()<>@,;:\\\"\t []/?="; + + // The EOF Token + private final static Token EOFToken = new Token(Token.EOF, null); + + /** + * Constructor that takes a rfc822 style header. + * + * @param header The rfc822 header to be tokenized + * @param delimiters Set of delimiter characters + * to be used to delimit ATOMS. These + * are usually <code>RFC822</code> or + * <code>MIME</code> + * @param skipComments If true, comments are skipped and + * not returned as tokens + */ + public HeaderTokenizer(String header, String delimiters, + boolean skipComments) { + string = (header == null) ? "" : header; // paranoia ?! + this.skipComments = skipComments; + this.delimiters = delimiters; + currentPos = nextPos = peekPos = 0; + maxPos = string.length(); + } + + /** + * Constructor. Comments are ignored and not returned as tokens + * + * @param header The header that is tokenized + * @param delimiters The delimiters to be used + */ + public HeaderTokenizer(String header, String delimiters) { + this(header, delimiters, true); + } + + /** + * Constructor. The RFC822 defined delimiters - RFC822 - are + * used to delimit ATOMS. Also comments are skipped and not + * returned as tokens + * + * @param header the header string + */ + public HeaderTokenizer(String header) { + this(header, RFC822); + } + + /** + * Parses the next token from this String. <p> + * + * Clients sit in a loop calling next() to parse successive + * tokens until an EOF Token is returned. + * + * @return the next Token + * @exception ParseException if the parse fails + */ + public Token next() throws ParseException { + return next('\0', false); + } + + /** + * Parses the next token from this String. + * If endOfAtom is not NUL, the token extends until the + * endOfAtom character is seen, or to the end of the header. + * This method is useful when parsing headers that don't + * obey the MIME specification, e.g., by failing to quote + * parameter values that contain spaces. + * + * @param endOfAtom if not NUL, character marking end of token + * @return the next Token + * @exception ParseException if the parse fails + * @since JavaMail 1.5 + */ + public Token next(char endOfAtom) throws ParseException { + return next(endOfAtom, false); + } + + /** + * Parses the next token from this String. + * endOfAtom is handled as above. If keepEscapes is true, + * any backslash escapes are preserved in the returned string. + * This method is useful when parsing headers that don't + * obey the MIME specification, e.g., by failing to escape + * backslashes in the filename parameter. + * + * @param endOfAtom if not NUL, character marking end of token + * @param keepEscapes keep all backslashes in returned string? + * @return the next Token + * @exception ParseException if the parse fails + * @since JavaMail 1.5 + */ + public Token next(char endOfAtom, boolean keepEscapes) + throws ParseException { + Token tk; + + currentPos = nextPos; // setup currentPos + tk = getNext(endOfAtom, keepEscapes); + nextPos = peekPos = currentPos; // update currentPos and peekPos + return tk; + } + + /** + * Peek at the next token, without actually removing the token + * from the parse stream. Invoking this method multiple times + * will return successive tokens, until <code>next()</code> is + * called. <p> + * + * @return the next Token + * @exception ParseException if the parse fails + */ + public Token peek() throws ParseException { + Token tk; + + currentPos = peekPos; // setup currentPos + tk = getNext('\0', false); + peekPos = currentPos; // update peekPos + return tk; + } + + /** + * Return the rest of the Header. + * + * @return String rest of header. null is returned if we are + * already at end of header + */ + public String getRemainder() { + if (nextPos >= string.length()) + return null; + return string.substring(nextPos); + } + + /* + * Return the next token starting from 'currentPos'. After the + * parse, 'currentPos' is updated to point to the start of the + * next token. + */ + private Token getNext(char endOfAtom, boolean keepEscapes) + throws ParseException { + // If we're already at end of string, return EOF + if (currentPos >= maxPos) + return EOFToken; + + // Skip white-space, position currentPos beyond the space + if (skipWhiteSpace() == Token.EOF) + return EOFToken; + + char c; + int start; + boolean filter = false; + + c = string.charAt(currentPos); + + // Check or Skip comments and position currentPos + // beyond the comment + while (c == '(') { + // Parsing comment .. + int nesting; + for (start = ++currentPos, nesting = 1; + nesting > 0 && currentPos < maxPos; + currentPos++) { + c = string.charAt(currentPos); + if (c == '\\') { // Escape sequence + currentPos++; // skip the escaped character + filter = true; + } else if (c == '\r') + filter = true; + else if (c == '(') + nesting++; + else if (c == ')') + nesting--; + } + if (nesting != 0) + throw new ParseException("Unbalanced comments"); + + if (!skipComments) { + // Return the comment, if we are asked to. + // Note that the comment start & end markers are ignored. + String s; + if (filter) // need to go thru the token again. + s = filterToken(string, start, currentPos-1, keepEscapes); + else + s = string.substring(start,currentPos-1); + + return new Token(Token.COMMENT, s); + } + + // Skip any whitespace after the comment. + if (skipWhiteSpace() == Token.EOF) + return EOFToken; + c = string.charAt(currentPos); + } + + // Check for quoted-string and position currentPos + // beyond the terminating quote + if (c == '"') { + currentPos++; // skip initial quote + return collectString('"', keepEscapes); + } + + // Check for SPECIAL or CTL + if (c < 040 || c >= 0177 || delimiters.indexOf(c) >= 0) { + if (endOfAtom > 0 && c != endOfAtom) { + // not expecting a special character here, + // pretend it's a quoted string + return collectString(endOfAtom, keepEscapes); + } + currentPos++; // re-position currentPos + char ch[] = new char[1]; + ch[0] = c; + return new Token((int)c, new String(ch)); + } + + // Check for ATOM + for (start = currentPos; currentPos < maxPos; currentPos++) { + c = string.charAt(currentPos); + // ATOM is delimited by either SPACE, CTL, "(", <"> + // or the specified SPECIALS + if (c < 040 || c >= 0177 || c == '(' || c == ' ' || + c == '"' || delimiters.indexOf(c) >= 0) { + if (endOfAtom > 0 && c != endOfAtom) { + // not the expected atom after all; + // back up and pretend it's a quoted string + currentPos = start; + return collectString(endOfAtom, keepEscapes); + } + break; + } + } + return new Token(Token.ATOM, string.substring(start, currentPos)); + } + + private Token collectString(char eos, boolean keepEscapes) + throws ParseException { + int start; + boolean filter = false; + for (start = currentPos; currentPos < maxPos; currentPos++) { + char c = string.charAt(currentPos); + if (c == '\\') { // Escape sequence + currentPos++; + filter = true; + } else if (c == '\r') + filter = true; + else if (c == eos) { + currentPos++; + String s; + + if (filter) + s = filterToken(string, start, currentPos-1, keepEscapes); + else + s = string.substring(start, currentPos-1); + + if (c != '"') { // not a real quoted string + s = trimWhiteSpace(s); + currentPos--; // back up before the eos char + } + + return new Token(Token.QUOTEDSTRING, s); + } + } + + // ran off the end of the string + + // if we're looking for a matching quote, that's an error + if (eos == '"') + throw new ParseException("Unbalanced quoted string"); + + // otherwise, just return whatever's left + String s; + if (filter) + s = filterToken(string, start, currentPos, keepEscapes); + else + s = string.substring(start, currentPos); + s = trimWhiteSpace(s); + return new Token(Token.QUOTEDSTRING, s); + } + + // Skip SPACE, HT, CR and NL + private int skipWhiteSpace() { + char c; + for (; currentPos < maxPos; currentPos++) + if (((c = string.charAt(currentPos)) != ' ') && + (c != '\t') && (c != '\r') && (c != '\n')) + return currentPos; + return Token.EOF; + } + + // Trim SPACE, HT, CR and NL from end of string + private static String trimWhiteSpace(String s) { + char c; + int i; + for (i = s.length() - 1; i >= 0; i--) { + if (((c = s.charAt(i)) != ' ') && + (c != '\t') && (c != '\r') && (c != '\n')) + break; + } + if (i <= 0) + return ""; + else + return s.substring(0, i + 1); + } + + /* Process escape sequences and embedded LWSPs from a comment or + * quoted string. + */ + private static String filterToken(String s, int start, int end, + boolean keepEscapes) { + StringBuilder sb = new StringBuilder(); + char c; + boolean gotEscape = false; + boolean gotCR = false; + + for (int i = start; i < end; i++) { + c = s.charAt(i); + if (c == '\n' && gotCR) { + // This LF is part of an unescaped + // CRLF sequence (i.e, LWSP). Skip it. + gotCR = false; + continue; + } + + gotCR = false; + if (!gotEscape) { + // Previous character was NOT '\' + if (c == '\\') // skip this character + gotEscape = true; + else if (c == '\r') // skip this character + gotCR = true; + else // append this character + sb.append(c); + } else { + // Previous character was '\'. So no need to + // bother with any special processing, just + // append this character. If keepEscapes is + // set, keep the backslash. IE6 fails to escape + // backslashes in quoted strings in HTTP headers, + // e.g., in the filename parameter. + if (keepEscapes) + sb.append('\\'); + sb.append(c); + gotEscape = false; + } + } + return sb.toString(); + } +}
diff --git a/mail/src/main/java/javax/mail/internet/InternetAddress.java b/mail/src/main/java/javax/mail/internet/InternetAddress.java new file mode 100644 index 0000000..97ceb7e --- /dev/null +++ b/mail/src/main/java/javax/mail/internet/InternetAddress.java
@@ -0,0 +1,1523 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import java.io.UnsupportedEncodingException; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.List; +import java.util.ArrayList; +import java.util.StringTokenizer; +import java.util.Locale; +import java.nio.charset.StandardCharsets; +import javax.mail.*; +import com.sun.mail.util.PropUtil; + +/** + * This class represents an Internet email address using the syntax + * of <a href="http://www.ietf.org/rfc/rfc822.txt" target="_top">RFC822</a>. + * Typical address syntax is of the form "user@host.domain" or + * "Personal Name <user@host.domain>". + * + * @author Bill Shannon + * @author John Mani + */ + +public class InternetAddress extends Address implements Cloneable { + + protected String address; // email address + + /** + * The personal name. + */ + protected String personal; + + /** + * The RFC 2047 encoded version of the personal name. <p> + * + * This field and the <code>personal</code> field track each + * other, so if a subclass sets one of these fields directly, it + * should set the other to <code>null</code>, so that it is + * suitably recomputed. + */ + protected String encodedPersonal; + + private static final long serialVersionUID = -7507595530758302903L; + + private static final boolean ignoreBogusGroupName = + PropUtil.getBooleanSystemProperty( + "mail.mime.address.ignorebogusgroupname", true); + + private static final boolean useCanonicalHostName = + PropUtil.getBooleanSystemProperty( + "mail.mime.address.usecanonicalhostname", true); + + private static final boolean allowUtf8 = + PropUtil.getBooleanSystemProperty("mail.mime.allowutf8", false); + + /** + * Default constructor. + */ + public InternetAddress() { } + + /** + * Constructor. <p> + * + * Parse the given string and create an InternetAddress. + * See the <code>parse</code> method for details of the parsing. + * The address is parsed using "strict" parsing. + * This constructor does <b>not</b> perform the additional + * syntax checks that the + * <code>InternetAddress(String address, boolean strict)</code> + * constructor does when <code>strict</code> is <code>true</code>. + * This constructor is equivalent to + * <code>InternetAddress(address, false)</code>. + * + * @param address the address in RFC822 format + * @exception AddressException if the parse failed + */ + public InternetAddress(String address) throws AddressException { + // use our address parsing utility routine to parse the string + InternetAddress a[] = parse(address, true); + // if we got back anything other than a single address, it's an error + if (a.length != 1) + throw new AddressException("Illegal address", address); + + /* + * Now copy the contents of the single address we parsed + * into the current object, which will be returned from the + * constructor. + * XXX - this sure is a round-about way of getting this done. + */ + this.address = a[0].address; + this.personal = a[0].personal; + this.encodedPersonal = a[0].encodedPersonal; + } + + /** + * Parse the given string and create an InternetAddress. + * If <code>strict</code> is false, the detailed syntax of the + * address isn't checked. + * + * @param address the address in RFC822 format + * @param strict enforce RFC822 syntax + * @exception AddressException if the parse failed + * @since JavaMail 1.3 + */ + public InternetAddress(String address, boolean strict) + throws AddressException { + this(address); + if (strict) { + if (isGroup()) + getGroup(true); // throw away the result + else + checkAddress(this.address, true, true); + } + } + + /** + * Construct an InternetAddress given the address and personal name. + * The address is assumed to be a syntactically valid RFC822 address. + * + * @param address the address in RFC822 format + * @param personal the personal name + * @exception UnsupportedEncodingException if the personal name + * can't be encoded in the given charset + */ + public InternetAddress(String address, String personal) + throws UnsupportedEncodingException { + this(address, personal, null); + } + + /** + * Construct an InternetAddress given the address and personal name. + * The address is assumed to be a syntactically valid RFC822 address. + * + * @param address the address in RFC822 format + * @param personal the personal name + * @param charset the MIME charset for the name + * @exception UnsupportedEncodingException if the personal name + * can't be encoded in the given charset + */ + public InternetAddress(String address, String personal, String charset) + throws UnsupportedEncodingException { + this.address = address; + setPersonal(personal, charset); + } + + /** + * Return a copy of this InternetAddress object. + * @since JavaMail 1.2 + */ + @Override + public Object clone() { + InternetAddress a = null; + try { + a = (InternetAddress)super.clone(); + } catch (CloneNotSupportedException e) {} // Won't happen + return a; + } + + /** + * Return the type of this address. The type of an InternetAddress + * is "rfc822". + */ + @Override + public String getType() { + return "rfc822"; + } + + /** + * Set the email address. + * + * @param address email address + */ + public void setAddress(String address) { + this.address = address; + } + + /** + * Set the personal name. If the name contains non US-ASCII + * characters, then the name will be encoded using the specified + * charset as per RFC 2047. If the name contains only US-ASCII + * characters, no encoding is done and the name is used as is. <p> + * + * @param name personal name + * @param charset MIME charset to be used to encode the name as + * per RFC 2047 + * @see #setPersonal(String) + * @exception UnsupportedEncodingException if the charset encoding + * fails. + */ + public void setPersonal(String name, String charset) + throws UnsupportedEncodingException { + personal = name; + if (name != null) + encodedPersonal = MimeUtility.encodeWord(name, charset, null); + else + encodedPersonal = null; + } + + /** + * Set the personal name. If the name contains non US-ASCII + * characters, then the name will be encoded using the platform's + * default charset. If the name contains only US-ASCII characters, + * no encoding is done and the name is used as is. <p> + * + * @param name personal name + * @see #setPersonal(String name, String charset) + * @exception UnsupportedEncodingException if the charset encoding + * fails. + */ + public void setPersonal(String name) + throws UnsupportedEncodingException { + personal = name; + if (name != null) + encodedPersonal = MimeUtility.encodeWord(name); + else + encodedPersonal = null; + } + + /** + * Get the email address. + * @return email address + */ + public String getAddress() { + return address; + } + + /** + * Get the personal name. If the name is encoded as per RFC 2047, + * it is decoded and converted into Unicode. If the decoding or + * conversion fails, the raw data is returned as is. + * + * @return personal name + */ + public String getPersonal() { + if (personal != null) + return personal; + + if (encodedPersonal != null) { + try { + personal = MimeUtility.decodeText(encodedPersonal); + return personal; + } catch (Exception ex) { + // 1. ParseException: either its an unencoded string or + // it can't be parsed + // 2. UnsupportedEncodingException: can't decode it. + return encodedPersonal; + } + } + // No personal or encodedPersonal, return null + return null; + } + + /** + * Convert this address into a RFC 822 / RFC 2047 encoded address. + * The resulting string contains only US-ASCII characters, and + * hence is mail-safe. + * + * @return possibly encoded address string + */ + @Override + public String toString() { + String a = address == null ? "" : address; + if (encodedPersonal == null && personal != null) + try { + encodedPersonal = MimeUtility.encodeWord(personal); + } catch (UnsupportedEncodingException ex) { } + + if (encodedPersonal != null) + return quotePhrase(encodedPersonal) + " <" + a + ">"; + else if (isGroup() || isSimple()) + return a; + else + return "<" + a + ">"; + } + + /** + * Returns a properly formatted address (RFC 822 syntax) of + * Unicode characters. + * + * @return Unicode address string + * @since JavaMail 1.2 + */ + public String toUnicodeString() { + String p = getPersonal(); + if (p != null) + return quotePhrase(p) + " <" + address + ">"; + else if (isGroup() || isSimple()) + return address; + else + return "<" + address + ">"; + } + + /* + * quotePhrase() quotes the words within a RFC822 phrase. + * + * This is tricky, since a phrase is defined as 1 or more + * RFC822 words, separated by LWSP. Now, a word that contains + * LWSP is supposed to be quoted, and this is exactly what the + * MimeUtility.quote() method does. However, when dealing with + * a phrase, any LWSP encountered can be construed to be the + * separator between words, and not part of the words themselves. + * To deal with this funkiness, we have the below variant of + * MimeUtility.quote(), which essentially ignores LWSP when + * deciding whether to quote a word. + * + * It aint pretty, but it gets the job done :) + */ + + private static final String rfc822phrase = + HeaderTokenizer.RFC822.replace(' ', '\0').replace('\t', '\0'); + + private static String quotePhrase(String phrase) { + int len = phrase.length(); + boolean needQuoting = false; + + for (int i = 0; i < len; i++) { + char c = phrase.charAt(i); + if (c == '"' || c == '\\') { + // need to escape them and then quote the whole string + StringBuilder sb = new StringBuilder(len + 3); + sb.append('"'); + for (int j = 0; j < len; j++) { + char cc = phrase.charAt(j); + if (cc == '"' || cc == '\\') + // Escape the character + sb.append('\\'); + sb.append(cc); + } + sb.append('"'); + return sb.toString(); + } else if ((c < 040 && c != '\r' && c != '\n' && c != '\t') || + (c >= 0177 && !allowUtf8) || rfc822phrase.indexOf(c) >= 0) + // These characters cause the string to be quoted + needQuoting = true; + } + + if (needQuoting) { + StringBuilder sb = new StringBuilder(len + 2); + sb.append('"').append(phrase).append('"'); + return sb.toString(); + } else + return phrase; + } + + private static String unquote(String s) { + if (s.startsWith("\"") && s.endsWith("\"") && s.length() > 1) { + s = s.substring(1, s.length() - 1); + // check for any escaped characters + if (s.indexOf('\\') >= 0) { + StringBuilder sb = new StringBuilder(s.length()); // approx + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c == '\\' && i < s.length() - 1) + c = s.charAt(++i); + sb.append(c); + } + s = sb.toString(); + } + } + return s; + } + + /** + * The equality operator. + */ + @Override + public boolean equals(Object a) { + if (!(a instanceof InternetAddress)) + return false; + + String s = ((InternetAddress)a).getAddress(); + if (s == address) + return true; + if (address != null && address.equalsIgnoreCase(s)) + return true; + + return false; + } + + /** + * Compute a hash code for the address. + */ + @Override + public int hashCode() { + if (address == null) + return 0; + else + return address.toLowerCase(Locale.ENGLISH).hashCode(); + } + + /** + * Convert the given array of InternetAddress objects into + * a comma separated sequence of address strings. The + * resulting string contains only US-ASCII characters, and + * hence is mail-safe. <p> + * + * @param addresses array of InternetAddress objects + * @exception ClassCastException if any address object in the + * given array is not an InternetAddress object. Note + * that this is a RuntimeException. + * @return comma separated string of addresses + */ + public static String toString(Address[] addresses) { + return toString(addresses, 0); + } + + /** + * Convert the given array of InternetAddress objects into + * a comma separated sequence of address strings. The + * resulting string contains Unicode characters. <p> + * + * @param addresses array of InternetAddress objects + * @exception ClassCastException if any address object in the + * given array is not an InternetAddress object. Note + * that this is a RuntimeException. + * @return comma separated string of addresses + * @since JavaMail 1.6 + */ + public static String toUnicodeString(Address[] addresses) { + return toUnicodeString(addresses, 0); + } + + /** + * Convert the given array of InternetAddress objects into + * a comma separated sequence of address strings. The + * resulting string contains only US-ASCII characters, and + * hence is mail-safe. <p> + * + * The 'used' parameter specifies the number of character positions + * already taken up in the field into which the resulting address + * sequence string is to be inserted. It is used to determine the + * line-break positions in the resulting address sequence string. + * + * @param addresses array of InternetAddress objects + * @param used number of character positions already used, in + * the field into which the address string is to + * be inserted. + * @exception ClassCastException if any address object in the + * given array is not an InternetAddress object. Note + * that this is a RuntimeException. + * @return comma separated string of addresses + */ + public static String toString(Address[] addresses, int used) { + if (addresses == null || addresses.length == 0) + return null; + + StringBuilder sb = new StringBuilder(); + + for (int i = 0; i < addresses.length; i++) { + if (i != 0) { // need to append comma + sb.append(", "); + used += 2; + } + + // prefer not to split a single address across lines so used=0 below + String s = MimeUtility.fold(0, addresses[i].toString()); + int len = lengthOfFirstSegment(s); // length till CRLF + if (used + len > 76) { // overflows ... + // smash trailing space from ", " above + int curlen = sb.length(); + if (curlen > 0 && sb.charAt(curlen - 1) == ' ') + sb.setLength(curlen - 1); + sb.append("\r\n\t"); // .. start new continuation line + used = 8; // account for the starting <tab> char + } + sb.append(s); + used = lengthOfLastSegment(s, used); + } + + return sb.toString(); + } + + /** + * Convert the given array of InternetAddress objects into + * a comma separated sequence of address strings. The + * resulting string contains Unicode characters. <p> + * + * The 'used' parameter specifies the number of character positions + * already taken up in the field into which the resulting address + * sequence string is to be inserted. It is used to determine the + * line-break positions in the resulting address sequence string. + * + * @param addresses array of InternetAddress objects + * @param used number of character positions already used, in + * the field into which the address string is to + * be inserted. + * @exception ClassCastException if any address object in the + * given array is not an InternetAddress object. Note + * that this is a RuntimeException. + * @return comma separated string of addresses + * @since JavaMail 1.6 + */ + /* + * XXX - This is exactly the same as the above, except it uses + * toUnicodeString instead of toString. + * XXX - Since the line length restrictions are in bytes, not characters, + * we convert all non-ASCII addresses to UTF-8 byte strings, + * which we then convert to ISO-8859-1 Strings where every + * character respresents one UTF-8 byte. At the end we reverse + * the conversion to get back to a correct Unicode string. + * This is a hack to allow all the other character-based methods + * to work properly with UTF-8 bytes. + */ + public static String toUnicodeString(Address[] addresses, int used) { + if (addresses == null || addresses.length == 0) + return null; + + StringBuilder sb = new StringBuilder(); + + boolean sawNonAscii = false; + for (int i = 0; i < addresses.length; i++) { + if (i != 0) { // need to append comma + sb.append(", "); + used += 2; + } + + // prefer not to split a single address across lines so used=0 below + String as = ((InternetAddress)addresses[i]).toUnicodeString(); + if (MimeUtility.checkAscii(as) != MimeUtility.ALL_ASCII) { + sawNonAscii = true; + as = new String(as.getBytes(StandardCharsets.UTF_8), + StandardCharsets.ISO_8859_1); + } + String s = MimeUtility.fold(0, as); + int len = lengthOfFirstSegment(s); // length till CRLF + if (used + len > 76) { // overflows ... + // smash trailing space from ", " above + int curlen = sb.length(); + if (curlen > 0 && sb.charAt(curlen - 1) == ' ') + sb.setLength(curlen - 1); + sb.append("\r\n\t"); // .. start new continuation line + used = 8; // account for the starting <tab> char + } + sb.append(s); + used = lengthOfLastSegment(s, used); + } + + String ret = sb.toString(); + if (sawNonAscii) + ret = new String(ret.getBytes(StandardCharsets.ISO_8859_1), + StandardCharsets.UTF_8); + return ret; + } + + /* + * Return the length of the first segment within this string. + * If no segments exist, the length of the whole line is returned. + */ + private static int lengthOfFirstSegment(String s) { + int pos; + if ((pos = s.indexOf("\r\n")) != -1) + return pos; + else + return s.length(); + } + + /* + * Return the length of the last segment within this string. + * If no segments exist, the length of the whole line plus + * <code>used</code> is returned. + */ + private static int lengthOfLastSegment(String s, int used) { + int pos; + if ((pos = s.lastIndexOf("\r\n")) != -1) + return s.length() - pos - 2; + else + return s.length() + used; + } + + /** + * Return an InternetAddress object representing the current user. + * The entire email address may be specified in the "mail.from" + * property. If not set, the "mail.user" and "mail.host" properties + * are tried. If those are not set, the "user.name" property and + * <code>InetAddress.getLocalHost</code> method are tried. + * Security exceptions that may occur while accessing this information + * are ignored. If it is not possible to determine an email address, + * null is returned. + * + * @param session Session object used for property lookup + * @return current user's email address + */ + public static InternetAddress getLocalAddress(Session session) { + try { + return _getLocalAddress(session); + } catch (SecurityException sex) { // ignore it + } catch (AddressException ex) { // ignore it + } catch (UnknownHostException ex) { } // ignore it + return null; + } + + /** + * A package-private version of getLocalAddress that doesn't swallow + * the exception. Used by MimeMessage.setFrom() to report the reason + * for the failure. + */ + // package-private + static InternetAddress _getLocalAddress(Session session) + throws SecurityException, AddressException, UnknownHostException { + String user = null, host = null, address = null; + if (session == null) { + user = System.getProperty("user.name"); + host = getLocalHostName(); + } else { + address = session.getProperty("mail.from"); + if (address == null) { + user = session.getProperty("mail.user"); + if (user == null || user.length() == 0) + user = session.getProperty("user.name"); + if (user == null || user.length() == 0) + user = System.getProperty("user.name"); + host = session.getProperty("mail.host"); + if (host == null || host.length() == 0) + host = getLocalHostName(); + } + } + + if (address == null && user != null && user.length() != 0 && + host != null && host.length() != 0) + address = MimeUtility.quote(user.trim(), specialsNoDot + "\t ") + + "@" + host; + + if (address == null) + return null; + + return new InternetAddress(address); + } + + /** + * Get the local host name from InetAddress and return it in a form + * suitable for use in an email address. + */ + private static String getLocalHostName() throws UnknownHostException { + String host = null; + InetAddress me = InetAddress.getLocalHost(); + if (me != null) { + // try canonical host name first + if (useCanonicalHostName) + host = me.getCanonicalHostName(); + if (host == null) + host = me.getHostName(); + // if we can't get our name, use local address literal + if (host == null) + host = me.getHostAddress(); + if (host != null && host.length() > 0 && isInetAddressLiteral(host)) + host = '[' + host + ']'; + } + return host; + } + + /** + * Is the address an IPv4 or IPv6 address literal, which needs to + * be enclosed in "[]" in an email address? IPv4 literals contain + * decimal digits and dots, IPv6 literals contain hex digits, dots, + * and colons. We're lazy and don't check the exact syntax, just + * the allowed characters; strings that have only the allowed + * characters in a literal but don't meet the syntax requirements + * for a literal definitely can't be a host name and thus will fail + * later when used as an address literal. + */ + private static boolean isInetAddressLiteral(String addr) { + boolean sawHex = false, sawColon = false; + for (int i = 0; i < addr.length(); i++) { + char c = addr.charAt(i); + if (c >= '0' && c <= '9') + ; // digits always ok + else if (c == '.') + ; // dot always ok + else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) + sawHex = true; // need to see a colon too + else if (c == ':') + sawColon = true; + else + return false; // anything else, definitely not a literal + } + return !sawHex || sawColon; + } + + /** + * Parse the given comma separated sequence of addresses into + * InternetAddress objects. Addresses must follow RFC822 syntax. + * + * @param addresslist comma separated address strings + * @return array of InternetAddress objects + * @exception AddressException if the parse failed + */ + public static InternetAddress[] parse(String addresslist) + throws AddressException { + return parse(addresslist, true); + } + + /** + * Parse the given sequence of addresses into InternetAddress + * objects. If <code>strict</code> is false, simple email addresses + * separated by spaces are also allowed. If <code>strict</code> is + * true, many (but not all) of the RFC822 syntax rules are enforced. + * In particular, even if <code>strict</code> is true, addresses + * composed of simple names (with no "@domain" part) are allowed. + * Such "illegal" addresses are not uncommon in real messages. <p> + * + * Non-strict parsing is typically used when parsing a list of + * mail addresses entered by a human. Strict parsing is typically + * used when parsing address headers in mail messages. + * + * @param addresslist comma separated address strings + * @param strict enforce RFC822 syntax + * @return array of InternetAddress objects + * @exception AddressException if the parse failed + */ + public static InternetAddress[] parse(String addresslist, boolean strict) + throws AddressException { + return parse(addresslist, strict, false); + } + + /** + * Parse the given sequence of addresses into InternetAddress + * objects. If <code>strict</code> is false, the full syntax rules for + * individual addresses are not enforced. If <code>strict</code> is + * true, many (but not all) of the RFC822 syntax rules are enforced. <p> + * + * To better support the range of "invalid" addresses seen in real + * messages, this method enforces fewer syntax rules than the + * <code>parse</code> method when the strict flag is false + * and enforces more rules when the strict flag is true. If the + * strict flag is false and the parse is successful in separating out an + * email address or addresses, the syntax of the addresses themselves + * is not checked. + * + * @param addresslist comma separated address strings + * @param strict enforce RFC822 syntax + * @return array of InternetAddress objects + * @exception AddressException if the parse failed + * @since JavaMail 1.3 + */ + public static InternetAddress[] parseHeader(String addresslist, + boolean strict) throws AddressException { + return parse(MimeUtility.unfold(addresslist), strict, true); + } + + /* + * RFC822 Address parser. + * + * XXX - This is complex enough that it ought to be a real parser, + * not this ad-hoc mess, and because of that, this is not perfect. + * + * XXX - Deal with encoded Headers too. + */ + @SuppressWarnings("fallthrough") + private static InternetAddress[] parse(String s, boolean strict, + boolean parseHdr) throws AddressException { + int start, end, index, nesting; + int start_personal = -1, end_personal = -1; + int length = s.length(); + boolean ignoreErrors = parseHdr && !strict; + boolean in_group = false; // we're processing a group term + boolean route_addr = false; // address came from route-addr term + boolean rfc822 = false; // looks like an RFC822 address + char c; + List<InternetAddress> v = new ArrayList<>(); + InternetAddress ma; + + for (start = end = -1, index = 0; index < length; index++) { + c = s.charAt(index); + + switch (c) { + case '(': // We are parsing a Comment. Ignore everything inside. + // XXX - comment fields should be parsed as whitespace, + // more than one allowed per address + rfc822 = true; + if (start >= 0 && end == -1) + end = index; + int pindex = index; + for (index++, nesting = 1; index < length && nesting > 0; + index++) { + c = s.charAt(index); + switch (c) { + case '\\': + index++; // skip both '\' and the escaped char + break; + case '(': + nesting++; + break; + case ')': + nesting--; + break; + default: + break; + } + } + if (nesting > 0) { + if (!ignoreErrors) + throw new AddressException("Missing ')'", s, index); + // pretend the first paren was a regular character and + // continue parsing after it + index = pindex + 1; + break; + } + index--; // point to closing paren + if (start_personal == -1) + start_personal = pindex + 1; + if (end_personal == -1) + end_personal = index; + break; + + case ')': + if (!ignoreErrors) + throw new AddressException("Missing '('", s, index); + // pretend the left paren was a regular character and + // continue parsing + if (start == -1) + start = index; + break; + + case '<': + rfc822 = true; + if (route_addr) { + if (!ignoreErrors) + throw new AddressException( + "Extra route-addr", s, index); + + // assume missing comma between addresses + if (start == -1) { + route_addr = false; + rfc822 = false; + start = end = -1; + break; // nope, nothing there + } + if (!in_group) { + // got a token, add this to our InternetAddress list + if (end == -1) // should never happen + end = index; + String addr = s.substring(start, end).trim(); + + ma = new InternetAddress(); + ma.setAddress(addr); + if (start_personal >= 0) { + ma.encodedPersonal = unquote( + s.substring(start_personal, end_personal). + trim()); + } + v.add(ma); + + route_addr = false; + rfc822 = false; + start = end = -1; + start_personal = end_personal = -1; + // continue processing this new address... + } + } + + int rindex = index; + boolean inquote = false; + outf: + for (index++; index < length; index++) { + c = s.charAt(index); + switch (c) { + case '\\': // XXX - is this needed? + index++; // skip both '\' and the escaped char + break; + case '"': + inquote = !inquote; + break; + case '>': + if (inquote) + continue; + break outf; // out of for loop + default: + break; + } + } + + // did we find a matching quote? + if (inquote) { + if (!ignoreErrors) + throw new AddressException("Missing '\"'", s, index); + // didn't find matching quote, try again ignoring quotes + // (e.g., ``<"@foo.com>'') + outq: + for (index = rindex + 1; index < length; index++) { + c = s.charAt(index); + if (c == '\\') // XXX - is this needed? + index++; // skip both '\' and the escaped char + else if (c == '>') + break; + } + } + + // did we find a terminating '>'? + if (index >= length) { + if (!ignoreErrors) + throw new AddressException("Missing '>'", s, index); + // pretend the "<" was a regular character and + // continue parsing after it (e.g., ``<@foo.com'') + index = rindex + 1; + if (start == -1) + start = rindex; // back up to include "<" + break; + } + + if (!in_group) { + if (start >= 0) { + // seen some characters? use them as the personal name + start_personal = start; + end_personal = rindex; + } + start = rindex + 1; + } + route_addr = true; + end = index; + break; + + case '>': + if (!ignoreErrors) + throw new AddressException("Missing '<'", s, index); + // pretend the ">" was a regular character and + // continue parsing (e.g., ``>@foo.com'') + if (start == -1) + start = index; + break; + + case '"': // parse quoted string + int qindex = index; + rfc822 = true; + if (start == -1) + start = index; + outq: + for (index++; index < length; index++) { + c = s.charAt(index); + switch (c) { + case '\\': + index++; // skip both '\' and the escaped char + break; + case '"': + break outq; // out of for loop + default: + break; + } + } + if (index >= length) { + if (!ignoreErrors) + throw new AddressException("Missing '\"'", s, index); + // pretend the quote was a regular character and + // continue parsing after it (e.g., ``"@foo.com'') + index = qindex + 1; + } + break; + + case '[': // a domain-literal, probably + int lindex = index; + rfc822 = true; + if (start == -1) + start = index; + outb: + for (index++; index < length; index++) { + c = s.charAt(index); + switch (c) { + case '\\': + index++; // skip both '\' and the escaped char + break; + case ']': + break outb; // out of for loop + default: + break; + } + } + if (index >= length) { + if (!ignoreErrors) + throw new AddressException("Missing ']'", s, index); + // pretend the "[" was a regular character and + // continue parsing after it (e.g., ``[@foo.com'') + index = lindex + 1; + } + break; + + case ';': + if (start == -1) { + route_addr = false; + rfc822 = false; + start = end = -1; + break; // nope, nothing there + } + if (in_group) { + in_group = false; + /* + * If parsing headers, but not strictly, peek ahead. + * If next char is "@", treat the group name + * like the local part of the address, e.g., + * "Undisclosed-Recipient:;@java.sun.com". + */ + if (parseHdr && !strict && + index + 1 < length && s.charAt(index + 1) == '@') + break; + ma = new InternetAddress(); + end = index + 1; + ma.setAddress(s.substring(start, end).trim()); + v.add(ma); + + route_addr = false; + rfc822 = false; + start = end = -1; + start_personal = end_personal = -1; + break; + } + if (!ignoreErrors) + throw new AddressException( + "Illegal semicolon, not in group", s, index); + + // otherwise, parsing a header; treat semicolon like comma + // fall through to comma case... + + case ',': // end of an address, probably + if (start == -1) { + route_addr = false; + rfc822 = false; + start = end = -1; + break; // nope, nothing there + } + if (in_group) { + route_addr = false; + break; + } + // got a token, add this to our InternetAddress list + if (end == -1) + end = index; + + String addr = s.substring(start, end).trim(); + String pers = null; + if (rfc822 && start_personal >= 0) { + pers = unquote( + s.substring(start_personal, end_personal).trim()); + if (pers.trim().length() == 0) + pers = null; + } + + /* + * If the personal name field has an "@" and the address + * field does not, assume they were reversed, e.g., + * ``"joe doe" (john.doe@example.com)''. + */ + if (parseHdr && !strict && pers != null && + pers.indexOf('@') >= 0 && + addr.indexOf('@') < 0 && addr.indexOf('!') < 0) { + String tmp = addr; + addr = pers; + pers = tmp; + } + if (rfc822 || strict || parseHdr) { + if (!ignoreErrors) + checkAddress(addr, route_addr, false); + ma = new InternetAddress(); + ma.setAddress(addr); + if (pers != null) + ma.encodedPersonal = pers; + v.add(ma); + } else { + // maybe we passed over more than one space-separated addr + StringTokenizer st = new StringTokenizer(addr); + while (st.hasMoreTokens()) { + String a = st.nextToken(); + checkAddress(a, false, false); + ma = new InternetAddress(); + ma.setAddress(a); + v.add(ma); + } + } + + route_addr = false; + rfc822 = false; + start = end = -1; + start_personal = end_personal = -1; + break; + + case ':': + rfc822 = true; + if (in_group) + if (!ignoreErrors) + throw new AddressException("Nested group", s, index); + if (start == -1) + start = index; + if (parseHdr && !strict) { + /* + * If next char is a special character that can't occur at + * the start of a valid address, treat the group name + * as the entire address, e.g., "Date:, Tue", "Re:@foo". + */ + if (index + 1 < length) { + String addressSpecials = ")>[]:@\\,."; + char nc = s.charAt(index + 1); + if (addressSpecials.indexOf(nc) >= 0) { + if (nc != '@') + break; // don't change in_group + /* + * Handle a common error: + * ``Undisclosed-Recipient:@example.com;'' + * + * Scan ahead. If we find a semicolon before + * one of these other special characters, + * consider it to be a group after all. + */ + for (int i = index + 2; i < length; i++) { + nc = s.charAt(i); + if (nc == ';') + break; + if (addressSpecials.indexOf(nc) >= 0) + break; + } + if (nc == ';') + break; // don't change in_group + } + } + + // ignore bogus "mailto:" prefix in front of an address, + // or bogus mail header name included in the address field + String gname = s.substring(start, index); + if (ignoreBogusGroupName && + (gname.equalsIgnoreCase("mailto") || + gname.equalsIgnoreCase("From") || + gname.equalsIgnoreCase("To") || + gname.equalsIgnoreCase("Cc") || + gname.equalsIgnoreCase("Subject") || + gname.equalsIgnoreCase("Re"))) + start = -1; // we're not really in a group + else + in_group = true; + } else + in_group = true; + break; + + // Ignore whitespace + case ' ': + case '\t': + case '\r': + case '\n': + break; + + default: + if (start == -1) + start = index; + break; + } + } + + if (start >= 0) { + /* + * The last token, add this to our InternetAddress list. + * Note that this block of code should be identical to the + * block above for "case ','". + */ + if (end == -1) + end = length; + + String addr = s.substring(start, end).trim(); + String pers = null; + if (rfc822 && start_personal >= 0) { + pers = unquote( + s.substring(start_personal, end_personal).trim()); + if (pers.trim().length() == 0) + pers = null; + } + + /* + * If the personal name field has an "@" and the address + * field does not, assume they were reversed, e.g., + * ``"joe doe" (john.doe@example.com)''. + */ + if (parseHdr && !strict && + pers != null && pers.indexOf('@') >= 0 && + addr.indexOf('@') < 0 && addr.indexOf('!') < 0) { + String tmp = addr; + addr = pers; + pers = tmp; + } + if (rfc822 || strict || parseHdr) { + if (!ignoreErrors) + checkAddress(addr, route_addr, false); + ma = new InternetAddress(); + ma.setAddress(addr); + if (pers != null) + ma.encodedPersonal = pers; + v.add(ma); + } else { + // maybe we passed over more than one space-separated addr + StringTokenizer st = new StringTokenizer(addr); + while (st.hasMoreTokens()) { + String a = st.nextToken(); + checkAddress(a, false, false); + ma = new InternetAddress(); + ma.setAddress(a); + v.add(ma); + } + } + } + + InternetAddress[] a = new InternetAddress[v.size()]; + v.toArray(a); + return a; + } + + /** + * Validate that this address conforms to the syntax rules of + * RFC 822. The current implementation checks many, but not + * all, syntax rules. Note that even though the syntax of + * the address may be correct, there's no guarantee that a + * mailbox of that name exists. + * + * @exception AddressException if the address isn't valid. + * @since JavaMail 1.3 + */ + public void validate() throws AddressException { + if (isGroup()) + getGroup(true); // throw away the result + else + checkAddress(getAddress(), true, true); + } + + private static final String specialsNoDotNoAt = "()<>,;:\\\"[]"; + private static final String specialsNoDot = specialsNoDotNoAt + "@"; + + /** + * Check that the address is a valid "mailbox" per RFC822. + * (We also allow simple names.) + * + * XXX - much more to check + * XXX - doesn't handle domain-literals properly (but no one uses them) + */ + private static void checkAddress(String addr, + boolean routeAddr, boolean validate) + throws AddressException { + int i, start = 0; + + if (addr == null) + throw new AddressException("Address is null"); + int len = addr.length(); + if (len == 0) + throw new AddressException("Empty address", addr); + + /* + * routeAddr indicates that the address is allowed + * to have an RFC 822 "route". + */ + if (routeAddr && addr.charAt(0) == '@') { + /* + * Check for a legal "route-addr": + * [@domain[,@domain ...]:]local@domain + */ + for (start = 0; (i = indexOfAny(addr, ",:", start)) >= 0; + start = i+1) { + if (addr.charAt(start) != '@') + throw new AddressException("Illegal route-addr", addr); + if (addr.charAt(i) == ':') { + // end of route-addr + start = i + 1; + break; + } + } + } + + /* + * The rest should be "local@domain", but we allow simply "local" + * unless called from validate. + * + * local-part must follow RFC 822 - no specials except '.' + * unless quoted. + */ + + char c = (char)-1; + char lastc = (char)-1; + boolean inquote = false; + for (i = start; i < len; i++) { + lastc = c; + c = addr.charAt(i); + // a quoted-pair is only supposed to occur inside a quoted string, + // but some people use it outside so we're more lenient + if (c == '\\' || lastc == '\\') + continue; + if (c == '"') { + if (inquote) { + // peek ahead, next char must be "@" + if (validate && i + 1 < len && addr.charAt(i + 1) != '@') + throw new AddressException( + "Quote not at end of local address", addr); + inquote = false; + } else { + if (validate && i != 0) + throw new AddressException( + "Quote not at start of local address", addr); + inquote = true; + } + continue; + } else if (c == '\r') { + // peek ahead, next char must be LF + if (i + 1 < len && addr.charAt(i + 1) != '\n') + throw new AddressException( + "Quoted local address contains CR without LF", addr); + } else if (c == '\n') { + /* + * CRLF followed by whitespace is allowed in a quoted string. + * We allowed naked LF, but ensure LF is always followed by + * whitespace to prevent spoofing the end of the header. + */ + if (i + 1 < len && addr.charAt(i + 1) != ' ' && + addr.charAt(i + 1) != '\t') + throw new AddressException( + "Quoted local address contains newline without whitespace", + addr); + } else if (c == '.') { + if (i == start) + throw new AddressException( + "Local address starts with dot", addr); + if (lastc == '.') + throw new AddressException( + "Local address contains dot-dot", addr); + } + if (inquote) + continue; + if (c == '@') { + if (i == 0) + throw new AddressException("Missing local name", addr); + if (lastc == '.') + throw new AddressException( + "Local address ends with dot", addr); + break; // done with local part + } + if (c <= 040 || c == 0177) + throw new AddressException( + "Local address contains control or whitespace", addr); + if (specialsNoDot.indexOf(c) >= 0) + throw new AddressException( + "Local address contains illegal character", addr); + } + if (inquote) + throw new AddressException("Unterminated quote", addr); + + /* + * Done with local part, now check domain. + * + * Note that the MimeMessage class doesn't remember addresses + * as separate objects; it writes them out as headers and then + * parses the headers when the addresses are requested. + * In order to support the case where a "simple" address is used, + * but the address also has a personal name and thus looks like + * it should be a valid RFC822 address when parsed, we only check + * this if we're explicitly called from the validate method. + */ + + if (c != '@') { + if (validate) + throw new AddressException("Missing final '@domain'", addr); + return; + } + + // check for illegal chars in the domain, but ignore domain literals + + start = i + 1; + if (start >= len) + throw new AddressException("Missing domain", addr); + + if (addr.charAt(start) == '.') + throw new AddressException("Domain starts with dot", addr); + boolean inliteral = false; + for (i = start; i < len; i++) { + c = addr.charAt(i); + if (c == '[') { + if (i != start) + throw new AddressException( + "Domain literal not at start of domain", addr); + inliteral = true; // domain literal, don't validate + } else if (c == ']') { + if (i != len - 1) + throw new AddressException( + "Domain literal end not at end of domain", addr); + inliteral = false; + } else if (c <= 040 || c == 0177) { + throw new AddressException( + "Domain contains control or whitespace", addr); + } else { + // RFC 2822 rule + //if (specialsNoDot.indexOf(c) >= 0) + /* + * RFC 1034 rule is more strict + * the full rule is: + * + * <domain> ::= <subdomain> | " " + * <subdomain> ::= <label> | <subdomain> "." <label> + * <label> ::= <letter> [ [ <ldh-str> ] <let-dig> ] + * <ldh-str> ::= <let-dig-hyp> | <let-dig-hyp> <ldh-str> + * <let-dig-hyp> ::= <let-dig> | "-" + * <let-dig> ::= <letter> | <digit> + */ + if (!inliteral) { + if (!(Character.isLetterOrDigit(c) || c == '-' || c == '.')) + throw new AddressException( + "Domain contains illegal character", addr); + if (c == '.' && lastc == '.') + throw new AddressException( + "Domain contains dot-dot", addr); + } + } + lastc = c; + } + if (lastc == '.') + throw new AddressException("Domain ends with dot", addr); + } + + /** + * Is this a "simple" address? Simple addresses don't contain quotes + * or any RFC822 special characters other than '@' and '.'. + */ + private boolean isSimple() { + return address == null || indexOfAny(address, specialsNoDotNoAt) < 0; + } + + /** + * Indicates whether this address is an RFC 822 group address. + * Note that a group address is different than the mailing + * list addresses supported by most mail servers. Group addresses + * are rarely used; see RFC 822 for details. + * + * @return true if this address represents a group + * @since JavaMail 1.3 + */ + public boolean isGroup() { + // quick and dirty check + return address != null && + address.endsWith(";") && address.indexOf(':') > 0; + } + + /** + * Return the members of a group address. A group may have zero, + * one, or more members. If this address is not a group, null + * is returned. The <code>strict</code> parameter controls whether + * the group list is parsed using strict RFC 822 rules or not. + * The parsing is done using the <code>parseHeader</code> method. + * + * @param strict use strict RFC 822 rules? + * @return array of InternetAddress objects, or null + * @exception AddressException if the group list can't be parsed + * @since JavaMail 1.3 + */ + public InternetAddress[] getGroup(boolean strict) throws AddressException { + String addr = getAddress(); + if (addr == null) + return null; + // groups are of the form "name:addr,addr,...;" + if (!addr.endsWith(";")) + return null; + int ix = addr.indexOf(':'); + if (ix < 0) + return null; + // extract the list + String list = addr.substring(ix + 1, addr.length() - 1); + // parse it and return the individual addresses + return InternetAddress.parseHeader(list, strict); + } + + /** + * Return the first index of any of the characters in "any" in "s", + * or -1 if none are found. + * + * This should be a method on String. + */ + private static int indexOfAny(String s, String any) { + return indexOfAny(s, any, 0); + } + + private static int indexOfAny(String s, String any, int start) { + try { + int len = s.length(); + for (int i = start; i < len; i++) { + if (any.indexOf(s.charAt(i)) >= 0) + return i; + } + return -1; + } catch (StringIndexOutOfBoundsException e) { + return -1; + } + } + + /* + public static void main(String argv[]) throws Exception { + for (int i = 0; i < argv.length; i++) { + InternetAddress[] a = InternetAddress.parse(argv[i]); + for (int j = 0; j < a.length; j++) { + System.out.println("arg " + i + " address " + j + ": " + a[j]); + System.out.println("\tAddress: " + a[j].getAddress() + + "\tPersonal: " + a[j].getPersonal()); + } + if (a.length > 1) { + System.out.println("address 0 hash code: " + a[0].hashCode()); + System.out.println("address 1 hash code: " + a[1].hashCode()); + if (a[0].hashCode() == a[1].hashCode()) + System.out.println("success, hashcodes equal"); + else + System.out.println("fail, hashcodes not equal"); + if (a[0].equals(a[1])) + System.out.println("success, addresses equal"); + else + System.out.println("fail, addresses not equal"); + if (a[1].equals(a[0])) + System.out.println("success, addresses equal"); + else + System.out.println("fail, addresses not equal"); + } + } + } + */ +}
diff --git a/mail/src/main/java/javax/mail/internet/InternetHeaders.java b/mail/src/main/java/javax/mail/internet/InternetHeaders.java new file mode 100644 index 0000000..3df0cca --- /dev/null +++ b/mail/src/main/java/javax/mail/internet/InternetHeaders.java
@@ -0,0 +1,683 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import java.io.*; +import java.util.*; +import javax.mail.*; +import com.sun.mail.util.LineInputStream; +import com.sun.mail.util.PropUtil; + +/** + * InternetHeaders is a utility class that manages RFC822 style + * headers. Given an RFC822 format message stream, it reads lines + * until the blank line that indicates end of header. The input stream + * is positioned at the start of the body. The lines are stored + * within the object and can be extracted as either Strings or + * {@link javax.mail.Header} objects. <p> + * + * This class is mostly intended for service providers. MimeMessage + * and MimeBody use this class for holding their headers. + * + * <hr> <strong>A note on RFC822 and MIME headers</strong><p> + * + * RFC822 and MIME header fields <strong>must</strong> contain only + * US-ASCII characters. If a header contains non US-ASCII characters, + * it must be encoded as per the rules in RFC 2047. The MimeUtility + * class provided in this package can be used to to achieve this. + * Callers of the <code>setHeader</code>, <code>addHeader</code>, and + * <code>addHeaderLine</code> methods are responsible for enforcing + * the MIME requirements for the specified headers. In addition, these + * header fields must be folded (wrapped) before being sent if they + * exceed the line length limitation for the transport (1000 bytes for + * SMTP). Received headers may have been folded. The application is + * responsible for folding and unfolding headers as appropriate. <p> + * + * The current implementation supports the System property + * <code>mail.mime.ignorewhitespacelines</code>, which if set to true + * will cause a line containing only whitespace to be considered + * a blank line terminating the header. + * + * @see javax.mail.internet.MimeUtility + * @author John Mani + * @author Bill Shannon + */ + +public class InternetHeaders { + private static final boolean ignoreWhitespaceLines = + PropUtil.getBooleanSystemProperty("mail.mime.ignorewhitespacelines", + false); + + /** + * An individual internet header. This class is only used by + * subclasses of InternetHeaders. <p> + * + * An InternetHeader object with a null value is used as a placeholder + * for headers of that name, to preserve the order of headers. + * A placeholder InternetHeader object with a name of ":" marks + * the location in the list of headers where new headers are + * added by default. + * + * @since JavaMail 1.4 + */ + protected static final class InternetHeader extends Header { + /* + * Note that the value field from the superclass + * isn't used in this class. We extract the value + * from the line field as needed. We store the line + * rather than just the value to ensure that we can + * get back the exact original line, with the original + * whitespace, etc. + */ + String line; // the entire RFC822 header "line", + // or null if placeholder + + /** + * Constructor that takes a line and splits out + * the header name. + * + * @param l the header line + */ + public InternetHeader(String l) { + super("", ""); // XXX - we'll change it later + int i = l.indexOf(':'); + if (i < 0) { + // should never happen + name = l.trim(); + } else { + name = l.substring(0, i).trim(); + } + line = l; + } + + /** + * Constructor that takes a header name and value. + * + * @param n the name of the header + * @param v the value of the header + */ + public InternetHeader(String n, String v) { + super(n, ""); + if (v != null) + line = n + ": " + v; + else + line = null; + } + + /** + * Return the "value" part of the header line. + */ + @Override + public String getValue() { + int i = line.indexOf(':'); + if (i < 0) + return line; + // skip whitespace after ':' + int j; + for (j = i + 1; j < line.length(); j++) { + char c = line.charAt(j); + if (!(c == ' ' || c == '\t' || c == '\r' || c == '\n')) + break; + } + return line.substring(j); + } + } + + /* + * The enumeration object used to enumerate an + * InternetHeaders object. Can return + * either a String or a Header object. + */ + static class MatchEnum { + private Iterator<InternetHeader> e; // enum object of headers List + // XXX - is this overkill? should we step through in index + // order instead? + private String names[]; // names to match, or not + private boolean match; // return matching headers? + private boolean want_line; // return header lines? + private InternetHeader next_header; // the next header to be returned + + /* + * Constructor. Initialize the enumeration for the entire + * List of headers, the set of headers, whether to return + * matching or non-matching headers, and whether to return + * header lines or Header objects. + */ + MatchEnum(List<InternetHeader> v, String n[], boolean m, boolean l) { + e = v.iterator(); + names = n; + match = m; + want_line = l; + next_header = null; + } + + /* + * Any more elements in this enumeration? + */ + public boolean hasMoreElements() { + // if necessary, prefetch the next matching header, + // and remember it. + if (next_header == null) + next_header = nextMatch(); + return next_header != null; + } + + /* + * Return the next element. + */ + public Object nextElement() { + if (next_header == null) + next_header = nextMatch(); + + if (next_header == null) + throw new NoSuchElementException("No more headers"); + + InternetHeader h = next_header; + next_header = null; + if (want_line) + return h.line; + else + return new Header(h.getName(), h.getValue()); + } + + /* + * Return the next Header object according to the match + * criteria, or null if none left. + */ + private InternetHeader nextMatch() { + next: + while (e.hasNext()) { + InternetHeader h = e.next(); + + // skip "place holder" headers + if (h.line == null) + continue; + + // if no names to match against, return appropriately + if (names == null) + return match ? null : h; + + // check whether this header matches any of the names + for (int i = 0; i < names.length; i++) { + if (names[i].equalsIgnoreCase(h.getName())) { + if (match) + return h; + else + // found a match, but we're + // looking for non-matches. + // try next header. + continue next; + } + } + // found no matches. if that's what we wanted, return it. + if (!match) + return h; + } + return null; + } + } + + static class MatchStringEnum extends MatchEnum + implements Enumeration<String> { + + MatchStringEnum(List<InternetHeader> v, String[] n, boolean m) { + super(v, n, m, true); + } + + @Override + public String nextElement() { + return (String) super.nextElement(); + } + + } + + static class MatchHeaderEnum extends MatchEnum + implements Enumeration<Header> { + + MatchHeaderEnum(List<InternetHeader> v, String[] n, boolean m) { + super(v, n, m, false); + } + + @Override + public Header nextElement() { + return (Header) super.nextElement(); + } + + } + + /** + * The actual list of Headers, including placeholder entries. + * Placeholder entries are Headers with a null value and + * are never seen by clients of the InternetHeaders class. + * Placeholder entries are used to keep track of the preferred + * order of headers. Headers are never actually removed from + * the list, they're converted into placeholder entries. + * New headers are added after existing headers of the same name + * (or before in the case of <code>Received</code> and + * <code>Return-Path</code> headers). If no existing header + * or placeholder for the header is found, new headers are + * added after the special placeholder with the name ":". + * + * @since JavaMail 1.4 + */ + protected List<InternetHeader> headers; + + /** + * Create an empty InternetHeaders object. Placeholder entries + * are inserted to indicate the preferred order of headers. + */ + public InternetHeaders() { + headers = new ArrayList<>(40); + headers.add(new InternetHeader("Return-Path", null)); + headers.add(new InternetHeader("Received", null)); + headers.add(new InternetHeader("Resent-Date", null)); + headers.add(new InternetHeader("Resent-From", null)); + headers.add(new InternetHeader("Resent-Sender", null)); + headers.add(new InternetHeader("Resent-To", null)); + headers.add(new InternetHeader("Resent-Cc", null)); + headers.add(new InternetHeader("Resent-Bcc", null)); + headers.add(new InternetHeader("Resent-Message-Id", null)); + headers.add(new InternetHeader("Date", null)); + headers.add(new InternetHeader("From", null)); + headers.add(new InternetHeader("Sender", null)); + headers.add(new InternetHeader("Reply-To", null)); + headers.add(new InternetHeader("To", null)); + headers.add(new InternetHeader("Cc", null)); + headers.add(new InternetHeader("Bcc", null)); + headers.add(new InternetHeader("Message-Id", null)); + headers.add(new InternetHeader("In-Reply-To", null)); + headers.add(new InternetHeader("References", null)); + headers.add(new InternetHeader("Subject", null)); + headers.add(new InternetHeader("Comments", null)); + headers.add(new InternetHeader("Keywords", null)); + headers.add(new InternetHeader("Errors-To", null)); + headers.add(new InternetHeader("MIME-Version", null)); + headers.add(new InternetHeader("Content-Type", null)); + headers.add(new InternetHeader("Content-Transfer-Encoding", null)); + headers.add(new InternetHeader("Content-MD5", null)); + headers.add(new InternetHeader(":", null)); + headers.add(new InternetHeader("Content-Length", null)); + headers.add(new InternetHeader("Status", null)); + } + + /** + * Read and parse the given RFC822 message stream till the + * blank line separating the header from the body. The input + * stream is left positioned at the start of the body. The + * header lines are stored internally. <p> + * + * For efficiency, wrap a BufferedInputStream around the actual + * input stream and pass it as the parameter. <p> + * + * No placeholder entries are inserted; the original order of + * the headers is preserved. + * + * @param is RFC822 input stream + * @exception MessagingException for any I/O error reading the stream + */ + public InternetHeaders(InputStream is) throws MessagingException { + this(is, false); + } + + /** + * Read and parse the given RFC822 message stream till the + * blank line separating the header from the body. The input + * stream is left positioned at the start of the body. The + * header lines are stored internally. <p> + * + * For efficiency, wrap a BufferedInputStream around the actual + * input stream and pass it as the parameter. <p> + * + * No placeholder entries are inserted; the original order of + * the headers is preserved. + * + * @param is RFC822 input stream + * @param allowutf8 if UTF-8 encoded headers are allowed + * @exception MessagingException for any I/O error reading the stream + * @since JavaMail 1.6 + */ + public InternetHeaders(InputStream is, boolean allowutf8) + throws MessagingException { + headers = new ArrayList<>(40); + load(is, allowutf8); + } + + /** + * Read and parse the given RFC822 message stream till the + * blank line separating the header from the body. Store the + * header lines inside this InternetHeaders object. The order + * of header lines is preserved. <p> + * + * Note that the header lines are added into this InternetHeaders + * object, so any existing headers in this object will not be + * affected. Headers are added to the end of the existing list + * of headers, in order. + * + * @param is RFC822 input stream + * @exception MessagingException for any I/O error reading the stream + */ + public void load(InputStream is) throws MessagingException { + load(is, false); + } + + /** + * Read and parse the given RFC822 message stream till the + * blank line separating the header from the body. Store the + * header lines inside this InternetHeaders object. The order + * of header lines is preserved. <p> + * + * Note that the header lines are added into this InternetHeaders + * object, so any existing headers in this object will not be + * affected. Headers are added to the end of the existing list + * of headers, in order. + * + * @param is RFC822 input stream + * @param allowutf8 if UTF-8 encoded headers are allowed + * @exception MessagingException for any I/O error reading the stream + * @since JavaMail 1.6 + */ + public void load(InputStream is, boolean allowutf8) + throws MessagingException { + // Read header lines until a blank line. It is valid + // to have BodyParts with no header lines. + String line; + LineInputStream lis = new LineInputStream(is, allowutf8); + String prevline = null; // the previous header line, as a string + // a buffer to accumulate the header in, when we know it's needed + StringBuilder lineBuffer = new StringBuilder(); + + try { + // if the first line being read is a continuation line, + // we ignore it if it's otherwise empty or we treat it as + // a non-continuation line if it has non-whitespace content + boolean first = true; + do { + line = lis.readLine(); + if (line != null && + (line.startsWith(" ") || line.startsWith("\t"))) { + // continuation of header + if (prevline != null) { + lineBuffer.append(prevline); + prevline = null; + } + if (first) { + String lt = line.trim(); + if (lt.length() > 0) + lineBuffer.append(lt); + } else { + if (lineBuffer.length() > 0) + lineBuffer.append("\r\n"); + lineBuffer.append(line); + } + } else { + // new header + if (prevline != null) + addHeaderLine(prevline); + else if (lineBuffer.length() > 0) { + // store previous header first + addHeaderLine(lineBuffer.toString()); + lineBuffer.setLength(0); + } + prevline = line; + } + first = false; + } while (line != null && !isEmpty(line)); + } catch (IOException ioex) { + throw new MessagingException("Error in input stream", ioex); + } + } + + /** + * Is this line an empty (blank) line? + */ + private static final boolean isEmpty(String line) { + return line.length() == 0 || + (ignoreWhitespaceLines && line.trim().length() == 0); + } + + /** + * Return all the values for the specified header. The + * values are String objects. Returns <code>null</code> + * if no headers with the specified name exist. + * + * @param name header name + * @return array of header values, or null if none + */ + public String[] getHeader(String name) { + Iterator<InternetHeader> e = headers.iterator(); + // XXX - should we just step through in index order? + List<String> v = new ArrayList<>(); // accumulate return values + + while (e.hasNext()) { + InternetHeader h = e.next(); + if (name.equalsIgnoreCase(h.getName()) && h.line != null) { + v.add(h.getValue()); + } + } + if (v.size() == 0) + return (null); + // convert List to an array for return + String r[] = new String[v.size()]; + r = v.toArray(r); + return (r); + } + + /** + * Get all the headers for this header name, returned as a single + * String, with headers separated by the delimiter. If the + * delimiter is <code>null</code>, only the first header is + * returned. Returns <code>null</code> + * if no headers with the specified name exist. + * + * @param name header name + * @param delimiter delimiter + * @return the value fields for all headers with + * this name, or null if none + */ + public String getHeader(String name, String delimiter) { + String s[] = getHeader(name); + + if (s == null) + return null; + + if ((s.length == 1) || delimiter == null) + return s[0]; + + StringBuilder r = new StringBuilder(s[0]); + for (int i = 1; i < s.length; i++) { + r.append(delimiter); + r.append(s[i]); + } + return r.toString(); + } + + /** + * Change the first header line that matches name + * to have value, adding a new header if no existing header + * matches. Remove all matching headers but the first. <p> + * + * Note that RFC822 headers can only contain US-ASCII characters + * + * @param name header name + * @param value header value + */ + public void setHeader(String name, String value) { + boolean found = false; + + for (int i = 0; i < headers.size(); i++) { + InternetHeader h = headers.get(i); + if (name.equalsIgnoreCase(h.getName())) { + if (!found) { + int j; + if (h.line != null && (j = h.line.indexOf(':')) >= 0) { + h.line = h.line.substring(0, j + 1) + " " + value; + // preserves capitalization, spacing + } else { + h.line = name + ": " + value; + } + found = true; + } else { + headers.remove(i); + i--; // have to look at i again + } + } + } + + if (!found) { + addHeader(name, value); + } + } + + /** + * Add a header with the specified name and value to the header list. <p> + * + * The current implementation knows about the preferred order of most + * well-known headers and will insert headers in that order. In + * addition, it knows that <code>Received</code> headers should be + * inserted in reverse order (newest before oldest), and that they + * should appear at the beginning of the headers, preceeded only by + * a possible <code>Return-Path</code> header. <p> + * + * Note that RFC822 headers can only contain US-ASCII characters. + * + * @param name header name + * @param value header value + */ + public void addHeader(String name, String value) { + int pos = headers.size(); + boolean addReverse = + name.equalsIgnoreCase("Received") || + name.equalsIgnoreCase("Return-Path"); + if (addReverse) + pos = 0; + for (int i = headers.size() - 1; i >= 0; i--) { + InternetHeader h = headers.get(i); + if (name.equalsIgnoreCase(h.getName())) { + if (addReverse) { + pos = i; + } else { + headers.add(i + 1, new InternetHeader(name, value)); + return; + } + } + // marker for default place to add new headers + if (!addReverse && h.getName().equals(":")) + pos = i; + } + headers.add(pos, new InternetHeader(name, value)); + } + + /** + * Remove all header entries that match the given name + * @param name header name + */ + public void removeHeader(String name) { + for (int i = 0; i < headers.size(); i++) { + InternetHeader h = headers.get(i); + if (name.equalsIgnoreCase(h.getName())) { + h.line = null; + //headers.remove(i); + //i--; // have to look at i again + } + } + } + + /** + * Return all the headers as an Enumeration of + * {@link javax.mail.Header} objects. + * + * @return Enumeration of Header objects + */ + public Enumeration<Header> getAllHeaders() { + return (new MatchHeaderEnum(headers, null, false)); + } + + /** + * Return all matching {@link javax.mail.Header} objects. + * + * @param names the headers to return + * @return Enumeration of matching Header objects + */ + public Enumeration<Header> getMatchingHeaders(String[] names) { + return (new MatchHeaderEnum(headers, names, true)); + } + + /** + * Return all non-matching {@link javax.mail.Header} objects. + * + * @param names the headers to not return + * @return Enumeration of non-matching Header objects + */ + public Enumeration<Header> getNonMatchingHeaders(String[] names) { + return (new MatchHeaderEnum(headers, names, false)); + } + + /** + * Add an RFC822 header line to the header store. + * If the line starts with a space or tab (a continuation line), + * add it to the last header line in the list. Otherwise, + * append the new header line to the list. <p> + * + * Note that RFC822 headers can only contain US-ASCII characters + * + * @param line raw RFC822 header line + */ + public void addHeaderLine(String line) { + try { + char c = line.charAt(0); + if (c == ' ' || c == '\t') { + InternetHeader h = headers.get(headers.size() - 1); + h.line += "\r\n" + line; + } else + headers.add(new InternetHeader(line)); + } catch (StringIndexOutOfBoundsException e) { + // line is empty, ignore it + return; + } catch (NoSuchElementException e) { + // XXX - list is empty? + } + } + + /** + * Return all the header lines as an Enumeration of Strings. + * + * @return Enumeration of Strings of all header lines + */ + public Enumeration<String> getAllHeaderLines() { + return (getNonMatchingHeaderLines(null)); + } + + /** + * Return all matching header lines as an Enumeration of Strings. + * + * @param names the headers to return + * @return Enumeration of Strings of all matching header lines + */ + public Enumeration<String> getMatchingHeaderLines(String[] names) { + return (new MatchStringEnum(headers, names, true)); + } + + /** + * Return all non-matching header lines + * + * @param names the headers to not return + * @return Enumeration of Strings of all non-matching header lines + */ + public Enumeration<String> getNonMatchingHeaderLines(String[] names) { + return (new MatchStringEnum(headers, names, false)); + } +}
diff --git a/mail/src/main/java/javax/mail/internet/MailDateFormat.java b/mail/src/main/java/javax/mail/internet/MailDateFormat.java new file mode 100644 index 0000000..0bd47b9 --- /dev/null +++ b/mail/src/main/java/javax/mail/internet/MailDateFormat.java
@@ -0,0 +1,1042 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectStreamException; +import java.util.Date; +import java.util.Calendar; +import java.util.Locale; +import java.util.TimeZone; +import java.util.logging.Level; +import java.text.DateFormatSymbols; +import java.text.SimpleDateFormat; +import java.text.NumberFormat; +import java.text.FieldPosition; +import java.text.ParsePosition; +import java.text.ParseException; + +import com.sun.mail.util.MailLogger; + +/** + * Formats and parses date specification based on + * <a href="http://www.ietf.org/rfc/rfc2822.txt" target="_top">RFC 2822</a>. <p> + * + * This class does not support methods that influence the format. It always + * formats the date based on the specification below.<p> + * + * 3.3. Date and Time Specification + * <p> + * Date and time occur in several header fields. This section specifies + * the syntax for a full date and time specification. Though folding + * white space is permitted throughout the date-time specification, it is + * RECOMMENDED that a single space be used in each place that FWS appears + * (whether it is required or optional); some older implementations may + * not interpret other occurrences of folding white space correctly. + * <pre> + * date-time = [ day-of-week "," ] date FWS time [CFWS] + * + * day-of-week = ([FWS] day-name) / obs-day-of-week + * + * day-name = "Mon" / "Tue" / "Wed" / "Thu" / + * "Fri" / "Sat" / "Sun" + * + * date = day month year + * + * year = 4*DIGIT / obs-year + * + * month = (FWS month-name FWS) / obs-month + * + * month-name = "Jan" / "Feb" / "Mar" / "Apr" / + * "May" / "Jun" / "Jul" / "Aug" / + * "Sep" / "Oct" / "Nov" / "Dec" + * + * day = ([FWS] 1*2DIGIT) / obs-day + * + * time = time-of-day FWS zone + * + * time-of-day = hour ":" minute [ ":" second ] + * + * hour = 2DIGIT / obs-hour + * + * minute = 2DIGIT / obs-minute + * + * second = 2DIGIT / obs-second + * + * zone = (( "+" / "-" ) 4DIGIT) / obs-zone + * </pre> + * The day is the numeric day of the month. The year is any numeric year + * 1900 or later. + * <p> + * The time-of-day specifies the number of hours, minutes, and optionally + * seconds since midnight of the date indicated. + * <p> + * The date and time-of-day SHOULD express local time. + * <p> + * The zone specifies the offset from Coordinated Universal Time (UTC, + * formerly referred to as "Greenwich Mean Time") that the date and + * time-of-day represent. The "+" or "-" indicates whether the + * time-of-day is ahead of (i.e., east of) or behind (i.e., west of) + * Universal Time. The first two digits indicate the number of hours + * difference from Universal Time, and the last two digits indicate the + * number of minutes difference from Universal Time. (Hence, +hhmm means + * +(hh * 60 + mm) minutes, and -hhmm means -(hh * 60 + mm) minutes). The + * form "+0000" SHOULD be used to indicate a time zone at Universal Time. + * Though "-0000" also indicates Universal Time, it is used to indicate + * that the time was generated on a system that may be in a local time + * zone other than Universal Time and therefore indicates that the + * date-time contains no information about the local time zone. + * <p> + * A date-time specification MUST be semantically valid. That is, the + * day-of-the-week (if included) MUST be the day implied by the date, the + * numeric day-of-month MUST be between 1 and the number of days allowed + * for the specified month (in the specified year), the time-of-day MUST + * be in the range 00:00:00 through 23:59:60 (the number of seconds + * allowing for a leap second; see [STD12]), and the zone MUST be within + * the range -9959 through +9959. + * + * <h3><a name="synchronization">Synchronization</a></h3> + * + * <p> + * Date formats are not synchronized. + * It is recommended to create separate format instances for each thread. + * If multiple threads access a format concurrently, it must be synchronized + * externally. + * + * @author Anthony Vanelverdinghe + * @author Max Spivak + * @since JavaMail 1.2 + */ +public class MailDateFormat extends SimpleDateFormat { + + private static final long serialVersionUID = -8148227605210628779L; + private static final String PATTERN = "EEE, d MMM yyyy HH:mm:ss Z (z)"; + + private static final MailLogger LOGGER = new MailLogger( + MailDateFormat.class, "DEBUG", false, System.out); + + private static final int UNKNOWN_DAY_NAME = -1; + private static final TimeZone UTC = TimeZone.getTimeZone("UTC"); + private static final int LEAP_SECOND = 60; + + /** + * Create a new date format for the RFC2822 specification with lenient + * parsing. + */ + public MailDateFormat() { + super(PATTERN, Locale.US); + } + + /** + * Allows to serialize instances such that they are deserializable with the + * previous implementation. + * + * @return the object to be serialized + * @throws ObjectStreamException never + */ + private Object writeReplace() throws ObjectStreamException { + MailDateFormat fmt = new MailDateFormat(); + fmt.superApplyPattern("EEE, d MMM yyyy HH:mm:ss 'XXXXX' (z)"); + fmt.setTimeZone(getTimeZone()); + return fmt; + } + + /** + * Allows to deserialize instances that were serialized with the previous + * implementation. + * + * @param in the stream containing the serialized object + * @throws IOException on read failures + * @throws ClassNotFoundException never + */ + private void readObject(ObjectInputStream in) + throws IOException, ClassNotFoundException { + in.defaultReadObject(); + super.applyPattern(PATTERN); + } + + /** + * Overrides Cloneable. + * + * @return a clone of this instance + * @since JavaMail 1.6 + */ + @Override + public MailDateFormat clone() { + return (MailDateFormat) super.clone(); + } + + /** + * Formats the given date in the format specified by + * RFC 2822 in the current TimeZone. + * + * @param date the Date object + * @param dateStrBuf the formatted string + * @param fieldPosition the current field position + * @return StringBuffer the formatted String + * @since JavaMail 1.2 + */ + @Override + public StringBuffer format(Date date, StringBuffer dateStrBuf, + FieldPosition fieldPosition) { + return super.format(date, dateStrBuf, fieldPosition); + } + + /** + * Parses the given date in the format specified by + * RFC 2822. + * <ul> + * <li>With strict parsing, obs-* tokens are unsupported. Lenient parsing + * supports obs-year and obs-zone, with the exception of the 1-character + * military time zones. + * <li>The optional CFWS token at the end is not parsed. + * <li>RFC 2822 specifies that a zone of "-0000" indicates that the + * date-time contains no information about the local time zone. This class + * uses the UTC time zone in this case. + * </ul> + * + * @param text the formatted date to be parsed + * @param pos the current parse position + * @return Date the parsed date. In case of error, returns null. + * @since JavaMail 1.2 + */ + @Override + public Date parse(String text, ParsePosition pos) { + if (text == null || pos == null) { + throw new NullPointerException(); + } else if (0 > pos.getIndex() || pos.getIndex() >= text.length()) { + return null; + } + + return isLenient() + ? new Rfc2822LenientParser(text, pos).parse() + : new Rfc2822StrictParser(text, pos).parse(); + } + + /** + * This method always throws an UnsupportedOperationException and should not + * be used because RFC 2822 mandates a specific calendar. + * + * @throws UnsupportedOperationException if this method is invoked + */ + @Override + public void setCalendar(Calendar newCalendar) { + throw new UnsupportedOperationException("Method " + + "setCalendar() shouldn't be called"); + } + + /** + * This method always throws an UnsupportedOperationException and should not + * be used because RFC 2822 mandates a specific number format. + * + * @throws UnsupportedOperationException if this method is invoked + */ + @Override + public void setNumberFormat(NumberFormat newNumberFormat) { + throw new UnsupportedOperationException("Method " + + "setNumberFormat() shouldn't be called"); + } + + /** + * This method always throws an UnsupportedOperationException and should not + * be used because RFC 2822 mandates a specific pattern. + * + * @throws UnsupportedOperationException if this method is invoked + * @since JavaMail 1.6 + */ + @Override + public void applyLocalizedPattern(String pattern) { + throw new UnsupportedOperationException("Method " + + "applyLocalizedPattern() shouldn't be called"); + } + + /** + * This method always throws an UnsupportedOperationException and should not + * be used because RFC 2822 mandates a specific pattern. + * + * @throws UnsupportedOperationException if this method is invoked + * @since JavaMail 1.6 + */ + @Override + public void applyPattern(String pattern) { + throw new UnsupportedOperationException("Method " + + "applyPattern() shouldn't be called"); + } + + /** + * This method allows serialization to change the pattern. + */ + private void superApplyPattern(String pattern) { + super.applyPattern(pattern); + } + + /** + * This method always throws an UnsupportedOperationException and should not + * be used because RFC 2822 mandates another strategy for interpreting + * 2-digits years. + * + * @return the start of the 100-year period into which two digit years are + * parsed + * @throws UnsupportedOperationException if this method is invoked + * @since JavaMail 1.6 + */ + @Override + public Date get2DigitYearStart() { + throw new UnsupportedOperationException("Method " + + "get2DigitYearStart() shouldn't be called"); + } + + /** + * This method always throws an UnsupportedOperationException and should not + * be used because RFC 2822 mandates another strategy for interpreting + * 2-digits years. + * + * @throws UnsupportedOperationException if this method is invoked + * @since JavaMail 1.6 + */ + @Override + public void set2DigitYearStart(Date startDate) { + throw new UnsupportedOperationException("Method " + + "set2DigitYearStart() shouldn't be called"); + } + + /** + * This method always throws an UnsupportedOperationException and should not + * be used because RFC 2822 mandates specific date format symbols. + * + * @throws UnsupportedOperationException if this method is invoked + * @since JavaMail 1.6 + */ + @Override + public void setDateFormatSymbols(DateFormatSymbols newFormatSymbols) { + throw new UnsupportedOperationException("Method " + + "setDateFormatSymbols() shouldn't be called"); + } + + /** + * Returns the date, as specified by the parameters. + * + * @param dayName + * @param day + * @param month + * @param year + * @param hour + * @param minute + * @param second + * @param zone + * @return the date, as specified by the parameters + * @throws IllegalArgumentException if this instance's Calendar is + * non-lenient and any of the parameters have invalid values, or if dayName + * is not consistent with day-month-year + */ + private Date toDate(int dayName, int day, int month, int year, + int hour, int minute, int second, int zone) { + if (second == LEAP_SECOND) { + second = 59; + } + + TimeZone tz = calendar.getTimeZone(); + try { + calendar.setTimeZone(UTC); + calendar.clear(); + calendar.set(year, month, day, hour, minute, second); + + if (dayName == UNKNOWN_DAY_NAME + || dayName == calendar.get(Calendar.DAY_OF_WEEK)) { + calendar.add(Calendar.MINUTE, zone); + return calendar.getTime(); + } else { + throw new IllegalArgumentException("Inconsistent day-name"); + } + } finally { + calendar.setTimeZone(tz); + } + } + + /** + * This class provides the building blocks for date parsing. + * <p> + * It has the following invariants: + * <ul> + * <li>no exceptions are thrown, except for java.text.ParseException from + * parse* methods + * <li>when parse* throws ParseException OR get* returns INVALID_CHAR OR + * skip* returns false OR peek* is invoked, then pos.getIndex() on method + * exit is the same as it was on method entry + * </ul> + */ + private static abstract class AbstractDateParser { + + static final int INVALID_CHAR = -1; + static final int MAX_YEAR_DIGITS = 8; // guarantees that: + // year < new GregorianCalendar().getMaximum(Calendar.YEAR) + + final String text; + final ParsePosition pos; + + AbstractDateParser(String text, ParsePosition pos) { + this.text = text; + this.pos = pos; + } + + final Date parse() { + int startPosition = pos.getIndex(); + try { + return tryParse(); + } catch (Exception e) { // == ParseException | RuntimeException e + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.log(Level.FINE, "Bad date: '" + text + "'", e); + } + pos.setErrorIndex(pos.getIndex()); + pos.setIndex(startPosition); + return null; + } + } + + abstract Date tryParse() throws ParseException; + + /** + * @return the java.util.Calendar constant for the parsed day name + */ + final int parseDayName() throws ParseException { + switch (getChar()) { + case 'S': + if (skipPair('u', 'n')) { + return Calendar.SUNDAY; + } else if (skipPair('a', 't')) { + return Calendar.SATURDAY; + } + break; + case 'T': + if (skipPair('u', 'e')) { + return Calendar.TUESDAY; + } else if (skipPair('h', 'u')) { + return Calendar.THURSDAY; + } + break; + case 'M': + if (skipPair('o', 'n')) { + return Calendar.MONDAY; + } + break; + case 'W': + if (skipPair('e', 'd')) { + return Calendar.WEDNESDAY; + } + break; + case 'F': + if (skipPair('r', 'i')) { + return Calendar.FRIDAY; + } + break; + case INVALID_CHAR: + throw new ParseException("Invalid day-name", + pos.getIndex()); + } + pos.setIndex(pos.getIndex() - 1); + throw new ParseException("Invalid day-name", pos.getIndex()); + } + + /** + * @return the java.util.Calendar constant for the parsed month name + */ + @SuppressWarnings("fallthrough") + final int parseMonthName(boolean caseSensitive) throws ParseException { + switch (getChar()) { + case 'j': + if (caseSensitive) { + break; + } + case 'J': + if (skipChar('u') || (!caseSensitive && skipChar('U'))) { + if (skipChar('l') || (!caseSensitive + && skipChar('L'))) { + return Calendar.JULY; + } else if (skipChar('n') || (!caseSensitive + && skipChar('N'))) { + return Calendar.JUNE; + } else { + pos.setIndex(pos.getIndex() - 1); + } + } else if (skipPair('a', 'n') || (!caseSensitive + && skipAlternativePair('a', 'A', 'n', 'N'))) { + return Calendar.JANUARY; + } + break; + case 'm': + if (caseSensitive) { + break; + } + case 'M': + if (skipChar('a') || (!caseSensitive && skipChar('A'))) { + if (skipChar('r') || (!caseSensitive + && skipChar('R'))) { + return Calendar.MARCH; + } else if (skipChar('y') || (!caseSensitive + && skipChar('Y'))) { + return Calendar.MAY; + } else { + pos.setIndex(pos.getIndex() - 1); + } + } + break; + case 'a': + if (caseSensitive) { + break; + } + case 'A': + if (skipPair('u', 'g') || (!caseSensitive + && skipAlternativePair('u', 'U', 'g', 'G'))) { + return Calendar.AUGUST; + } else if (skipPair('p', 'r') || (!caseSensitive + && skipAlternativePair('p', 'P', 'r', 'R'))) { + return Calendar.APRIL; + } + break; + case 'd': + if (caseSensitive) { + break; + } + case 'D': + if (skipPair('e', 'c') || (!caseSensitive + && skipAlternativePair('e', 'E', 'c', 'C'))) { + return Calendar.DECEMBER; + } + break; + case 'o': + if (caseSensitive) { + break; + } + case 'O': + if (skipPair('c', 't') || (!caseSensitive + && skipAlternativePair('c', 'C', 't', 'T'))) { + return Calendar.OCTOBER; + } + break; + case 's': + if (caseSensitive) { + break; + } + case 'S': + if (skipPair('e', 'p') || (!caseSensitive + && skipAlternativePair('e', 'E', 'p', 'P'))) { + return Calendar.SEPTEMBER; + } + break; + case 'n': + if (caseSensitive) { + break; + } + case 'N': + if (skipPair('o', 'v') || (!caseSensitive + && skipAlternativePair('o', 'O', 'v', 'V'))) { + return Calendar.NOVEMBER; + } + break; + case 'f': + if (caseSensitive) { + break; + } + case 'F': + if (skipPair('e', 'b') || (!caseSensitive + && skipAlternativePair('e', 'E', 'b', 'B'))) { + return Calendar.FEBRUARY; + } + break; + case INVALID_CHAR: + throw new ParseException("Invalid month", pos.getIndex()); + } + pos.setIndex(pos.getIndex() - 1); + throw new ParseException("Invalid month", pos.getIndex()); + } + + /** + * @return the number of minutes to be added to the time in the local + * time zone, in order to obtain the equivalent time in the UTC time + * zone. Returns 0 if the date-time contains no information about the + * local time zone. + */ + final int parseZoneOffset() throws ParseException { + int sign = getChar(); + if (sign == '+' || sign == '-') { + int offset = parseAsciiDigits(4, 4, true); + if (!isValidZoneOffset(offset)) { + pos.setIndex(pos.getIndex() - 5); + throw new ParseException("Invalid zone", pos.getIndex()); + } + + return ((sign == '+') ? -1 : 1) + * (offset / 100 * 60 + offset % 100); + } else if (sign != INVALID_CHAR) { + pos.setIndex(pos.getIndex() - 1); + } + throw new ParseException("Invalid zone", pos.getIndex()); + } + + boolean isValidZoneOffset(int offset) { + return (offset % 100) < 60; + } + + final int parseAsciiDigits(int count) throws ParseException { + return parseAsciiDigits(count, count); + } + + final int parseAsciiDigits(int min, int max) throws ParseException { + return parseAsciiDigits(min, max, false); + } + + final int parseAsciiDigits(int min, int max, boolean isEOF) + throws ParseException { + int result = 0; + int nbDigitsParsed = 0; + while (nbDigitsParsed < max && peekAsciiDigit()) { + result = result * 10 + getAsciiDigit(); + nbDigitsParsed++; + } + + if ((nbDigitsParsed < min) + || (nbDigitsParsed == max && !isEOF && peekAsciiDigit())) { + pos.setIndex(pos.getIndex() - nbDigitsParsed); + } else { + return result; + } + + String range = (min == max) + ? Integer.toString(min) + : "between " + min + " and " + max; + throw new ParseException("Invalid input: expected " + + range + " ASCII digits", pos.getIndex()); + } + + final void parseFoldingWhiteSpace() throws ParseException { + if (!skipFoldingWhiteSpace()) { + throw new ParseException("Invalid input: expected FWS", + pos.getIndex()); + } + } + + final void parseChar(char ch) throws ParseException { + if (!skipChar(ch)) { + throw new ParseException("Invalid input: expected '" + ch + "'", + pos.getIndex()); + } + } + + final int getAsciiDigit() { + int ch = getChar(); + if ('0' <= ch && ch <= '9') { + return Character.digit((char) ch, 10); + } else { + if (ch != INVALID_CHAR) { + pos.setIndex(pos.getIndex() - 1); + } + return INVALID_CHAR; + } + } + + final int getChar() { + if (pos.getIndex() < text.length()) { + char ch = text.charAt(pos.getIndex()); + pos.setIndex(pos.getIndex() + 1); + return ch; + } else { + return INVALID_CHAR; + } + } + + boolean skipFoldingWhiteSpace() { + // fast paths: a single ASCII space or no FWS + if (skipChar(' ')) { + if (!peekFoldingWhiteSpace()) { + return true; + } else { + pos.setIndex(pos.getIndex() - 1); + } + } else if (!peekFoldingWhiteSpace()) { + return false; + } + + // normal path + int startIndex = pos.getIndex(); + if (skipWhiteSpace()) { + while (skipNewline()) { + if (!skipWhiteSpace()) { + pos.setIndex(startIndex); + return false; + } + } + return true; + } else if (skipNewline() && skipWhiteSpace()) { + return true; + } else { + pos.setIndex(startIndex); + return false; + } + } + + final boolean skipWhiteSpace() { + int startIndex = pos.getIndex(); + while (skipAlternative(' ', '\t')) { /* empty */ } + return pos.getIndex() > startIndex; + } + + final boolean skipNewline() { + return skipPair('\r', '\n'); + } + + final boolean skipAlternativeTriple( + char firstStandard, char firstAlternative, + char secondStandard, char secondAlternative, + char thirdStandard, char thirdAlternative + ) { + if (skipAlternativePair(firstStandard, firstAlternative, + secondStandard, secondAlternative)) { + if (skipAlternative(thirdStandard, thirdAlternative)) { + return true; + } else { + pos.setIndex(pos.getIndex() - 2); + } + } + return false; + } + + final boolean skipAlternativePair( + char firstStandard, char firstAlternative, + char secondStandard, char secondAlternative + ) { + if (skipAlternative(firstStandard, firstAlternative)) { + if (skipAlternative(secondStandard, secondAlternative)) { + return true; + } else { + pos.setIndex(pos.getIndex() - 1); + } + } + return false; + } + + final boolean skipAlternative(char standard, char alternative) { + return skipChar(standard) || skipChar(alternative); + } + + final boolean skipPair(char first, char second) { + if (skipChar(first)) { + if (skipChar(second)) { + return true; + } else { + pos.setIndex(pos.getIndex() - 1); + } + } + return false; + } + + final boolean skipChar(char ch) { + if (pos.getIndex() < text.length() + && text.charAt(pos.getIndex()) == ch) { + pos.setIndex(pos.getIndex() + 1); + return true; + } else { + return false; + } + } + + final boolean peekAsciiDigit() { + return (pos.getIndex() < text.length() + && '0' <= text.charAt(pos.getIndex()) + && text.charAt(pos.getIndex()) <= '9'); + } + + boolean peekFoldingWhiteSpace() { + return (pos.getIndex() < text.length() + && (text.charAt(pos.getIndex()) == ' ' + || text.charAt(pos.getIndex()) == '\t' + || text.charAt(pos.getIndex()) == '\r')); + } + + final boolean peekChar(char ch) { + return (pos.getIndex() < text.length() + && text.charAt(pos.getIndex()) == ch); + } + + } + + private class Rfc2822StrictParser extends AbstractDateParser { + + Rfc2822StrictParser(String text, ParsePosition pos) { + super(text, pos); + } + + @Override + Date tryParse() throws ParseException { + int dayName = parseOptionalBegin(); + + int day = parseDay(); + int month = parseMonth(); + int year = parseYear(); + + parseFoldingWhiteSpace(); + + int hour = parseHour(); + parseChar(':'); + int minute = parseMinute(); + int second = (skipChar(':')) ? parseSecond() : 0; + + parseFwsBetweenTimeOfDayAndZone(); + + int zone = parseZone(); + + try { + return MailDateFormat.this.toDate(dayName, day, month, year, + hour, minute, second, zone); + } catch (IllegalArgumentException e) { + throw new ParseException("Invalid input: some of the calendar " + + "fields have invalid values, or day-name is " + + "inconsistent with date", pos.getIndex()); + } + } + + /** + * @return the java.util.Calendar constant for the parsed day name, or + * UNKNOWN_DAY_NAME iff the begin is missing + */ + int parseOptionalBegin() throws ParseException { + int dayName; + if (!peekAsciiDigit()) { + skipFoldingWhiteSpace(); + dayName = parseDayName(); + parseChar(','); + } else { + dayName = UNKNOWN_DAY_NAME; + } + return dayName; + } + + int parseDay() throws ParseException { + skipFoldingWhiteSpace(); + return parseAsciiDigits(1, 2); + } + + /** + * @return the java.util.Calendar constant for the parsed month name + */ + int parseMonth() throws ParseException { + parseFwsInMonth(); + int month = parseMonthName(isMonthNameCaseSensitive()); + parseFwsInMonth(); + return month; + } + + void parseFwsInMonth() throws ParseException { + parseFoldingWhiteSpace(); + } + + boolean isMonthNameCaseSensitive() { + return true; + } + + int parseYear() throws ParseException { + int year = parseAsciiDigits(4, MAX_YEAR_DIGITS); + if (year >= 1900) { + return year; + } else { + pos.setIndex(pos.getIndex() - 4); + while (text.charAt(pos.getIndex() - 1) == '0') { + pos.setIndex(pos.getIndex() - 1); + } + throw new ParseException("Invalid year", pos.getIndex()); + } + } + + int parseHour() throws ParseException { + return parseAsciiDigits(2); + } + + int parseMinute() throws ParseException { + return parseAsciiDigits(2); + } + + int parseSecond() throws ParseException { + return parseAsciiDigits(2); + } + + void parseFwsBetweenTimeOfDayAndZone() throws ParseException { + parseFoldingWhiteSpace(); + } + + int parseZone() throws ParseException { + return parseZoneOffset(); + } + + } + + private class Rfc2822LenientParser extends Rfc2822StrictParser { + + private Boolean hasDefaultFws; + + Rfc2822LenientParser(String text, ParsePosition pos) { + super(text, pos); + } + + @Override + int parseOptionalBegin() { + while (pos.getIndex() < text.length() && !peekAsciiDigit()) { + pos.setIndex(pos.getIndex() + 1); + } + + return UNKNOWN_DAY_NAME; + } + + @Override + int parseDay() throws ParseException { + skipFoldingWhiteSpace(); + return parseAsciiDigits(1, 3); + } + + @Override + void parseFwsInMonth() throws ParseException { + // '-' is allowed to accomodate for the date format as specified in + // <a href="http://www.ietf.org/rfc/rfc3501.txt">RFC 3501</a> + if (hasDefaultFws == null) { + hasDefaultFws = !skipChar('-'); + skipFoldingWhiteSpace(); + } else if (hasDefaultFws) { + skipFoldingWhiteSpace(); + } else { + parseChar('-'); + } + } + + @Override + boolean isMonthNameCaseSensitive() { + return false; + } + + @Override + int parseYear() throws ParseException { + int year = parseAsciiDigits(1, MAX_YEAR_DIGITS); + if (year >= 1000) { + return year; + } else if (year >= 50) { + return year + 1900; + } else { + return year + 2000; + } + } + + @Override + int parseHour() throws ParseException { + return parseAsciiDigits(1, 2); + } + + @Override + int parseMinute() throws ParseException { + return parseAsciiDigits(1, 2); + } + + @Override + int parseSecond() throws ParseException { + return parseAsciiDigits(1, 2); + } + + @Override + void parseFwsBetweenTimeOfDayAndZone() throws ParseException { + skipFoldingWhiteSpace(); + } + + @Override + int parseZone() throws ParseException { + try { + if (pos.getIndex() >= text.length()) { + throw new ParseException("Missing zone", pos.getIndex()); + } + + if (peekChar('+') || peekChar('-')) { + return parseZoneOffset(); + } else if (skipAlternativePair('U', 'u', 'T', 't')) { + return 0; + } else if (skipAlternativeTriple('G', 'g', 'M', 'm', + 'T', 't')) { + return 0; + } else { + int hoursOffset; + if (skipAlternative('E', 'e')) { + hoursOffset = 4; + } else if (skipAlternative('C', 'c')) { + hoursOffset = 5; + } else if (skipAlternative('M', 'm')) { + hoursOffset = 6; + } else if (skipAlternative('P', 'p')) { + hoursOffset = 7; + } else { + throw new ParseException("Invalid zone", + pos.getIndex()); + } + if (skipAlternativePair('S', 's', 'T', 't')) { + hoursOffset += 1; + } else if (skipAlternativePair('D', 'd', 'T', 't')) { + } else { + pos.setIndex(pos.getIndex() - 1); + throw new ParseException("Invalid zone", + pos.getIndex()); + } + return hoursOffset * 60; + } + } catch (ParseException e) { + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.log(Level.FINE, "No timezone? : '" + text + "'", e); + } + + return 0; + } + } + + @Override + boolean isValidZoneOffset(int offset) { + return true; + } + + @Override + boolean skipFoldingWhiteSpace() { + boolean result = peekFoldingWhiteSpace(); + + skipLoop: + while (pos.getIndex() < text.length()) { + switch (text.charAt(pos.getIndex())) { + case ' ': + case '\t': + case '\r': + case '\n': + pos.setIndex(pos.getIndex() + 1); + break; + default: + break skipLoop; + } + } + + return result; + } + + @Override + boolean peekFoldingWhiteSpace() { + return super.peekFoldingWhiteSpace() + || (pos.getIndex() < text.length() + && text.charAt(pos.getIndex()) == '\n'); + } + + } + +}
diff --git a/mail/src/main/java/javax/mail/internet/MimeBodyPart.java b/mail/src/main/java/javax/mail/internet/MimeBodyPart.java new file mode 100644 index 0000000..1dd3876 --- /dev/null +++ b/mail/src/main/java/javax/mail/internet/MimeBodyPart.java
@@ -0,0 +1,1712 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import javax.mail.*; +import javax.activation.*; +import java.io.*; +import java.util.*; +import com.sun.mail.util.PropUtil; +import com.sun.mail.util.ASCIIUtility; +import com.sun.mail.util.MimeUtil; +import com.sun.mail.util.MessageRemovedIOException; +import com.sun.mail.util.FolderClosedIOException; +import com.sun.mail.util.LineOutputStream; + +/** + * This class represents a MIME body part. It implements the + * <code>BodyPart</code> abstract class and the <code>MimePart</code> + * interface. MimeBodyParts are contained in <code>MimeMultipart</code> + * objects. <p> + * + * MimeBodyPart uses the <code>InternetHeaders</code> class to parse + * and store the headers of that body part. + * + * <hr><strong>A note on RFC 822 and MIME headers</strong><p> + * + * RFC 822 header fields <strong>must</strong> contain only + * US-ASCII characters. MIME allows non ASCII characters to be present + * in certain portions of certain headers, by encoding those characters. + * RFC 2047 specifies the rules for doing this. The MimeUtility + * class provided in this package can be used to to achieve this. + * Callers of the <code>setHeader</code>, <code>addHeader</code>, and + * <code>addHeaderLine</code> methods are responsible for enforcing + * the MIME requirements for the specified headers. In addition, these + * header fields must be folded (wrapped) before being sent if they + * exceed the line length limitation for the transport (1000 bytes for + * SMTP). Received headers may have been folded. The application is + * responsible for folding and unfolding headers as appropriate. <p> + * + * @author John Mani + * @author Bill Shannon + * @author Kanwar Oberoi + * @see javax.mail.Part + * @see javax.mail.internet.MimePart + * @see javax.mail.internet.MimeUtility + */ + +public class MimeBodyPart extends BodyPart implements MimePart { + + // Paranoia: + // allow this last minute change to be disabled if it causes problems + private static final boolean setDefaultTextCharset = + PropUtil.getBooleanSystemProperty( + "mail.mime.setdefaulttextcharset", true); + + private static final boolean setContentTypeFileName = + PropUtil.getBooleanSystemProperty( + "mail.mime.setcontenttypefilename", true); + + private static final boolean encodeFileName = + PropUtil.getBooleanSystemProperty("mail.mime.encodefilename", false); + private static final boolean decodeFileName = + PropUtil.getBooleanSystemProperty("mail.mime.decodefilename", false); + private static final boolean ignoreMultipartEncoding = + PropUtil.getBooleanSystemProperty( + "mail.mime.ignoremultipartencoding", true); + private static final boolean allowutf8 = + PropUtil.getBooleanSystemProperty("mail.mime.allowutf8", true); + + // Paranoia: + // allow this last minute change to be disabled if it causes problems + static final boolean cacheMultipart = // accessed by MimeMessage + PropUtil.getBooleanSystemProperty("mail.mime.cachemultipart", true); + + + /** + * The DataHandler object representing this Part's content. + */ + protected DataHandler dh; + + /** + * Byte array that holds the bytes of the content of this Part. + */ + protected byte[] content; + + /** + * If the data for this body part was supplied by an + * InputStream that implements the SharedInputStream interface, + * <code>contentStream</code> is another such stream representing + * the content of this body part. In this case, <code>content</code> + * will be null. + * + * @since JavaMail 1.2 + */ + protected InputStream contentStream; + + /** + * The InternetHeaders object that stores all the headers + * of this body part. + */ + protected InternetHeaders headers; + + /** + * If our content is a Multipart of Message object, we save it + * the first time it's created by parsing a stream so that changes + * to the contained objects will not be lost. + * + * If this field is not null, it's return by the {@link #getContent} + * method. The {@link #getContent} method sets this field if it + * would return a Multipart or MimeMessage object. This field is + * is cleared by the {@link #setDataHandler} method. + * + * @since JavaMail 1.5 + */ + protected Object cachedContent; + + /** + * An empty MimeBodyPart object is created. + * This body part maybe filled in by a client constructing a multipart + * message. + */ + public MimeBodyPart() { + super(); + headers = new InternetHeaders(); + } + + /** + * Constructs a MimeBodyPart by reading and parsing the data from + * the specified input stream. The parser consumes data till the end + * of the given input stream. The input stream must start at the + * beginning of a valid MIME body part and must terminate at the end + * of that body part. <p> + * + * Note that the "boundary" string that delimits body parts must + * <strong>not</strong> be included in the input stream. The intention + * is that the MimeMultipart parser will extract each body part's bytes + * from a multipart stream and feed them into this constructor, without + * the delimiter strings. + * + * @param is the body part Input Stream + * @exception MessagingException for failures + */ + public MimeBodyPart(InputStream is) throws MessagingException { + if (!(is instanceof ByteArrayInputStream) && + !(is instanceof BufferedInputStream) && + !(is instanceof SharedInputStream)) + is = new BufferedInputStream(is); + + headers = new InternetHeaders(is); + + if (is instanceof SharedInputStream) { + SharedInputStream sis = (SharedInputStream)is; + contentStream = sis.newStream(sis.getPosition(), -1); + } else { + try { + content = ASCIIUtility.getBytes(is); + } catch (IOException ioex) { + throw new MessagingException("Error reading input stream", ioex); + } + } + + } + + /** + * Constructs a MimeBodyPart using the given header and + * content bytes. <p> + * + * Used by providers. + * + * @param headers The header of this part + * @param content bytes representing the body of this part. + * @exception MessagingException for failures + */ + public MimeBodyPart(InternetHeaders headers, byte[] content) + throws MessagingException { + super(); + this.headers = headers; + this.content = content; + } + + /** + * Return the size of the content of this body part in bytes. + * Return -1 if the size cannot be determined. <p> + * + * Note that this number may not be an exact measure of the + * content size and may or may not account for any transfer + * encoding of the content. <p> + * + * This implementation returns the size of the <code>content</code> + * array (if not null), or, if <code>contentStream</code> is not + * null, and the <code>available</code> method returns a positive + * number, it returns that number as the size. Otherwise, it returns + * -1. + * + * @return size in bytes, or -1 if not known + */ + @Override + public int getSize() throws MessagingException { + if (content != null) + return content.length; + if (contentStream != null) { + try { + int size = contentStream.available(); + // only believe the size if it's greate than zero, since zero + // is the default returned by the InputStream class itself + if (size > 0) + return size; + } catch (IOException ex) { + // ignore it + } + } + return -1; + } + + /** + * Return the number of lines for the content of this Part. + * Return -1 if this number cannot be determined. <p> + * + * Note that this number may not be an exact measure of the + * content length and may or may not account for any transfer + * encoding of the content. <p> + * + * This implementation returns -1. + * + * @return number of lines, or -1 if not known + */ + @Override + public int getLineCount() throws MessagingException { + return -1; + } + + /** + * Returns the value of the RFC 822 "Content-Type" header field. + * This represents the content type of the content of this + * body part. This value must not be null. If this field is + * unavailable, "text/plain" should be returned. <p> + * + * This implementation uses <code>getHeader(name)</code> + * to obtain the requisite header field. + * + * @return Content-Type of this body part + */ + @Override + public String getContentType() throws MessagingException { + String s = getHeader("Content-Type", null); + s = MimeUtil.cleanContentType(this, s); + if (s == null) + s = "text/plain"; + return s; + } + + /** + * Is this Part of the specified MIME type? This method + * compares <strong>only the <code>primaryType</code> and + * <code>subType</code></strong>. + * The parameters of the content types are ignored. <p> + * + * For example, this method will return <code>true</code> when + * comparing a Part of content type <strong>"text/plain"</strong> + * with <strong>"text/plain; charset=foobar"</strong>. <p> + * + * If the <code>subType</code> of <code>mimeType</code> is the + * special character '*', then the subtype is ignored during the + * comparison. + * + * @exception MessagingException for failures + */ + @Override + public boolean isMimeType(String mimeType) throws MessagingException { + return isMimeType(this, mimeType); + } + + /** + * Returns the disposition from the "Content-Disposition" header field. + * This represents the disposition of this part. The disposition + * describes how the part should be presented to the user. <p> + * + * If the Content-Disposition field is unavailable, + * null is returned. <p> + * + * This implementation uses <code>getHeader(name)</code> + * to obtain the requisite header field. + * + * @exception MessagingException for failures + * @see #headers + */ + @Override + public String getDisposition() throws MessagingException { + return getDisposition(this); + } + + /** + * Set the disposition in the "Content-Disposition" header field + * of this body part. If the disposition is null, any existing + * "Content-Disposition" header field is removed. + * + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this body part is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + @Override + public void setDisposition(String disposition) throws MessagingException { + setDisposition(this, disposition); + } + + /** + * Returns the content transfer encoding from the + * "Content-Transfer-Encoding" header + * field. Returns <code>null</code> if the header is unavailable + * or its value is absent. <p> + * + * This implementation uses <code>getHeader(name)</code> + * to obtain the requisite header field. + * + * @see #headers + */ + @Override + public String getEncoding() throws MessagingException { + return getEncoding(this); + } + + /** + * Returns the value of the "Content-ID" header field. Returns + * <code>null</code> if the field is unavailable or its value is + * absent. <p> + * + * This implementation uses <code>getHeader(name)</code> + * to obtain the requisite header field. + */ + @Override + public String getContentID() throws MessagingException { + return getHeader("Content-Id", null); + } + + /** + * Set the "Content-ID" header field of this body part. + * If the <code>cid</code> parameter is null, any existing + * "Content-ID" is removed. + * + * @param cid the Content-ID + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this body part is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + * @since JavaMail 1.3 + */ + public void setContentID(String cid) throws MessagingException { + if (cid == null) + removeHeader("Content-ID"); + else + setHeader("Content-ID", cid); + } + + /** + * Return the value of the "Content-MD5" header field. Returns + * <code>null</code> if this field is unavailable or its value + * is absent. <p> + * + * This implementation uses <code>getHeader(name)</code> + * to obtain the requisite header field. + */ + @Override + public String getContentMD5() throws MessagingException { + return getHeader("Content-MD5", null); + } + + /** + * Set the "Content-MD5" header field of this body part. + * + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this body part is + * obtained from a READ_ONLY folder. + */ + @Override + public void setContentMD5(String md5) throws MessagingException { + setHeader("Content-MD5", md5); + } + + /** + * Get the languages specified in the Content-Language header + * of this MimePart. The Content-Language header is defined by + * RFC 1766. Returns <code>null</code> if this header is not + * available or its value is absent. <p> + * + * This implementation uses <code>getHeader(name)</code> + * to obtain the requisite header field. + */ + @Override + public String[] getContentLanguage() throws MessagingException { + return getContentLanguage(this); + } + + /** + * Set the Content-Language header of this MimePart. The + * Content-Language header is defined by RFC 1766. + * + * @param languages array of language tags + */ + @Override + public void setContentLanguage(String[] languages) + throws MessagingException { + setContentLanguage(this, languages); + } + + /** + * Returns the "Content-Description" header field of this body part. + * This typically associates some descriptive information with + * this part. Returns null if this field is unavailable or its + * value is absent. <p> + * + * If the Content-Description field is encoded as per RFC 2047, + * it is decoded and converted into Unicode. If the decoding or + * conversion fails, the raw data is returned as is. <p> + * + * This implementation uses <code>getHeader(name)</code> + * to obtain the requisite header field. + * + * @return content description + */ + @Override + public String getDescription() throws MessagingException { + return getDescription(this); + } + + /** + * Set the "Content-Description" header field for this body part. + * If the description parameter is <code>null</code>, then any + * existing "Content-Description" fields are removed. <p> + * + * If the description contains non US-ASCII characters, it will + * be encoded using the platform's default charset. If the + * description contains only US-ASCII characters, no encoding + * is done and it is used as is. <p> + * + * Note that if the charset encoding process fails, a + * MessagingException is thrown, and an UnsupportedEncodingException + * is included in the chain of nested exceptions within the + * MessagingException. + * + * @param description content description + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this body part is + * obtained from a READ_ONLY folder. + * @exception MessagingException otherwise; an + * UnsupportedEncodingException may be included + * in the exception chain if the charset + * conversion fails. + */ + @Override + public void setDescription(String description) throws MessagingException { + setDescription(description, null); + } + + /** + * Set the "Content-Description" header field for this body part. + * If the description parameter is <code>null</code>, then any + * existing "Content-Description" fields are removed. <p> + * + * If the description contains non US-ASCII characters, it will + * be encoded using the specified charset. If the description + * contains only US-ASCII characters, no encoding is done and + * it is used as is. <p> + * + * Note that if the charset encoding process fails, a + * MessagingException is thrown, and an UnsupportedEncodingException + * is included in the chain of nested exceptions within the + * MessagingException. + * + * @param description Description + * @param charset Charset for encoding + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this body part is + * obtained from a READ_ONLY folder. + * @exception MessagingException otherwise; an + * UnsupportedEncodingException may be included + * in the exception chain if the charset + * conversion fails. + */ + public void setDescription(String description, String charset) + throws MessagingException { + setDescription(this, description, charset); + } + + /** + * Get the filename associated with this body part. <p> + * + * Returns the value of the "filename" parameter from the + * "Content-Disposition" header field of this body part. If its + * not available, returns the value of the "name" parameter from + * the "Content-Type" header field of this body part. + * Returns <code>null</code> if both are absent. <p> + * + * If the <code>mail.mime.decodefilename</code> System property + * is set to true, the {@link MimeUtility#decodeText + * MimeUtility.decodeText} method will be used to decode the + * filename. While such encoding is not supported by the MIME + * spec, many mailers use this technique to support non-ASCII + * characters in filenames. The default value of this property + * is false. + * + * @return filename + */ + @Override + public String getFileName() throws MessagingException { + return getFileName(this); + } + + /** + * Set the filename associated with this body part, if possible. <p> + * + * Sets the "filename" parameter of the "Content-Disposition" + * header field of this body part. For compatibility with older + * mailers, the "name" parameter of the "Content-Type" header is + * also set. <p> + * + * If the <code>mail.mime.encodefilename</code> System property + * is set to true, the {@link MimeUtility#encodeText + * MimeUtility.encodeText} method will be used to encode the + * filename. While such encoding is not supported by the MIME + * spec, many mailers use this technique to support non-ASCII + * characters in filenames. The default value of this property + * is false. + * + * @param filename the file name + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this body part is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + @Override + public void setFileName(String filename) throws MessagingException { + setFileName(this, filename); + } + + /** + * Return a decoded input stream for this body part's "content". <p> + * + * This implementation obtains the input stream from the DataHandler. + * That is, it invokes getDataHandler().getInputStream(); + * + * @return an InputStream + * @exception IOException this is typically thrown by the + * DataHandler. Refer to the documentation for + * javax.activation.DataHandler for more details. + * @exception MessagingException for other failures + * + * @see #getContentStream + * @see javax.activation.DataHandler#getInputStream + */ + @Override + public InputStream getInputStream() + throws IOException, MessagingException { + return getDataHandler().getInputStream(); + } + + /** + * Produce the raw bytes of the content. This method is used + * when creating a DataHandler object for the content. Subclasses + * that can provide a separate input stream for just the Part + * content might want to override this method. <p> + * + * @return an InputStream containing the raw bytes + * @exception MessagingException for failures + * @see #content + * @see MimeMessage#getContentStream + */ + protected InputStream getContentStream() throws MessagingException { + if (contentStream != null) + return ((SharedInputStream)contentStream).newStream(0, -1); + if (content != null) + return new ByteArrayInputStream(content); + + throw new MessagingException("No MimeBodyPart content"); + } + + /** + * Return an InputStream to the raw data with any Content-Transfer-Encoding + * intact. This method is useful if the "Content-Transfer-Encoding" + * header is incorrect or corrupt, which would prevent the + * <code>getInputStream</code> method or <code>getContent</code> method + * from returning the correct data. In such a case the application may + * use this method and attempt to decode the raw data itself. <p> + * + * This implementation simply calls the <code>getContentStream</code> + * method. + * + * @return an InputStream containing the raw bytes + * @exception MessagingException for failures + * @see #getInputStream + * @see #getContentStream + * @since JavaMail 1.2 + */ + public InputStream getRawInputStream() throws MessagingException { + return getContentStream(); + } + + /** + * Return a DataHandler for this body part's content. <p> + * + * The implementation provided here works just like the + * the implementation in MimeMessage. + * @see MimeMessage#getDataHandler + */ + @Override + public DataHandler getDataHandler() throws MessagingException { + if (dh == null) + dh = new MimePartDataHandler(this); + return dh; + } + + /** + * Return the content as a Java object. The type of the object + * returned is of course dependent on the content itself. For + * example, the native format of a text/plain content is usually + * a String object. The native format for a "multipart" + * content is always a Multipart subclass. For content types that are + * unknown to the DataHandler system, an input stream is returned + * as the content. <p> + * + * This implementation obtains the content from the DataHandler. + * That is, it invokes getDataHandler().getContent(); + * If the content is a Multipart or Message object and was created by + * parsing a stream, the object is cached and returned in subsequent + * calls so that modifications to the content will not be lost. + * + * @return Object + * @exception IOException this is typically thrown by the + * DataHandler. Refer to the documentation for + * javax.activation.DataHandler for more details. + * @exception MessagingException for other failures + */ + @Override + public Object getContent() throws IOException, MessagingException { + if (cachedContent != null) + return cachedContent; + Object c; + try { + c = getDataHandler().getContent(); + } catch (FolderClosedIOException fex) { + throw new FolderClosedException(fex.getFolder(), fex.getMessage()); + } catch (MessageRemovedIOException mex) { + throw new MessageRemovedException(mex.getMessage()); + } + if (cacheMultipart && + (c instanceof Multipart || c instanceof Message) && + (content != null || contentStream != null)) { + cachedContent = c; + /* + * We may abandon the input stream so make sure + * the MimeMultipart has consumed the stream. + */ + if (c instanceof MimeMultipart) + ((MimeMultipart)c).parse(); + } + return c; + } + + /** + * This method provides the mechanism to set this body part's content. + * The given DataHandler object should wrap the actual content. + * + * @param dh The DataHandler for the content + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this body part is + * obtained from a READ_ONLY folder. + */ + @Override + public void setDataHandler(DataHandler dh) + throws MessagingException { + this.dh = dh; + cachedContent = null; + MimeBodyPart.invalidateContentHeaders(this); + } + + /** + * A convenience method for setting this body part's content. <p> + * + * The content is wrapped in a DataHandler object. Note that a + * DataContentHandler class for the specified type should be + * available to the JavaMail implementation for this to work right. + * That is, to do <code>setContent(foobar, "application/x-foobar")</code>, + * a DataContentHandler for "application/x-foobar" should be installed. + * Refer to the Java Activation Framework for more information. + * + * @param o the content object + * @param type Mime type of the object + * @exception IllegalWriteException if the underlying + * implementation does not support modification of + * existing values + * @exception IllegalStateException if this body part is + * obtained from a READ_ONLY folder. + */ + @Override + public void setContent(Object o, String type) + throws MessagingException { + if (o instanceof Multipart) { + setContent((Multipart)o); + } else { + setDataHandler(new DataHandler(o, type)); + } + } + + /** + * Convenience method that sets the given String as this + * part's content, with a MIME type of "text/plain". If the + * string contains non US-ASCII characters, it will be encoded + * using the platform's default charset. The charset is also + * used to set the "charset" parameter. <p> + * + * Note that there may be a performance penalty if + * <code>text</code> is large, since this method may have + * to scan all the characters to determine what charset to + * use. <p> + * + * If the charset is already known, use the + * <code>setText</code> method that takes the charset parameter. + * + * @param text the text content to set + * @exception MessagingException if an error occurs + * @see #setText(String text, String charset) + */ + @Override + public void setText(String text) throws MessagingException { + setText(text, null); + } + + /** + * Convenience method that sets the given String as this part's + * content, with a MIME type of "text/plain" and the specified + * charset. The given Unicode string will be charset-encoded + * using the specified charset. The charset is also used to set + * the "charset" parameter. + * + * @param text the text content to set + * @param charset the charset to use for the text + * @exception MessagingException if an error occurs + */ + @Override + public void setText(String text, String charset) + throws MessagingException { + setText(this, text, charset, "plain"); + } + + /** + * Convenience method that sets the given String as this part's + * content, with a primary MIME type of "text" and the specified + * MIME subtype. The given Unicode string will be charset-encoded + * using the specified charset. The charset is also used to set + * the "charset" parameter. + * + * @param text the text content to set + * @param charset the charset to use for the text + * @param subtype the MIME subtype to use (e.g., "html") + * @exception MessagingException if an error occurs + * @since JavaMail 1.4 + */ + @Override + public void setText(String text, String charset, String subtype) + throws MessagingException { + setText(this, text, charset, subtype); + } + + /** + * This method sets the body part's content to a Multipart object. + * + * @param mp The multipart object that is the Message's content + * @exception IllegalWriteException if the underlying + * implementation does not support modification of + * existing values. + * @exception IllegalStateException if this body part is + * obtained from a READ_ONLY folder. + */ + @Override + public void setContent(Multipart mp) throws MessagingException { + setDataHandler(new DataHandler(mp, mp.getContentType())); + mp.setParent(this); + } + + /** + * Use the specified file to provide the data for this part. + * The simple file name is used as the file name for this + * part and the data in the file is used as the data for this + * part. The encoding will be chosen appropriately for the + * file data. The disposition of this part is set to + * {@link Part#ATTACHMENT Part.ATTACHMENT}. + * + * @param file the File object to attach + * @exception IOException errors related to accessing the file + * @exception MessagingException message related errors + * @since JavaMail 1.4 + */ + public void attachFile(File file) throws IOException, MessagingException { + FileDataSource fds = new FileDataSource(file); + this.setDataHandler(new DataHandler(fds)); + this.setFileName(fds.getName()); + this.setDisposition(ATTACHMENT); + } + + /** + * Use the specified file to provide the data for this part. + * The simple file name is used as the file name for this + * part and the data in the file is used as the data for this + * part. The encoding will be chosen appropriately for the + * file data. + * + * @param file the name of the file to attach + * @exception IOException errors related to accessing the file + * @exception MessagingException message related errors + * @since JavaMail 1.4 + */ + public void attachFile(String file) throws IOException, MessagingException { + File f = new File(file); + attachFile(f); + } + + /** + * Use the specified file with the specified Content-Type and + * Content-Transfer-Encoding to provide the data for this part. + * If contentType or encoding are null, appropriate values will + * be chosen. + * The simple file name is used as the file name for this + * part and the data in the file is used as the data for this + * part. The disposition of this part is set to + * {@link Part#ATTACHMENT Part.ATTACHMENT}. + * + * @param file the File object to attach + * @param contentType the Content-Type, or null + * @param encoding the Content-Transfer-Encoding, or null + * @exception IOException errors related to accessing the file + * @exception MessagingException message related errors + * @since JavaMail 1.5 + */ + public void attachFile(File file, String contentType, String encoding) + throws IOException, MessagingException { + DataSource fds = new EncodedFileDataSource(file, contentType, encoding); + this.setDataHandler(new DataHandler(fds)); + this.setFileName(fds.getName()); + this.setDisposition(ATTACHMENT); + } + + /** + * Use the specified file with the specified Content-Type and + * Content-Transfer-Encoding to provide the data for this part. + * If contentType or encoding are null, appropriate values will + * be chosen. + * The simple file name is used as the file name for this + * part and the data in the file is used as the data for this + * part. The disposition of this part is set to + * {@link Part#ATTACHMENT Part.ATTACHMENT}. + * + * @param file the name of the file + * @param contentType the Content-Type, or null + * @param encoding the Content-Transfer-Encoding, or null + * @exception IOException errors related to accessing the file + * @exception MessagingException message related errors + * @since JavaMail 1.5 + */ + public void attachFile(String file, String contentType, String encoding) + throws IOException, MessagingException { + attachFile(new File(file), contentType, encoding); + } + + /** + * A FileDataSource class that allows us to specify the + * Content-Type and Content-Transfer-Encoding. + */ + private static class EncodedFileDataSource extends FileDataSource + implements EncodingAware { + private String contentType; + private String encoding; + + public EncodedFileDataSource(File file, String contentType, + String encoding) { + super(file); + this.contentType = contentType; + this.encoding = encoding; + } + + // overrides DataSource.getContentType() + @Override + public String getContentType() { + return contentType != null ? contentType : super.getContentType(); + } + + // implements EncodingAware.getEncoding() + @Override + public String getEncoding() { + return encoding; + } + } + + /** + * Save the contents of this part in the specified file. The content + * is decoded and saved, without any of the MIME headers. + * + * @param file the File object to write to + * @exception IOException errors related to accessing the file + * @exception MessagingException message related errors + * @since JavaMail 1.4 + */ + public void saveFile(File file) throws IOException, MessagingException { + OutputStream out = null; + InputStream in = null; + try { + out = new BufferedOutputStream(new FileOutputStream(file)); + in = this.getInputStream(); + byte[] buf = new byte[8192]; + int len; + while ((len = in.read(buf)) > 0) + out.write(buf, 0, len); + } finally { + // close streams, but don't mask original exception, if any + try { + if (in != null) + in.close(); + } catch (IOException ex) { } + try { + if (out != null) + out.close(); + } catch (IOException ex) { } + } + } + + /** + * Save the contents of this part in the specified file. The content + * is decoded and saved, without any of the MIME headers. + * + * @param file the name of the file to write to + * @exception IOException errors related to accessing the file + * @exception MessagingException message related errors + * @since JavaMail 1.4 + */ + public void saveFile(String file) throws IOException, MessagingException { + File f = new File(file); + saveFile(f); + } + + /** + * Output the body part as an RFC 822 format stream. + * + * @exception IOException if an error occurs writing to the + * stream or if an error is generated + * by the javax.activation layer. + * @exception MessagingException for other failures + * @see javax.activation.DataHandler#writeTo + */ + @Override + public void writeTo(OutputStream os) + throws IOException, MessagingException { + writeTo(this, os, null); + } + + /** + * Get all the headers for this header_name. Note that certain + * headers may be encoded as per RFC 2047 if they contain + * non US-ASCII characters and these should be decoded. + * + * @param name name of header + * @return array of headers + * @see javax.mail.internet.MimeUtility + */ + @Override + public String[] getHeader(String name) throws MessagingException { + return headers.getHeader(name); + } + + /** + * Get all the headers for this header name, returned as a single + * String, with headers separated by the delimiter. If the + * delimiter is <code>null</code>, only the first header is + * returned. + * + * @param name the name of this header + * @param delimiter delimiter between fields in returned string + * @return the value fields for all headers with + * this name + * @exception MessagingException for failures + */ + @Override + public String getHeader(String name, String delimiter) + throws MessagingException { + return headers.getHeader(name, delimiter); + } + + /** + * Set the value for this header_name. Replaces all existing + * header values with this new value. Note that RFC 822 headers + * must contain only US-ASCII characters, so a header that + * contains non US-ASCII characters must be encoded as per the + * rules of RFC 2047. + * + * @param name header name + * @param value header value + * @see javax.mail.internet.MimeUtility + */ + @Override + public void setHeader(String name, String value) + throws MessagingException { + headers.setHeader(name, value); + } + + /** + * Add this value to the existing values for this header_name. + * Note that RFC 822 headers must contain only US-ASCII + * characters, so a header that contains non US-ASCII characters + * must be encoded as per the rules of RFC 2047. + * + * @param name header name + * @param value header value + * @see javax.mail.internet.MimeUtility + */ + @Override + public void addHeader(String name, String value) + throws MessagingException { + headers.addHeader(name, value); + } + + /** + * Remove all headers with this name. + */ + @Override + public void removeHeader(String name) throws MessagingException { + headers.removeHeader(name); + } + + /** + * Return all the headers from this Message as an Enumeration of + * Header objects. + */ + @Override + public Enumeration<Header> getAllHeaders() throws MessagingException { + return headers.getAllHeaders(); + } + + /** + * Return matching headers from this Message as an Enumeration of + * Header objects. + */ + @Override + public Enumeration<Header> getMatchingHeaders(String[] names) + throws MessagingException { + return headers.getMatchingHeaders(names); + } + + /** + * Return non-matching headers from this Message as an + * Enumeration of Header objects. + */ + @Override + public Enumeration<Header> getNonMatchingHeaders(String[] names) + throws MessagingException { + return headers.getNonMatchingHeaders(names); + } + + /** + * Add a header line to this body part + */ + @Override + public void addHeaderLine(String line) throws MessagingException { + headers.addHeaderLine(line); + } + + /** + * Get all header lines as an Enumeration of Strings. A Header + * line is a raw RFC 822 header line, containing both the "name" + * and "value" field. + */ + @Override + public Enumeration<String> getAllHeaderLines() throws MessagingException { + return headers.getAllHeaderLines(); + } + + /** + * Get matching header lines as an Enumeration of Strings. + * A Header line is a raw RFC 822 header line, containing both + * the "name" and "value" field. + */ + @Override + public Enumeration<String> getMatchingHeaderLines(String[] names) + throws MessagingException { + return headers.getMatchingHeaderLines(names); + } + + /** + * Get non-matching header lines as an Enumeration of Strings. + * A Header line is a raw RFC 822 header line, containing both + * the "name" and "value" field. + */ + @Override + public Enumeration<String> getNonMatchingHeaderLines(String[] names) + throws MessagingException { + return headers.getNonMatchingHeaderLines(names); + } + + /** + * Examine the content of this body part and update the appropriate + * MIME headers. Typical headers that get set here are + * <code>Content-Type</code> and <code>Content-Transfer-Encoding</code>. + * Headers might need to be updated in two cases: + * + * <br> + * - A message being crafted by a mail application will certainly + * need to activate this method at some point to fill up its internal + * headers. + * + * <br> + * - A message read in from a Store will have obtained + * all its headers from the store, and so doesn't need this. + * However, if this message is editable and if any edits have + * been made to either the content or message structure, we might + * need to resync our headers. + * + * <br> + * In both cases this method is typically called by the + * <code>Message.saveChanges</code> method. <p> + * + * If the {@link #cachedContent} field is not null (that is, + * it references a Multipart or Message object), then + * that object is used to set a new DataHandler, any + * stream data used to create this object is discarded, + * and the {@link #cachedContent} field is cleared. + * + * @exception MessagingException for failures + */ + protected void updateHeaders() throws MessagingException { + updateHeaders(this); + /* + * If we've cached a Multipart or Message object then + * we're now committed to using this instance of the + * object and we discard any stream data used to create + * this object. + */ + if (cachedContent != null) { + dh = new DataHandler(cachedContent, getContentType()); + cachedContent = null; + content = null; + if (contentStream != null) { + try { + contentStream.close(); + } catch (IOException ioex) { } // nothing to do + } + contentStream = null; + } + } + + ///////////////////////////////////////////////////////////// + // Package private convenience methods to share code among // + // MimeMessage and MimeBodyPart // + ///////////////////////////////////////////////////////////// + + static boolean isMimeType(MimePart part, String mimeType) + throws MessagingException { + // XXX - lots of room for optimization here! + String type = part.getContentType(); + try { + return new ContentType(type).match(mimeType); + } catch (ParseException ex) { + // we only need the type and subtype so throw away the rest + try { + int i = type.indexOf(';'); + if (i > 0) + return new ContentType(type.substring(0, i)).match(mimeType); + } catch (ParseException pex2) { + } + return type.equalsIgnoreCase(mimeType); + } + } + + static void setText(MimePart part, String text, String charset, + String subtype) throws MessagingException { + if (charset == null) { + if (MimeUtility.checkAscii(text) != MimeUtility.ALL_ASCII) + charset = MimeUtility.getDefaultMIMECharset(); + else + charset = "us-ascii"; + } + // XXX - should at least ensure that subtype is an atom + part.setContent(text, "text/" + subtype + "; charset=" + + MimeUtility.quote(charset, HeaderTokenizer.MIME)); + } + + static String getDisposition(MimePart part) throws MessagingException { + String s = part.getHeader("Content-Disposition", null); + + if (s == null) + return null; + + ContentDisposition cd = new ContentDisposition(s); + return cd.getDisposition(); + } + + static void setDisposition(MimePart part, String disposition) + throws MessagingException { + if (disposition == null) + part.removeHeader("Content-Disposition"); + else { + String s = part.getHeader("Content-Disposition", null); + if (s != null) { + /* A Content-Disposition header already exists .. + * + * Override disposition, but attempt to retain + * existing disposition parameters + */ + ContentDisposition cd = new ContentDisposition(s); + cd.setDisposition(disposition); + disposition = cd.toString(); + } + part.setHeader("Content-Disposition", disposition); + } + } + + static String getDescription(MimePart part) + throws MessagingException { + String rawvalue = part.getHeader("Content-Description", null); + + if (rawvalue == null) + return null; + + try { + return MimeUtility.decodeText(MimeUtility.unfold(rawvalue)); + } catch (UnsupportedEncodingException ex) { + return rawvalue; + } + } + + static void + setDescription(MimePart part, String description, String charset) + throws MessagingException { + if (description == null) { + part.removeHeader("Content-Description"); + return; + } + + try { + part.setHeader("Content-Description", MimeUtility.fold(21, + MimeUtility.encodeText(description, charset, null))); + } catch (UnsupportedEncodingException uex) { + throw new MessagingException("Encoding error", uex); + } + } + + static String getFileName(MimePart part) throws MessagingException { + String filename = null; + String s = part.getHeader("Content-Disposition", null); + + if (s != null) { + // Parse the header .. + ContentDisposition cd = new ContentDisposition(s); + filename = cd.getParameter("filename"); + } + if (filename == null) { + // Still no filename ? Try the "name" ContentType parameter + s = part.getHeader("Content-Type", null); + s = MimeUtil.cleanContentType(part, s); + if (s != null) { + try { + ContentType ct = new ContentType(s); + filename = ct.getParameter("name"); + } catch (ParseException pex) { } // ignore it + } + } + if (decodeFileName && filename != null) { + try { + filename = MimeUtility.decodeText(filename); + } catch (UnsupportedEncodingException ex) { + throw new MessagingException("Can't decode filename", ex); + } + } + return filename; + } + + static void setFileName(MimePart part, String name) + throws MessagingException { + if (encodeFileName && name != null) { + try { + name = MimeUtility.encodeText(name); + } catch (UnsupportedEncodingException ex) { + throw new MessagingException("Can't encode filename", ex); + } + } + + // Set the Content-Disposition "filename" parameter + String s = part.getHeader("Content-Disposition", null); + ContentDisposition cd = + new ContentDisposition(s == null ? Part.ATTACHMENT : s); + // ensure that the filename is encoded if necessary + String charset = MimeUtility.getDefaultMIMECharset(); + ParameterList p = cd.getParameterList(); + if (p == null) { + p = new ParameterList(); + cd.setParameterList(p); + } + if (encodeFileName) + p.setLiteral("filename", name); + else + p.set("filename", name, charset); + part.setHeader("Content-Disposition", cd.toString()); + + /* + * Also attempt to set the Content-Type "name" parameter, + * to satisfy ancient MUAs. XXX - This is not RFC compliant. + */ + if (setContentTypeFileName) { + s = part.getHeader("Content-Type", null); + s = MimeUtil.cleanContentType(part, s); + if (s != null) { + try { + ContentType cType = new ContentType(s); + // ensure that the filename is encoded if necessary + p = cType.getParameterList(); + if (p == null) { + p = new ParameterList(); + cType.setParameterList(p); + } + if (encodeFileName) + p.setLiteral("name", name); + else + p.set("name", name, charset); + part.setHeader("Content-Type", cType.toString()); + } catch (ParseException pex) { } // ignore it + } + } + } + + static String[] getContentLanguage(MimePart part) + throws MessagingException { + String s = part.getHeader("Content-Language", null); + + if (s == null) + return null; + + // Tokenize the header to obtain the Language-tags (skip comments) + HeaderTokenizer h = new HeaderTokenizer(s, HeaderTokenizer.MIME); + List<String> v = new ArrayList<>(); + + HeaderTokenizer.Token tk; + int tkType; + + while (true) { + tk = h.next(); // get a language-tag + tkType = tk.getType(); + if (tkType == HeaderTokenizer.Token.EOF) + break; // done + else if (tkType == HeaderTokenizer.Token.ATOM) + v.add(tk.getValue()); + else // invalid token, skip it. + continue; + } + + if (v.isEmpty()) + return null; + + String[] language = new String[v.size()]; + v.toArray(language); + return language; + } + + static void setContentLanguage(MimePart part, String[] languages) + throws MessagingException { + StringBuilder sb = new StringBuilder(languages[0]); + int len = "Content-Language".length() + 2 + languages[0].length(); + for (int i = 1; i < languages.length; i++) { + sb.append(','); + len++; + if (len > 76) { + sb.append("\r\n\t"); + len = 8; + } + sb.append(languages[i]); + len += languages[i].length(); + } + part.setHeader("Content-Language", sb.toString()); + } + + static String getEncoding(MimePart part) throws MessagingException { + String s = part.getHeader("Content-Transfer-Encoding", null); + + if (s == null) + return null; + + s = s.trim(); // get rid of trailing spaces + if (s.length() == 0) + return null; + // quick check for known values to avoid unnecessary use + // of tokenizer. + if (s.equalsIgnoreCase("7bit") || s.equalsIgnoreCase("8bit") || + s.equalsIgnoreCase("quoted-printable") || + s.equalsIgnoreCase("binary") || + s.equalsIgnoreCase("base64")) + return s; + + // Tokenize the header to obtain the encoding (skip comments) + HeaderTokenizer h = new HeaderTokenizer(s, HeaderTokenizer.MIME); + + HeaderTokenizer.Token tk; + int tkType; + + for (;;) { + tk = h.next(); // get a token + tkType = tk.getType(); + if (tkType == HeaderTokenizer.Token.EOF) + break; // done + else if (tkType == HeaderTokenizer.Token.ATOM) + return tk.getValue(); + else // invalid token, skip it. + continue; + } + return s; + } + + static void setEncoding(MimePart part, String encoding) + throws MessagingException { + part.setHeader("Content-Transfer-Encoding", encoding); + } + + /** + * Restrict the encoding to values allowed for the + * Content-Type of the specified MimePart. Returns + * either the original encoding or null. + */ + static String restrictEncoding(MimePart part, String encoding) + throws MessagingException { + if (!ignoreMultipartEncoding || encoding == null) + return encoding; + + if (encoding.equalsIgnoreCase("7bit") || + encoding.equalsIgnoreCase("8bit") || + encoding.equalsIgnoreCase("binary")) + return encoding; // these encodings are always valid + + String type = part.getContentType(); + if (type == null) + return encoding; + + try { + /* + * multipart and message types aren't allowed to have + * encodings except for the three mentioned above. + * If it's one of these types, ignore the encoding. + */ + ContentType cType = new ContentType(type); + if (cType.match("multipart/*")) + return null; + if (cType.match("message/*") && + !PropUtil.getBooleanSystemProperty( + "mail.mime.allowencodedmessages", false)) + return null; + } catch (ParseException pex) { + // ignore it + } + return encoding; + } + + static void updateHeaders(MimePart part) throws MessagingException { + DataHandler dh = part.getDataHandler(); + if (dh == null) // Huh ? + return; + + try { + String type = dh.getContentType(); + boolean composite = false; + boolean needCTHeader = part.getHeader("Content-Type") == null; + + ContentType cType = new ContentType(type); + + /* + * If this is a multipart, give sub-parts a chance to + * update their headers. Even though the data for this + * multipart may have come from a stream, one of the + * sub-parts may have been updated. + */ + if (cType.match("multipart/*")) { + // If multipart, recurse + composite = true; + Object o; + if (part instanceof MimeBodyPart) { + MimeBodyPart mbp = (MimeBodyPart)part; + o = mbp.cachedContent != null ? + mbp.cachedContent : dh.getContent(); + } else if (part instanceof MimeMessage) { + MimeMessage msg = (MimeMessage)part; + o = msg.cachedContent != null ? + msg.cachedContent : dh.getContent(); + } else + o = dh.getContent(); + if (o instanceof MimeMultipart) + ((MimeMultipart)o).updateHeaders(); + else + throw new MessagingException("MIME part of type \"" + + type + "\" contains object of type " + + o.getClass().getName() + " instead of MimeMultipart"); + } else if (cType.match("message/rfc822")) { + composite = true; + // XXX - call MimeMessage.updateHeaders()? + } + + /* + * If this is our own MimePartDataHandler, we can't update any + * of the headers. + * + * If this is a MimePartDataHandler coming from another part, + * we need to copy over the content headers from the other part. + * Note that the MimePartDataHandler still refers to the original + * data and the original MimePart. + */ + if (dh instanceof MimePartDataHandler) { + MimePartDataHandler mdh = (MimePartDataHandler)dh; + MimePart mpart = mdh.getPart(); + if (mpart != part) { + if (needCTHeader) + part.setHeader("Content-Type", mpart.getContentType()); + // XXX - can't change the encoding of the data from the + // other part without decoding and reencoding it, so + // we just force it to match the original, but if the + // original has no encoding we'll consider reencoding it + String enc = mpart.getEncoding(); + if (enc != null) { + setEncoding(part, enc); + return; + } + } else + return; + } + + // Content-Transfer-Encoding, but only if we don't + // already have one + if (!composite) { // not allowed on composite parts + if (part.getHeader("Content-Transfer-Encoding") == null) + setEncoding(part, MimeUtility.getEncoding(dh)); + + if (needCTHeader && setDefaultTextCharset && + cType.match("text/*") && + cType.getParameter("charset") == null) { + /* + * Set a default charset for text parts. + * We really should examine the data to determine + * whether or not it's all ASCII, but that's too + * expensive so we make an assumption: If we + * chose 7bit encoding for this data, it's probably + * ASCII. (MimeUtility.getEncoding will choose + * 7bit only in this case, but someone might've + * set the Content-Transfer-Encoding header manually.) + */ + String charset; + String enc = part.getEncoding(); + if (enc != null && enc.equalsIgnoreCase("7bit")) + charset = "us-ascii"; + else + charset = MimeUtility.getDefaultMIMECharset(); + cType.setParameter("charset", charset); + type = cType.toString(); + } + } + + // Now, let's update our own headers ... + + // Content-type, but only if we don't already have one + if (needCTHeader) { + /* + * Pull out "filename" from Content-Disposition, and + * use that to set the "name" parameter. This is to + * satisfy older MUAs (DtMail, Roam and probably + * a bunch of others). + */ + if (setContentTypeFileName) { + String s = part.getHeader("Content-Disposition", null); + if (s != null) { + // Parse the header .. + ContentDisposition cd = new ContentDisposition(s); + String filename = cd.getParameter("filename"); + if (filename != null) { + ParameterList p = cType.getParameterList(); + if (p == null) { + p = new ParameterList(); + cType.setParameterList(p); + } + if (encodeFileName) + p.setLiteral("name", + MimeUtility.encodeText(filename)); + else + p.set("name", filename, + MimeUtility.getDefaultMIMECharset()); + type = cType.toString(); + } + } + } + + part.setHeader("Content-Type", type); + } + } catch (IOException ex) { + throw new MessagingException("IOException updating headers", ex); + } + } + + static void invalidateContentHeaders(MimePart part) + throws MessagingException { + part.removeHeader("Content-Type"); + part.removeHeader("Content-Transfer-Encoding"); + } + + static void writeTo(MimePart part, OutputStream os, String[] ignoreList) + throws IOException, MessagingException { + + // see if we already have a LOS + LineOutputStream los = null; + if (os instanceof LineOutputStream) { + los = (LineOutputStream) os; + } else { + los = new LineOutputStream(os, allowutf8); + } + + // First, write out the header + Enumeration<String> hdrLines + = part.getNonMatchingHeaderLines(ignoreList); + while (hdrLines.hasMoreElements()) + los.writeln(hdrLines.nextElement()); + + // The CRLF separator between header and content + los.writeln(); + + // Finally, the content. Encode if required. + // XXX: May need to account for ESMTP ? + InputStream is = null; + byte[] buf = null; + try { + /* + * If the data for this part comes from a stream, + * and is already encoded, + * just copy it to the output stream without decoding + * and reencoding it. + */ + DataHandler dh = part.getDataHandler(); + if (dh instanceof MimePartDataHandler) { + MimePartDataHandler mpdh = (MimePartDataHandler)dh; + MimePart mpart = mpdh.getPart(); + if (mpart.getEncoding() != null) + is = mpdh.getContentStream(); + } + if (is != null) { + // now copy the data to the output stream + buf = new byte[8192]; + int len; + while ((len = is.read(buf)) > 0) + os.write(buf, 0, len); + } else { + os = MimeUtility.encode(os, + restrictEncoding(part, part.getEncoding())); + part.getDataHandler().writeTo(os); + } + } finally { + if (is != null) + is.close(); + buf = null; + } + os.flush(); // Needed to complete encoding + } + + /** + * A special DataHandler used only as a marker to indicate that + * the source of the data is a MimePart (that is, a byte array + * or a stream). This prevents updateHeaders from trying to + * change the headers for such data. In particular, the original + * Content-Transfer-Encoding for the data must be preserved. + * Otherwise the data would need to be decoded and reencoded. + */ + static class MimePartDataHandler extends DataHandler { + MimePart part; + public MimePartDataHandler(MimePart part) { + super(new MimePartDataSource(part)); + this.part = part; + } + + InputStream getContentStream() throws MessagingException { + InputStream is = null; + + if (part instanceof MimeBodyPart) { + MimeBodyPart mbp = (MimeBodyPart)part; + is = mbp.getContentStream(); + } else if (part instanceof MimeMessage) { + MimeMessage msg = (MimeMessage)part; + is = msg.getContentStream(); + } + return is; + } + + MimePart getPart() { + return part; + } + } +}
diff --git a/mail/src/main/java/javax/mail/internet/MimeMessage.java b/mail/src/main/java/javax/mail/internet/MimeMessage.java new file mode 100644 index 0000000..bc5a8f3 --- /dev/null +++ b/mail/src/main/java/javax/mail/internet/MimeMessage.java
@@ -0,0 +1,2299 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import javax.mail.*; +import javax.activation.*; +import java.lang.*; +import java.io.*; +import java.util.*; +import java.text.ParseException; +import com.sun.mail.util.PropUtil; +import com.sun.mail.util.ASCIIUtility; +import com.sun.mail.util.MimeUtil; +import com.sun.mail.util.MessageRemovedIOException; +import com.sun.mail.util.FolderClosedIOException; +import com.sun.mail.util.LineOutputStream; +import javax.mail.util.SharedByteArrayInputStream; + +/** + * This class represents a MIME style email message. It implements + * the <code>Message</code> abstract class and the <code>MimePart</code> + * interface. <p> + * + * Clients wanting to create new MIME style messages will instantiate + * an empty MimeMessage object and then fill it with appropriate + * attributes and content. <p> + * + * Service providers that implement MIME compliant backend stores may + * want to subclass MimeMessage and override certain methods to provide + * specific implementations. The simplest case is probably a provider + * that generates a MIME style input stream and leaves the parsing of + * the stream to this class. <p> + * + * MimeMessage uses the <code>InternetHeaders</code> class to parse and + * store the top level RFC 822 headers of a message. <p> + * + * The <code>mail.mime.address.strict</code> session property controls + * the parsing of address headers. By default, strict parsing of address + * headers is done. If this property is set to <code>"false"</code>, + * strict parsing is not done and many illegal addresses that sometimes + * occur in real messages are allowed. See the <code>InternetAddress</code> + * class for details. + * + * <hr><strong>A note on RFC 822 and MIME headers</strong><p> + * + * RFC 822 header fields <strong>must</strong> contain only + * US-ASCII characters. MIME allows non ASCII characters to be present + * in certain portions of certain headers, by encoding those characters. + * RFC 2047 specifies the rules for doing this. The MimeUtility + * class provided in this package can be used to to achieve this. + * Callers of the <code>setHeader</code>, <code>addHeader</code>, and + * <code>addHeaderLine</code> methods are responsible for enforcing + * the MIME requirements for the specified headers. In addition, these + * header fields must be folded (wrapped) before being sent if they + * exceed the line length limitation for the transport (1000 bytes for + * SMTP). Received headers may have been folded. The application is + * responsible for folding and unfolding headers as appropriate. <p> + * + * @author John Mani + * @author Bill Shannon + * @author Max Spivak + * @author Kanwar Oberoi + * @see javax.mail.internet.MimeUtility + * @see javax.mail.Part + * @see javax.mail.Message + * @see javax.mail.internet.MimePart + * @see javax.mail.internet.InternetAddress + */ + +public class MimeMessage extends Message implements MimePart { + + /** + * The DataHandler object representing this Message's content. + */ + protected DataHandler dh; + + /** + * Byte array that holds the bytes of this Message's content. + */ + protected byte[] content; + + /** + * If the data for this message was supplied by an + * InputStream that implements the SharedInputStream interface, + * <code>contentStream</code> is another such stream representing + * the content of this message. In this case, <code>content</code> + * will be null. + * + * @since JavaMail 1.2 + */ + protected InputStream contentStream; + + /** + * The InternetHeaders object that stores the header + * of this message. + */ + protected InternetHeaders headers; + + /** + * The Flags for this message. + */ + protected Flags flags; + + /** + * A flag indicating whether the message has been modified. + * If the message has not been modified, any data in the + * <code>content</code> array is assumed to be valid and is used + * directly in the <code>writeTo</code> method. This flag is + * set to true when an empty message is created or when the + * <code>saveChanges</code> method is called. + * + * @since JavaMail 1.2 + */ + protected boolean modified = false; + + /** + * Does the <code>saveChanges</code> method need to be called on + * this message? This flag is set to false by the public constructor + * and set to true by the <code>saveChanges</code> method. The + * <code>writeTo</code> method checks this flag and calls the + * <code>saveChanges</code> method as necessary. This avoids the + * common mistake of forgetting to call the <code>saveChanges</code> + * method on a newly constructed message. + * + * @since JavaMail 1.2 + */ + protected boolean saved = false; + + /** + * If our content is a Multipart or Message object, we save it + * the first time it's created by parsing a stream so that changes + * to the contained objects will not be lost. <p> + * + * If this field is not null, it's return by the {@link #getContent} + * method. The {@link #getContent} method sets this field if it + * would return a Multipart or MimeMessage object. This field is + * is cleared by the {@link #setDataHandler} method. + * + * @since JavaMail 1.5 + */ + protected Object cachedContent; + + // Used to parse dates + private static final MailDateFormat mailDateFormat = new MailDateFormat(); + + // Should addresses in headers be parsed in "strict" mode? + private boolean strict = true; + // Is UTF-8 allowed in headers? + private boolean allowutf8 = false; + + /** + * Default constructor. An empty message object is created. + * The <code>headers</code> field is set to an empty InternetHeaders + * object. The <code>flags</code> field is set to an empty Flags + * object. The <code>modified</code> flag is set to true. + * + * @param session the Sesssion + */ + public MimeMessage(Session session) { + super(session); + modified = true; + headers = new InternetHeaders(); + flags = new Flags(); // empty flags object + initStrict(); + } + + /** + * Constructs a MimeMessage by reading and parsing the data from the + * specified MIME InputStream. The InputStream will be left positioned + * at the end of the data for the message. Note that the input stream + * parse is done within this constructor itself. <p> + * + * The input stream contains an entire MIME formatted message with + * headers and data. + * + * @param session Session object for this message + * @param is the message input stream + * @exception MessagingException for failures + */ + public MimeMessage(Session session, InputStream is) + throws MessagingException { + super(session); + flags = new Flags(); // empty Flags object + initStrict(); + parse(is); + saved = true; + } + + /** + * Constructs a new MimeMessage with content initialized from the + * <code>source</code> MimeMessage. The new message is independent + * of the original. <p> + * + * Note: The current implementation is rather inefficient, copying + * the data more times than strictly necessary. + * + * @param source the message to copy content from + * @exception MessagingException for failures + * @since JavaMail 1.2 + */ + public MimeMessage(MimeMessage source) throws MessagingException { + super(source.session); + flags = source.getFlags(); + if (flags == null) // make sure flags is always set + flags = new Flags(); + ByteArrayOutputStream bos; + int size = source.getSize(); + if (size > 0) + bos = new ByteArrayOutputStream(size); + else + bos = new ByteArrayOutputStream(); + try { + strict = source.strict; + source.writeTo(bos); + bos.close(); + SharedByteArrayInputStream bis = + new SharedByteArrayInputStream(bos.toByteArray()); + parse(bis); + bis.close(); + saved = true; + } catch (IOException ex) { + // should never happen, but just in case... + throw new MessagingException("IOException while copying message", + ex); + } + } + + /** + * Constructs an empty MimeMessage object with the given Folder + * and message number. <p> + * + * This method is for providers subclassing <code>MimeMessage</code>. + * + * @param folder the Folder this message is from + * @param msgnum the number of this message + */ + protected MimeMessage(Folder folder, int msgnum) { + super(folder, msgnum); + flags = new Flags(); // empty Flags object + saved = true; + initStrict(); + } + + /** + * Constructs a MimeMessage by reading and parsing the data from the + * specified MIME InputStream. The InputStream will be left positioned + * at the end of the data for the message. Note that the input stream + * parse is done within this constructor itself. <p> + * + * This method is for providers subclassing <code>MimeMessage</code>. + * + * @param folder The containing folder. + * @param is the message input stream + * @param msgnum Message number of this message within its folder + * @exception MessagingException for failures + */ + protected MimeMessage(Folder folder, InputStream is, int msgnum) + throws MessagingException { + this(folder, msgnum); + initStrict(); + parse(is); + } + + /** + * Constructs a MimeMessage from the given InternetHeaders object + * and content. + * + * This method is for providers subclassing <code>MimeMessage</code>. + * + * @param folder The containing folder. + * @param headers The headers + * @param content The message content + * @param msgnum Message number of this message within its folder + * @exception MessagingException for failures + */ + protected MimeMessage(Folder folder, InternetHeaders headers, + byte[] content, int msgnum) throws MessagingException { + this(folder, msgnum); + this.headers = headers; + this.content = content; + initStrict(); + } + + /** + * Set the strict flag based on property. + */ + private void initStrict() { + if (session != null) { + Properties props = session.getProperties(); + strict = PropUtil.getBooleanProperty(props, + "mail.mime.address.strict", true); + allowutf8 = PropUtil.getBooleanProperty(props, + "mail.mime.allowutf8", false); + } + } + + /** + * Parse the InputStream setting the <code>headers</code> and + * <code>content</code> fields appropriately. Also resets the + * <code>modified</code> flag. <p> + * + * This method is intended for use by subclasses that need to + * control when the InputStream is parsed. + * + * @param is The message input stream + * @exception MessagingException for failures + */ + protected void parse(InputStream is) throws MessagingException { + + if (!(is instanceof ByteArrayInputStream) && + !(is instanceof BufferedInputStream) && + !(is instanceof SharedInputStream)) + is = new BufferedInputStream(is); + + headers = createInternetHeaders(is); + + if (is instanceof SharedInputStream) { + SharedInputStream sis = (SharedInputStream)is; + contentStream = sis.newStream(sis.getPosition(), -1); + } else { + try { + content = ASCIIUtility.getBytes(is); + } catch (IOException ioex) { + throw new MessagingException("IOException", ioex); + } + } + + modified = false; + } + + /** + * Returns the value of the RFC 822 "From" header fields. If this + * header field is absent, the "Sender" header field is used. + * If the "Sender" header field is also absent, <code>null</code> + * is returned.<p> + * + * This implementation uses the <code>getHeader</code> method + * to obtain the requisite header field. + * + * @return Address object + * @exception MessagingException for failures + * @see #headers + */ + @Override + public Address[] getFrom() throws MessagingException { + Address[] a = getAddressHeader("From"); + if (a == null) + a = getAddressHeader("Sender"); + + return a; + } + + /** + * Set the RFC 822 "From" header field. Any existing values are + * replaced with the given address. If address is <code>null</code>, + * this header is removed. + * + * @param address the sender of this message + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + @Override + public void setFrom(Address address) throws MessagingException { + if (address == null) + removeHeader("From"); + else + setHeader("From", MimeUtility.fold(6, address.toString())); + } + + /** + * Set the RFC 822 "From" header field. Any existing values are + * replaced with the given addresses. If address is <code>null</code>, + * this header is removed. + * + * @param address the sender(s) of this message + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + * @since JvaMail 1.5 + */ + public void setFrom(String address) throws MessagingException { + if (address == null) + removeHeader("From"); + else + setAddressHeader("From", InternetAddress.parse(address)); + } + + /** + * Set the RFC 822 "From" header field using the value of the + * <code>InternetAddress.getLocalAddress</code> method. + * + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + @Override + public void setFrom() throws MessagingException { + InternetAddress me = null; + try { + me = InternetAddress._getLocalAddress(session); + } catch (Exception ex) { + // if anything goes wrong (SecurityException, UnknownHostException), + // chain the exception + throw new MessagingException("No From address", ex); + } + if (me != null) + setFrom(me); + else + throw new MessagingException("No From address"); + } + + /** + * Add the specified addresses to the existing "From" field. If + * the "From" field does not already exist, it is created. + * + * @param addresses the senders of this message + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + @Override + public void addFrom(Address[] addresses) throws MessagingException { + addAddressHeader("From", addresses); + } + + /** + * Returns the value of the RFC 822 "Sender" header field. + * If the "Sender" header field is absent, <code>null</code> + * is returned.<p> + * + * This implementation uses the <code>getHeader</code> method + * to obtain the requisite header field. + * + * @return Address object + * @exception MessagingException for failures + * @see #headers + * @since JavaMail 1.3 + */ + public Address getSender() throws MessagingException { + Address[] a = getAddressHeader("Sender"); + if (a == null || a.length == 0) + return null; + return a[0]; // there can be only one + } + + /** + * Set the RFC 822 "Sender" header field. Any existing values are + * replaced with the given address. If address is <code>null</code>, + * this header is removed. + * + * @param address the sender of this message + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + * @since JavaMail 1.3 + */ + public void setSender(Address address) throws MessagingException { + if (address == null) + removeHeader("Sender"); + else + setHeader("Sender", MimeUtility.fold(8, address.toString())); + } + + /** + * This inner class extends the javax.mail.Message.RecipientType + * class to add additional RecipientTypes. The one additional + * RecipientType currently defined here is NEWSGROUPS. + * + * @see javax.mail.Message.RecipientType + */ + public static class RecipientType extends Message.RecipientType { + + private static final long serialVersionUID = -5468290701714395543L; + + /** + * The "Newsgroup" (Usenet news) recipients. + */ + public static final RecipientType NEWSGROUPS = + new RecipientType("Newsgroups"); + protected RecipientType(String type) { + super(type); + } + + @Override + protected Object readResolve() throws ObjectStreamException { + if (type.equals("Newsgroups")) + return NEWSGROUPS; + else + return super.readResolve(); + } + } + + /** + * Returns the recepients specified by the type. The mapping + * between the type and the corresponding RFC 822 header is + * as follows: + * <pre> + * Message.RecipientType.TO "To" + * Message.RecipientType.CC "Cc" + * Message.RecipientType.BCC "Bcc" + * MimeMessage.RecipientType.NEWSGROUPS "Newsgroups" + * </pre><br> + * + * Returns null if the header specified by the type is not found + * or if its value is empty. <p> + * + * This implementation uses the <code>getHeader</code> method + * to obtain the requisite header field. + * + * @param type Type of recepient + * @return array of Address objects + * @exception MessagingException if header could not + * be retrieved + * @exception AddressException if the header is misformatted + * @see #headers + * @see javax.mail.Message.RecipientType#TO + * @see javax.mail.Message.RecipientType#CC + * @see javax.mail.Message.RecipientType#BCC + * @see javax.mail.internet.MimeMessage.RecipientType#NEWSGROUPS + */ + @Override + public Address[] getRecipients(Message.RecipientType type) + throws MessagingException { + if (type == RecipientType.NEWSGROUPS) { + String s = getHeader("Newsgroups", ","); + return (s == null) ? null : NewsAddress.parse(s); + } else + return getAddressHeader(getHeaderName(type)); + } + + /** + * Get all the recipient addresses for the message. + * Extracts the TO, CC, BCC, and NEWSGROUPS recipients. + * + * @return array of Address objects + * @exception MessagingException for failures + * @see javax.mail.Message.RecipientType#TO + * @see javax.mail.Message.RecipientType#CC + * @see javax.mail.Message.RecipientType#BCC + * @see javax.mail.internet.MimeMessage.RecipientType#NEWSGROUPS + */ + @Override + public Address[] getAllRecipients() throws MessagingException { + Address[] all = super.getAllRecipients(); + Address[] ng = getRecipients(RecipientType.NEWSGROUPS); + + if (ng == null) + return all; // the common case + if (all == null) + return ng; // a rare case + + Address[] addresses = new Address[all.length + ng.length]; + System.arraycopy(all, 0, addresses, 0, all.length); + System.arraycopy(ng, 0, addresses, all.length, ng.length); + return addresses; + } + + /** + * Set the specified recipient type to the given addresses. + * If the address parameter is <code>null</code>, the corresponding + * recipient field is removed. + * + * @param type Recipient type + * @param addresses Addresses + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + * @see #getRecipients + */ + @Override + public void setRecipients(Message.RecipientType type, Address[] addresses) + throws MessagingException { + if (type == RecipientType.NEWSGROUPS) { + if (addresses == null || addresses.length == 0) + removeHeader("Newsgroups"); + else + setHeader("Newsgroups", NewsAddress.toString(addresses)); + } else + setAddressHeader(getHeaderName(type), addresses); + } + + /** + * Set the specified recipient type to the given addresses. + * If the address parameter is <code>null</code>, the corresponding + * recipient field is removed. + * + * @param type Recipient type + * @param addresses Addresses + * @exception AddressException if the attempt to parse the + * addresses String fails + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + * @see #getRecipients + * @since JavaMail 1.2 + */ + public void setRecipients(Message.RecipientType type, String addresses) + throws MessagingException { + if (type == RecipientType.NEWSGROUPS) { + if (addresses == null || addresses.length() == 0) + removeHeader("Newsgroups"); + else + setHeader("Newsgroups", addresses); + } else + setAddressHeader(getHeaderName(type), + addresses == null ? null : InternetAddress.parse(addresses)); + } + + /** + * Add the given addresses to the specified recipient type. + * + * @param type Recipient type + * @param addresses Addresses + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + @Override + public void addRecipients(Message.RecipientType type, Address[] addresses) + throws MessagingException { + if (type == RecipientType.NEWSGROUPS) { + String s = NewsAddress.toString(addresses); + if (s != null) + addHeader("Newsgroups", s); + } else + addAddressHeader(getHeaderName(type), addresses); + } + + /** + * Add the given addresses to the specified recipient type. + * + * @param type Recipient type + * @param addresses Addresses + * @exception AddressException if the attempt to parse the + * addresses String fails + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + * @since JavaMail 1.2 + */ + public void addRecipients(Message.RecipientType type, String addresses) + throws MessagingException { + if (type == RecipientType.NEWSGROUPS) { + if (addresses != null && addresses.length() != 0) + addHeader("Newsgroups", addresses); + } else + addAddressHeader(getHeaderName(type), + InternetAddress.parse(addresses)); + } + + /** + * Return the value of the RFC 822 "Reply-To" header field. If + * this header is unavailable or its value is absent, then + * the <code>getFrom</code> method is called and its value is returned. + * + * This implementation uses the <code>getHeader</code> method + * to obtain the requisite header field. + * + * @exception MessagingException for failures + * @see #headers + */ + @Override + public Address[] getReplyTo() throws MessagingException { + Address[] a = getAddressHeader("Reply-To"); + if (a == null || a.length == 0) + a = getFrom(); + return a; + } + + /** + * Set the RFC 822 "Reply-To" header field. If the address + * parameter is <code>null</code>, this header is removed. + * + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + @Override + public void setReplyTo(Address[] addresses) throws MessagingException { + setAddressHeader("Reply-To", addresses); + } + + // Convenience method to get addresses + private Address[] getAddressHeader(String name) + throws MessagingException { + String s = getHeader(name, ","); + return (s == null) ? null : InternetAddress.parseHeader(s, strict); + } + + // Convenience method to set addresses + private void setAddressHeader(String name, Address[] addresses) + throws MessagingException { + String s; + if (allowutf8) + s = InternetAddress.toUnicodeString(addresses, name.length() + 2); + else + s = InternetAddress.toString(addresses, name.length() + 2); + if (s == null) + removeHeader(name); + else + setHeader(name, s); + } + + private void addAddressHeader(String name, Address[] addresses) + throws MessagingException { + if (addresses == null || addresses.length == 0) + return; + Address[] a = getAddressHeader(name); + Address[] anew; + if (a == null || a.length == 0) + anew = addresses; + else { + anew = new Address[a.length + addresses.length]; + System.arraycopy(a, 0, anew, 0, a.length); + System.arraycopy(addresses, 0, anew, a.length, addresses.length); + } + String s; + if (allowutf8) + s = InternetAddress.toUnicodeString(anew, name.length() + 2); + else + s = InternetAddress.toString(anew, name.length() + 2); + if (s == null) + return; + setHeader(name, s); + } + + /** + * Returns the value of the "Subject" header field. Returns null + * if the subject field is unavailable or its value is absent. <p> + * + * If the subject is encoded as per RFC 2047, it is decoded and + * converted into Unicode. If the decoding or conversion fails, the + * raw data is returned as is. <p> + * + * This implementation uses the <code>getHeader</code> method + * to obtain the requisite header field. + * + * @return Subject + * @exception MessagingException for failures + * @see #headers + */ + @Override + public String getSubject() throws MessagingException { + String rawvalue = getHeader("Subject", null); + + if (rawvalue == null) + return null; + + try { + return MimeUtility.decodeText(MimeUtility.unfold(rawvalue)); + } catch (UnsupportedEncodingException ex) { + return rawvalue; + } + } + + /** + * Set the "Subject" header field. If the subject contains + * non US-ASCII characters, it will be encoded using the + * platform's default charset. If the subject contains only + * US-ASCII characters, no encoding is done and it is used + * as-is. If the subject is null, the existing "Subject" field + * is removed. <p> + * + * The application must ensure that the subject does not contain + * any line breaks. <p> + * + * Note that if the charset encoding process fails, a + * MessagingException is thrown, and an UnsupportedEncodingException + * is included in the chain of nested exceptions within the + * MessagingException. + * + * @param subject The subject + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + @Override + public void setSubject(String subject) throws MessagingException { + setSubject(subject, null); + } + + /** + * Set the "Subject" header field. If the subject contains non + * US-ASCII characters, it will be encoded using the specified + * charset. If the subject contains only US-ASCII characters, no + * encoding is done and it is used as-is. If the subject is null, + * the existing "Subject" header field is removed. <p> + * + * The application must ensure that the subject does not contain + * any line breaks. <p> + * + * Note that if the charset encoding process fails, a + * MessagingException is thrown, and an UnsupportedEncodingException + * is included in the chain of nested exceptions within the + * MessagingException. + * + * @param subject The subject + * @param charset The charset + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + public void setSubject(String subject, String charset) + throws MessagingException { + if (subject == null) { + removeHeader("Subject"); + } else { + try { + setHeader("Subject", MimeUtility.fold(9, + MimeUtility.encodeText(subject, charset, null))); + } catch (UnsupportedEncodingException uex) { + throw new MessagingException("Encoding error", uex); + } + } + } + + /** + * Returns the value of the RFC 822 "Date" field. This is the date + * on which this message was sent. Returns null if this field is + * unavailable or its value is absent. <p> + * + * This implementation uses the <code>getHeader</code> method + * to obtain the requisite header field. + * + * @return The sent Date + * @exception MessagingException for failures + */ + @Override + public Date getSentDate() throws MessagingException { + String s = getHeader("Date", null); + if (s != null) { + try { + synchronized (mailDateFormat) { + return mailDateFormat.parse(s); + } + } catch (ParseException pex) { + return null; + } + } + + return null; + } + + /** + * Set the RFC 822 "Date" header field. This is the date on which the + * creator of the message indicates that the message is complete + * and ready for delivery. If the date parameter is + * <code>null</code>, the existing "Date" field is removed. + * + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + @Override + public void setSentDate(Date d) throws MessagingException { + if (d == null) + removeHeader("Date"); + else { + synchronized (mailDateFormat) { + setHeader("Date", mailDateFormat.format(d)); + } + } + } + + /** + * Returns the Date on this message was received. Returns + * <code>null</code> if this date cannot be obtained. <p> + * + * Note that RFC 822 does not define a field for the received + * date. Hence only implementations that can provide this date + * need return a valid value. <p> + * + * This implementation returns <code>null</code>. + * + * @return the date this message was received + * @exception MessagingException for failures + */ + @Override + public Date getReceivedDate() throws MessagingException { + return null; + } + + /** + * Return the size of the content of this message in bytes. + * Return -1 if the size cannot be determined. <p> + * + * Note that this number may not be an exact measure of the + * content size and may or may not account for any transfer + * encoding of the content. <p> + * + * This implementation returns the size of the <code>content</code> + * array (if not null), or, if <code>contentStream</code> is not + * null, and the <code>available</code> method returns a positive + * number, it returns that number as the size. Otherwise, it returns + * -1. + * + * @return size of content in bytes + * @exception MessagingException for failures + */ + @Override + public int getSize() throws MessagingException { + if (content != null) + return content.length; + if (contentStream != null) { + try { + int size = contentStream.available(); + // only believe the size if it's greater than zero, since zero + // is the default returned by the InputStream class itself + if (size > 0) + return size; + } catch (IOException ex) { + // ignore it + } + } + return -1; + } + + /** + * Return the number of lines for the content of this message. + * Return -1 if this number cannot be determined. <p> + * + * Note that this number may not be an exact measure of the + * content length and may or may not account for any transfer + * encoding of the content. <p> + * + * This implementation returns -1. + * + * @return number of lines in the content. + * @exception MessagingException for failures + */ + @Override + public int getLineCount() throws MessagingException { + return -1; + } + + /** + * Returns the value of the RFC 822 "Content-Type" header field. + * This represents the content-type of the content of this + * message. This value must not be null. If this field is + * unavailable, "text/plain" should be returned. <p> + * + * This implementation uses the <code>getHeader</code> method + * to obtain the requisite header field. + * + * @return The ContentType of this part + * @exception MessagingException for failures + * @see javax.activation.DataHandler + */ + @Override + public String getContentType() throws MessagingException { + String s = getHeader("Content-Type", null); + s = MimeUtil.cleanContentType(this, s); + if (s == null) + return "text/plain"; + return s; + } + + /** + * Is this Part of the specified MIME type? This method + * compares <strong>only the <code>primaryType</code> and + * <code>subType</code></strong>. + * The parameters of the content types are ignored. <p> + * + * For example, this method will return <code>true</code> when + * comparing a Part of content type <strong>"text/plain"</strong> + * with <strong>"text/plain; charset=foobar"</strong>. <p> + * + * If the <code>subType</code> of <code>mimeType</code> is the + * special character '*', then the subtype is ignored during the + * comparison. + * + * @param mimeType the MIME type to check + * @return true if it matches the MIME type + * @exception MessagingException for failures + */ + @Override + public boolean isMimeType(String mimeType) throws MessagingException { + return MimeBodyPart.isMimeType(this, mimeType); + } + + /** + * Returns the disposition from the "Content-Disposition" header field. + * This represents the disposition of this part. The disposition + * describes how the part should be presented to the user. <p> + * + * If the Content-Disposition field is unavailable, + * <code>null</code> is returned. <p> + * + * This implementation uses the <code>getHeader</code> method + * to obtain the requisite header field. + * + * @return disposition of this part, or null if unknown + * @exception MessagingException for failures + */ + @Override + public String getDisposition() throws MessagingException { + return MimeBodyPart.getDisposition(this); + } + + /** + * Set the disposition in the "Content-Disposition" header field + * of this body part. If the disposition is null, any existing + * "Content-Disposition" header field is removed. + * + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + @Override + public void setDisposition(String disposition) throws MessagingException { + MimeBodyPart.setDisposition(this, disposition); + } + + /** + * Returns the content transfer encoding from the + * "Content-Transfer-Encoding" header + * field. Returns <code>null</code> if the header is unavailable + * or its value is absent. <p> + * + * This implementation uses the <code>getHeader</code> method + * to obtain the requisite header field. + * + * @return content-transfer-encoding + * @exception MessagingException for failures + */ + @Override + public String getEncoding() throws MessagingException { + return MimeBodyPart.getEncoding(this); + } + + /** + * Returns the value of the "Content-ID" header field. Returns + * <code>null</code> if the field is unavailable or its value is + * absent. <p> + * + * This implementation uses the <code>getHeader</code> method + * to obtain the requisite header field. + * + * @return content-ID + * @exception MessagingException for failures + */ + @Override + public String getContentID() throws MessagingException { + return getHeader("Content-Id", null); + } + + /** + * Set the "Content-ID" header field of this Message. + * If the <code>cid</code> parameter is null, any existing + * "Content-ID" is removed. + * + * @param cid the content ID + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + public void setContentID(String cid) throws MessagingException { + if (cid == null) + removeHeader("Content-ID"); + else + setHeader("Content-ID", cid); + } + + /** + * Return the value of the "Content-MD5" header field. Returns + * <code>null</code> if this field is unavailable or its value + * is absent. <p> + * + * This implementation uses the <code>getHeader</code> method + * to obtain the requisite header field. + * + * @return content-MD5 + * @exception MessagingException for failures + */ + @Override + public String getContentMD5() throws MessagingException { + return getHeader("Content-MD5", null); + } + + /** + * Set the "Content-MD5" header field of this Message. + * + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + @Override + public void setContentMD5(String md5) throws MessagingException { + setHeader("Content-MD5", md5); + } + + /** + * Returns the "Content-Description" header field of this Message. + * This typically associates some descriptive information with + * this part. Returns null if this field is unavailable or its + * value is absent. <p> + * + * If the Content-Description field is encoded as per RFC 2047, + * it is decoded and converted into Unicode. If the decoding or + * conversion fails, the raw data is returned as-is <p> + * + * This implementation uses the <code>getHeader</code> method + * to obtain the requisite header field. + * + * @return content-description + * @exception MessagingException for failures + */ + @Override + public String getDescription() throws MessagingException { + return MimeBodyPart.getDescription(this); + } + + /** + * Set the "Content-Description" header field for this Message. + * If the description parameter is <code>null</code>, then any + * existing "Content-Description" fields are removed. <p> + * + * If the description contains non US-ASCII characters, it will + * be encoded using the platform's default charset. If the + * description contains only US-ASCII characters, no encoding + * is done and it is used as-is. <p> + * + * Note that if the charset encoding process fails, a + * MessagingException is thrown, and an UnsupportedEncodingException + * is included in the chain of nested exceptions within the + * MessagingException. + * + * @param description content-description + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException An + * UnsupportedEncodingException may be included + * in the exception chain if the charset + * conversion fails. + */ + @Override + public void setDescription(String description) throws MessagingException { + setDescription(description, null); + } + + /** + * Set the "Content-Description" header field for this Message. + * If the description parameter is <code>null</code>, then any + * existing "Content-Description" fields are removed. <p> + * + * If the description contains non US-ASCII characters, it will + * be encoded using the specified charset. If the description + * contains only US-ASCII characters, no encoding is done and + * it is used as-is. <p> + * + * Note that if the charset encoding process fails, a + * MessagingException is thrown, and an UnsupportedEncodingException + * is included in the chain of nested exceptions within the + * MessagingException. + * + * @param description Description + * @param charset Charset for encoding + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException An + * UnsupportedEncodingException may be included + * in the exception chain if the charset + * conversion fails. + */ + public void setDescription(String description, String charset) + throws MessagingException { + MimeBodyPart.setDescription(this, description, charset); + } + + /** + * Get the languages specified in the "Content-Language" header + * field of this message. The Content-Language header is defined by + * RFC 1766. Returns <code>null</code> if this field is unavailable + * or its value is absent. <p> + * + * This implementation uses the <code>getHeader</code> method + * to obtain the requisite header field. + * + * @return value of content-language header. + * @exception MessagingException for failures + */ + @Override + public String[] getContentLanguage() throws MessagingException { + return MimeBodyPart.getContentLanguage(this); + } + + /** + * Set the "Content-Language" header of this MimePart. The + * Content-Language header is defined by RFC 1766. + * + * @param languages array of language tags + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + @Override + public void setContentLanguage(String[] languages) + throws MessagingException { + MimeBodyPart.setContentLanguage(this, languages); + } + + /** + * Returns the value of the "Message-ID" header field. Returns + * null if this field is unavailable or its value is absent. <p> + * + * The default implementation provided here uses the + * <code>getHeader</code> method to return the value of the + * "Message-ID" field. + * + * @return Message-ID + * @exception MessagingException if the retrieval of this field + * causes any exception. + * @see javax.mail.search.MessageIDTerm + * @since JavaMail 1.1 + */ + public String getMessageID() throws MessagingException { + return getHeader("Message-ID", null); + } + + /** + * Get the filename associated with this Message. <p> + * + * Returns the value of the "filename" parameter from the + * "Content-Disposition" header field of this message. If it's + * not available, returns the value of the "name" parameter from + * the "Content-Type" header field of this BodyPart. + * Returns <code>null</code> if both are absent. <p> + * + * If the <code>mail.mime.encodefilename</code> System property + * is set to true, the {@link MimeUtility#decodeText + * MimeUtility.decodeText} method will be used to decode the + * filename. While such encoding is not supported by the MIME + * spec, many mailers use this technique to support non-ASCII + * characters in filenames. The default value of this property + * is false. + * + * @return filename + * @exception MessagingException for failures + */ + @Override + public String getFileName() throws MessagingException { + return MimeBodyPart.getFileName(this); + } + + /** + * Set the filename associated with this part, if possible. <p> + * + * Sets the "filename" parameter of the "Content-Disposition" + * header field of this message. <p> + * + * If the <code>mail.mime.encodefilename</code> System property + * is set to true, the {@link MimeUtility#encodeText + * MimeUtility.encodeText} method will be used to encode the + * filename. While such encoding is not supported by the MIME + * spec, many mailers use this technique to support non-ASCII + * characters in filenames. The default value of this property + * is false. + * + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + @Override + public void setFileName(String filename) throws MessagingException { + MimeBodyPart.setFileName(this, filename); + } + + private String getHeaderName(Message.RecipientType type) + throws MessagingException { + String headerName; + + if (type == Message.RecipientType.TO) + headerName = "To"; + else if (type == Message.RecipientType.CC) + headerName = "Cc"; + else if (type == Message.RecipientType.BCC) + headerName = "Bcc"; + else if (type == MimeMessage.RecipientType.NEWSGROUPS) + headerName = "Newsgroups"; + else + throw new MessagingException("Invalid Recipient Type"); + return headerName; + } + + + /** + * Return a decoded input stream for this Message's "content". <p> + * + * This implementation obtains the input stream from the DataHandler, + * that is, it invokes <code>getDataHandler().getInputStream()</code>. + * + * @return an InputStream + * @exception IOException this is typically thrown by the + * DataHandler. Refer to the documentation for + * javax.activation.DataHandler for more details. + * @exception MessagingException for other failures + * + * @see #getContentStream + * @see javax.activation.DataHandler#getInputStream + */ + @Override + public InputStream getInputStream() + throws IOException, MessagingException { + return getDataHandler().getInputStream(); + } + + /** + * Produce the raw bytes of the content. This method is used during + * parsing, to create a DataHandler object for the content. Subclasses + * that can provide a separate input stream for just the message + * content might want to override this method. <p> + * + * This implementation returns a SharedInputStream, if + * <code>contentStream</code> is not null. Otherwise, it + * returns a ByteArrayInputStream constructed + * out of the <code>content</code> byte array. + * + * @return an InputStream containing the raw bytes + * @exception MessagingException for failures + * @see #content + */ + protected InputStream getContentStream() throws MessagingException { + if (contentStream != null) + return ((SharedInputStream)contentStream).newStream(0, -1); + if (content != null) + return new SharedByteArrayInputStream(content); + + throw new MessagingException("No MimeMessage content"); + } + + /** + * Return an InputStream to the raw data with any Content-Transfer-Encoding + * intact. This method is useful if the "Content-Transfer-Encoding" + * header is incorrect or corrupt, which would prevent the + * <code>getInputStream</code> method or <code>getContent</code> method + * from returning the correct data. In such a case the application may + * use this method and attempt to decode the raw data itself. <p> + * + * This implementation simply calls the <code>getContentStream</code> + * method. + * + * @return an InputStream containing the raw bytes + * @exception MessagingException for failures + * @see #getInputStream + * @see #getContentStream + * @since JavaMail 1.2 + */ + public InputStream getRawInputStream() throws MessagingException { + return getContentStream(); + } + + /** + * Return a DataHandler for this Message's content. <p> + * + * The implementation provided here works approximately as follows. + * Note the use of the <code>getContentStream</code> method to + * generate the byte stream for the content. Also note that + * any transfer-decoding is done automatically within this method. + * + * <blockquote><pre> + * getDataHandler() { + * if (dh == null) { + * dh = new DataHandler(new MimePartDataSource(this)); + * } + * return dh; + * } + * + * class MimePartDataSource implements DataSource { + * public getInputStream() { + * return MimeUtility.decode( + * getContentStream(), getEncoding()); + * } + * + * .... <other DataSource methods> + * } + * </pre></blockquote><p> + * + * @exception MessagingException for failures + */ + @Override + public synchronized DataHandler getDataHandler() + throws MessagingException { + if (dh == null) + dh = new MimeBodyPart.MimePartDataHandler(this); + return dh; + } + + /** + * Return the content as a Java object. The type of this + * object is dependent on the content itself. For + * example, the native format of a "text/plain" content + * is usually a String object. The native format for a "multipart" + * message is always a Multipart subclass. For content types that are + * unknown to the DataHandler system, an input stream is returned + * as the content. <p> + * + * This implementation obtains the content from the DataHandler, + * that is, it invokes <code>getDataHandler().getContent()</code>. + * If the content is a Multipart or Message object and was created by + * parsing a stream, the object is cached and returned in subsequent + * calls so that modifications to the content will not be lost. + * + * @return Object + * @see javax.mail.Part + * @see javax.activation.DataHandler#getContent + * @exception IOException this is typically thrown by the + * DataHandler. Refer to the documentation for + * javax.activation.DataHandler for more details. + * @exception MessagingException for other failures + */ + @Override + public Object getContent() throws IOException, MessagingException { + if (cachedContent != null) + return cachedContent; + Object c; + try { + c = getDataHandler().getContent(); + } catch (FolderClosedIOException fex) { + throw new FolderClosedException(fex.getFolder(), fex.getMessage()); + } catch (MessageRemovedIOException mex) { + throw new MessageRemovedException(mex.getMessage()); + } + if (MimeBodyPart.cacheMultipart && + (c instanceof Multipart || c instanceof Message) && + (content != null || contentStream != null)) { + cachedContent = c; + /* + * We may abandon the input stream so make sure + * the MimeMultipart has consumed the stream. + */ + if (c instanceof MimeMultipart) + ((MimeMultipart)c).parse(); + } + return c; + } + + /** + * This method provides the mechanism to set this part's content. + * The given DataHandler object should wrap the actual content. + * + * @param dh The DataHandler for the content. + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + @Override + public synchronized void setDataHandler(DataHandler dh) + throws MessagingException { + this.dh = dh; + cachedContent = null; + MimeBodyPart.invalidateContentHeaders(this); + } + + /** + * A convenience method for setting this Message's content. <p> + * + * The content is wrapped in a DataHandler object. Note that a + * DataContentHandler class for the specified type should be + * available to the JavaMail implementation for this to work right. + * i.e., to do <code>setContent(foobar, "application/x-foobar")</code>, + * a DataContentHandler for "application/x-foobar" should be installed. + * Refer to the Java Activation Framework for more information. + * + * @param o the content object + * @param type Mime type of the object + * @exception IllegalWriteException if the underlying + * implementation does not support modification of + * existing values + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + @Override + public void setContent(Object o, String type) + throws MessagingException { + if (o instanceof Multipart) + setContent((Multipart)o); + else + setDataHandler(new DataHandler(o, type)); + } + + /** + * Convenience method that sets the given String as this + * part's content, with a MIME type of "text/plain". If the + * string contains non US-ASCII characters. it will be encoded + * using the platform's default charset. The charset is also + * used to set the "charset" parameter.<p> + * + * Note that there may be a performance penalty if + * <code>text</code> is large, since this method may have + * to scan all the characters to determine what charset to + * use. <p> + * + * If the charset is already known, use the + * <code>setText</code> method that takes the charset parameter. + * + * @param text the text content to set + * @exception MessagingException if an error occurs + * @see #setText(String text, String charset) + */ + @Override + public void setText(String text) throws MessagingException { + setText(text, null); + } + + /** + * Convenience method that sets the given String as this part's + * content, with a MIME type of "text/plain" and the specified + * charset. The given Unicode string will be charset-encoded + * using the specified charset. The charset is also used to set + * the "charset" parameter. + * + * @param text the text content to set + * @param charset the charset to use for the text + * @exception MessagingException if an error occurs + */ + @Override + public void setText(String text, String charset) + throws MessagingException { + MimeBodyPart.setText(this, text, charset, "plain"); + } + + /** + * Convenience method that sets the given String as this part's + * content, with a primary MIME type of "text" and the specified + * MIME subtype. The given Unicode string will be charset-encoded + * using the specified charset. The charset is also used to set + * the "charset" parameter. + * + * @param text the text content to set + * @param charset the charset to use for the text + * @param subtype the MIME subtype to use (e.g., "html") + * @exception MessagingException if an error occurs + * @since JavaMail 1.4 + */ + @Override + public void setText(String text, String charset, String subtype) + throws MessagingException { + MimeBodyPart.setText(this, text, charset, subtype); + } + + /** + * This method sets the Message's content to a Multipart object. + * + * @param mp The multipart object that is the Message's content + * @exception IllegalWriteException if the underlying + * implementation does not support modification of + * existing values + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + @Override + public void setContent(Multipart mp) throws MessagingException { + setDataHandler(new DataHandler(mp, mp.getContentType())); + mp.setParent(this); + } + + /** + * Get a new Message suitable for a reply to this message. + * The new Message will have its attributes and headers + * set up appropriately. Note that this new message object + * will be empty, i.e., it will <strong>not</strong> have a "content". + * These will have to be suitably filled in by the client. <p> + * + * If <code>replyToAll</code> is set, the new Message will be addressed + * to all recipients of this message. Otherwise, the reply will be + * addressed to only the sender of this message (using the value + * of the <code>getReplyTo</code> method). <p> + * + * The "Subject" field is filled in with the original subject + * prefixed with "Re:" (unless it already starts with "Re:"). + * The "In-Reply-To" header is set in the new message if this + * message has a "Message-Id" header. The <code>ANSWERED</code> + * flag is set in this message. + * + * The current implementation also sets the "References" header + * in the new message to include the contents of the "References" + * header (or, if missing, the "In-Reply-To" header) in this message, + * plus the contents of the "Message-Id" header of this message, + * as described in RFC 2822. + * + * @param replyToAll reply should be sent to all recipients + * of this message + * @return the reply Message + * @exception MessagingException for failures + */ + @Override + public Message reply(boolean replyToAll) throws MessagingException { + return reply(replyToAll, true); + } + + /** + * Get a new Message suitable for a reply to this message. + * The new Message will have its attributes and headers + * set up appropriately. Note that this new message object + * will be empty, i.e., it will <strong>not</strong> have a "content". + * These will have to be suitably filled in by the client. <p> + * + * If <code>replyToAll</code> is set, the new Message will be addressed + * to all recipients of this message. Otherwise, the reply will be + * addressed to only the sender of this message (using the value + * of the <code>getReplyTo</code> method). <p> + * + * If <code>setAnswered</code> is set, the + * {@link javax.mail.Flags.Flag#ANSWERED ANSWERED} flag is set + * in this message. <p> + * + * The "Subject" field is filled in with the original subject + * prefixed with "Re:" (unless it already starts with "Re:"). + * The "In-Reply-To" header is set in the new message if this + * message has a "Message-Id" header. + * + * The current implementation also sets the "References" header + * in the new message to include the contents of the "References" + * header (or, if missing, the "In-Reply-To" header) in this message, + * plus the contents of the "Message-Id" header of this message, + * as described in RFC 2822. + * + * @param replyToAll reply should be sent to all recipients + * of this message + * @param setAnswered set the ANSWERED flag in this message? + * @return the reply Message + * @exception MessagingException for failures + * @since JavaMail 1.5 + */ + public Message reply(boolean replyToAll, boolean setAnswered) + throws MessagingException { + MimeMessage reply = createMimeMessage(session); + /* + * Have to manipulate the raw Subject header so that we don't lose + * any encoding information. This is safe because "Re:" isn't + * internationalized and (generally) isn't encoded. If the entire + * Subject header is encoded, prefixing it with "Re: " still leaves + * a valid and correct encoded header. + */ + String subject = getHeader("Subject", null); + if (subject != null) { + if (!subject.regionMatches(true, 0, "Re: ", 0, 4)) + subject = "Re: " + subject; + reply.setHeader("Subject", subject); + } + Address a[] = getReplyTo(); + reply.setRecipients(Message.RecipientType.TO, a); + if (replyToAll) { + List<Address> v = new ArrayList<>(); + // add my own address to list + InternetAddress me = InternetAddress.getLocalAddress(session); + if (me != null) + v.add(me); + // add any alternate names I'm known by + String alternates = null; + if (session != null) + alternates = session.getProperty("mail.alternates"); + if (alternates != null) + eliminateDuplicates(v, + InternetAddress.parse(alternates, false)); + // should we Cc all other original recipients? + String replyallccStr = null; + boolean replyallcc = false; + if (session != null) + replyallcc = PropUtil.getBooleanProperty( + session.getProperties(), + "mail.replyallcc", false); + // add the recipients from the To field so far + eliminateDuplicates(v, a); + a = getRecipients(Message.RecipientType.TO); + a = eliminateDuplicates(v, a); + if (a != null && a.length > 0) { + if (replyallcc) + reply.addRecipients(Message.RecipientType.CC, a); + else + reply.addRecipients(Message.RecipientType.TO, a); + } + a = getRecipients(Message.RecipientType.CC); + a = eliminateDuplicates(v, a); + if (a != null && a.length > 0) + reply.addRecipients(Message.RecipientType.CC, a); + // don't eliminate duplicate newsgroups + a = getRecipients(RecipientType.NEWSGROUPS); + if (a != null && a.length > 0) + reply.setRecipients(RecipientType.NEWSGROUPS, a); + } + + String msgId = getHeader("Message-Id", null); + if (msgId != null) + reply.setHeader("In-Reply-To", msgId); + + /* + * Set the References header as described in RFC 2822: + * + * The "References:" field will contain the contents of the parent's + * "References:" field (if any) followed by the contents of the parent's + * "Message-ID:" field (if any). If the parent message does not contain + * a "References:" field but does have an "In-Reply-To:" field + * containing a single message identifier, then the "References:" field + * will contain the contents of the parent's "In-Reply-To:" field + * followed by the contents of the parent's "Message-ID:" field (if + * any). If the parent has none of the "References:", "In-Reply-To:", + * or "Message-ID:" fields, then the new message will have no + * "References:" field. + */ + String refs = getHeader("References", " "); + if (refs == null) { + // XXX - should only use if it contains a single message identifier + refs = getHeader("In-Reply-To", " "); + } + if (msgId != null) { + if (refs != null) + refs = MimeUtility.unfold(refs) + " " + msgId; + else + refs = msgId; + } + if (refs != null) + reply.setHeader("References", MimeUtility.fold(12, refs)); + + if (setAnswered) { + try { + setFlags(answeredFlag, true); + } catch (MessagingException mex) { + // ignore it + } + } + return reply; + } + + // used above in reply() + private static final Flags answeredFlag = new Flags(Flags.Flag.ANSWERED); + + /** + * Check addrs for any duplicates that may already be in v. + * Return a new array without the duplicates. Add any new + * addresses to v. Note that the input array may be modified. + */ + private Address[] eliminateDuplicates(List<Address> v, Address[] addrs) { + if (addrs == null) + return null; + int gone = 0; + for (int i = 0; i < addrs.length; i++) { + boolean found = false; + // search the list for this address + for (int j = 0; j < v.size(); j++) { + if (((InternetAddress)v.get(j)).equals(addrs[i])) { + // found it; count it and remove it from the input array + found = true; + gone++; + addrs[i] = null; + break; + } + } + if (!found) + v.add(addrs[i]); // add new address to list + } + // if we found any duplicates, squish the array + if (gone != 0) { + Address[] a; + // new array should be same type as original array + // XXX - there must be a better way, perhaps reflection? + if (addrs instanceof InternetAddress[]) + a = new InternetAddress[addrs.length - gone]; + else + a = new Address[addrs.length - gone]; + for (int i = 0, j = 0; i < addrs.length; i++) + if (addrs[i] != null) + a[j++] = addrs[i]; + addrs = a; + } + return addrs; + } + + /** + * Output the message as an RFC 822 format stream. <p> + * + * Note that, depending on how the messag was constructed, it may + * use a variety of line termination conventions. Generally the + * output should be sent through an appropriate FilterOutputStream + * that converts the line terminators to the desired form, either + * CRLF for MIME compatibility and for use in Internet protocols, + * or the local platform's line terminator for storage in a local + * text file. <p> + * + * This implementation calls the <code>writeTo(OutputStream, + * String[])</code> method with a null ignore list. + * + * @exception IOException if an error occurs writing to the stream + * or if an error is generated by the + * javax.activation layer. + * @exception MessagingException for other failures + * @see javax.activation.DataHandler#writeTo + */ + @Override + public void writeTo(OutputStream os) + throws IOException, MessagingException { + writeTo(os, null); + } + + /** + * Output the message as an RFC 822 format stream, without + * specified headers. If the <code>saved</code> flag is not set, + * the <code>saveChanges</code> method is called. + * If the <code>modified</code> flag is not + * set and the <code>content</code> array is not null, the + * <code>content</code> array is written directly, after + * writing the appropriate message headers. + * + * @param os the stream to write to + * @param ignoreList the headers to not include in the output + * @exception IOException if an error occurs writing to the stream + * or if an error is generated by the + * javax.activation layer. + * @exception javax.mail.MessagingException for other failures + * @see javax.activation.DataHandler#writeTo + */ + public void writeTo(OutputStream os, String[] ignoreList) + throws IOException, MessagingException { + if (!saved) + saveChanges(); + + if (modified) { + MimeBodyPart.writeTo(this, os, ignoreList); + return; + } + + // Else, the content is untouched, so we can just output it + // First, write out the header + Enumeration<String> hdrLines = getNonMatchingHeaderLines(ignoreList); + LineOutputStream los = new LineOutputStream(os, allowutf8); + while (hdrLines.hasMoreElements()) + los.writeln(hdrLines.nextElement()); + + // The CRLF separator between header and content + los.writeln(); + + // Finally, the content. + if (content == null) { + // call getContentStream to give subclass a chance to + // provide the data on demand + InputStream is = null; + byte[] buf = new byte[8192]; + try { + is = getContentStream(); + // now copy the data to the output stream + int len; + while ((len = is.read(buf)) > 0) + os.write(buf, 0, len); + } finally { + if (is != null) + is.close(); + buf = null; + } + } else { + os.write(content); + } + os.flush(); + } + + /** + * Get all the headers for this header_name. Note that certain + * headers may be encoded as per RFC 2047 if they contain + * non US-ASCII characters and these should be decoded. <p> + * + * This implementation obtains the headers from the + * <code>headers</code> InternetHeaders object. + * + * @param name name of header + * @return array of headers + * @exception MessagingException for failures + * @see javax.mail.internet.MimeUtility + */ + @Override + public String[] getHeader(String name) + throws MessagingException { + return headers.getHeader(name); + } + + /** + * Get all the headers for this header name, returned as a single + * String, with headers separated by the delimiter. If the + * delimiter is <code>null</code>, only the first header is + * returned. + * + * @param name the name of this header + * @param delimiter separator between values + * @return the value fields for all headers with + * this name + * @exception MessagingException for failures + */ + @Override + public String getHeader(String name, String delimiter) + throws MessagingException { + return headers.getHeader(name, delimiter); + } + + /** + * Set the value for this header_name. Replaces all existing + * header values with this new value. Note that RFC 822 headers + * must contain only US-ASCII characters, so a header that + * contains non US-ASCII characters must have been encoded by the + * caller as per the rules of RFC 2047. + * + * @param name header name + * @param value header value + * @see javax.mail.internet.MimeUtility + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + @Override + public void setHeader(String name, String value) + throws MessagingException { + headers.setHeader(name, value); + } + + /** + * Add this value to the existing values for this header_name. + * Note that RFC 822 headers must contain only US-ASCII + * characters, so a header that contains non US-ASCII characters + * must have been encoded as per the rules of RFC 2047. + * + * @param name header name + * @param value header value + * @see javax.mail.internet.MimeUtility + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + @Override + public void addHeader(String name, String value) + throws MessagingException { + headers.addHeader(name, value); + } + + /** + * Remove all headers with this name. + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + @Override + public void removeHeader(String name) + throws MessagingException { + headers.removeHeader(name); + } + + /** + * Return all the headers from this Message as an enumeration + * of Header objects. <p> + * + * Note that certain headers may be encoded as per RFC 2047 + * if they contain non US-ASCII characters and these should + * be decoded. <p> + * + * This implementation obtains the headers from the + * <code>headers</code> InternetHeaders object. + * + * @return array of header objects + * @exception MessagingException for failures + * @see javax.mail.internet.MimeUtility + */ + @Override + public Enumeration<Header> getAllHeaders() throws MessagingException { + return headers.getAllHeaders(); + } + + /** + * Return matching headers from this Message as an Enumeration of + * Header objects. This implementation obtains the headers from + * the <code>headers</code> InternetHeaders object. + * + * @exception MessagingException for failures + */ + @Override + public Enumeration<Header> getMatchingHeaders(String[] names) + throws MessagingException { + return headers.getMatchingHeaders(names); + } + + /** + * Return non-matching headers from this Message as an + * Enumeration of Header objects. This implementation + * obtains the header from the <code>headers</code> InternetHeaders object. + * + * @exception MessagingException for failures + */ + @Override + public Enumeration<Header> getNonMatchingHeaders(String[] names) + throws MessagingException { + return headers.getNonMatchingHeaders(names); + } + + /** + * Add a raw RFC 822 header-line. + * + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + @Override + public void addHeaderLine(String line) throws MessagingException { + headers.addHeaderLine(line); + } + + /** + * Get all header lines as an Enumeration of Strings. A Header + * line is a raw RFC 822 header-line, containing both the "name" + * and "value" field. + * + * @exception MessagingException for failures + */ + @Override + public Enumeration<String> getAllHeaderLines() throws MessagingException { + return headers.getAllHeaderLines(); + } + + /** + * Get matching header lines as an Enumeration of Strings. + * A Header line is a raw RFC 822 header-line, containing both + * the "name" and "value" field. + * + * @exception MessagingException for failures + */ + @Override + public Enumeration<String> getMatchingHeaderLines(String[] names) + throws MessagingException { + return headers.getMatchingHeaderLines(names); + } + + /** + * Get non-matching header lines as an Enumeration of Strings. + * A Header line is a raw RFC 822 header-line, containing both + * the "name" and "value" field. + * + * @exception MessagingException for failures + */ + @Override + public Enumeration<String> getNonMatchingHeaderLines(String[] names) + throws MessagingException { + return headers.getNonMatchingHeaderLines(names); + } + + /** + * Return a <code>Flags</code> object containing the flags for + * this message. <p> + * + * Note that a clone of the internal Flags object is returned, so + * modifying the returned Flags object will not affect the flags + * of this message. + * + * @return Flags object containing the flags for this message + * @exception MessagingException for failures + * @see javax.mail.Flags + */ + @Override + public synchronized Flags getFlags() throws MessagingException { + return (Flags)flags.clone(); + } + + /** + * Check whether the flag specified in the <code>flag</code> + * argument is set in this message. <p> + * + * This implementation checks this message's internal + * <code>flags</code> object. + * + * @param flag the flag + * @return value of the specified flag for this message + * @exception MessagingException for failures + * @see javax.mail.Flags.Flag + * @see javax.mail.Flags.Flag#ANSWERED + * @see javax.mail.Flags.Flag#DELETED + * @see javax.mail.Flags.Flag#DRAFT + * @see javax.mail.Flags.Flag#FLAGGED + * @see javax.mail.Flags.Flag#RECENT + * @see javax.mail.Flags.Flag#SEEN + */ + @Override + public synchronized boolean isSet(Flags.Flag flag) + throws MessagingException { + return (flags.contains(flag)); + } + + /** + * Set the flags for this message. <p> + * + * This implementation modifies the <code>flags</code> field. + * + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + @Override + public synchronized void setFlags(Flags flag, boolean set) + throws MessagingException { + if (set) + flags.add(flag); + else + flags.remove(flag); + } + + /** + * Updates the appropriate header fields of this message to be + * consistent with the message's contents. If this message is + * contained in a Folder, any changes made to this message are + * committed to the containing folder. <p> + * + * If any part of a message's headers or contents are changed, + * <code>saveChanges</code> must be called to ensure that those + * changes are permanent. Otherwise, any such modifications may or + * may not be saved, depending on the folder implementation. <p> + * + * Messages obtained from folders opened READ_ONLY should not be + * modified and saveChanges should not be called on such messages. <p> + * + * This method sets the <code>modified</code> flag to true, the + * <code>save</code> flag to true, and then calls the + * <code>updateHeaders</code> method. + * + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + @Override + public void saveChanges() throws MessagingException { + modified = true; + saved = true; + updateHeaders(); + } + + /** + * Update the Message-ID header. This method is called + * by the <code>updateHeaders</code> and allows a subclass + * to override only the algorithm for choosing a Message-ID. + * + * @exception MessagingException for failures + * @since JavaMail 1.4 + */ + protected void updateMessageID() throws MessagingException { + setHeader("Message-ID", + "<" + UniqueValue.getUniqueMessageIDValue(session) + ">"); + + } + + /** + * Called by the <code>saveChanges</code> method to actually + * update the MIME headers. The implementation here sets the + * <code>Content-Transfer-Encoding</code> header (if needed + * and not already set), the <code>Date</code> header (if + * not already set), the <code>MIME-Version</code> header + * and the <code>Message-ID</code> header. Also, if the content + * of this message is a <code>MimeMultipart</code>, its + * <code>updateHeaders</code> method is called. <p> + * + * If the {@link #cachedContent} field is not null (that is, + * it references a Multipart or Message object), then + * that object is used to set a new DataHandler, any + * stream data used to create this object is discarded, + * and the {@link #cachedContent} field is cleared. + * + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + protected synchronized void updateHeaders() throws MessagingException { + MimeBodyPart.updateHeaders(this); + setHeader("MIME-Version", "1.0"); + if (getHeader("Date") == null) + setSentDate(new Date()); + updateMessageID(); + + if (cachedContent != null) { + dh = new DataHandler(cachedContent, getContentType()); + cachedContent = null; + content = null; + if (contentStream != null) { + try { + contentStream.close(); + } catch (IOException ioex) { } // nothing to do + } + contentStream = null; + } + } + + /** + * Create and return an InternetHeaders object that loads the + * headers from the given InputStream. Subclasses can override + * this method to return a subclass of InternetHeaders, if + * necessary. This implementation simply constructs and returns + * an InternetHeaders object. + * + * @return an InternetHeaders object + * @param is the InputStream to read the headers from + * @exception MessagingException for failures + * @since JavaMail 1.2 + */ + protected InternetHeaders createInternetHeaders(InputStream is) + throws MessagingException { + return new InternetHeaders(is, allowutf8); + } + + /** + * Create and return a MimeMessage object. The reply method + * uses this method to create the MimeMessage object that it + * will return. Subclasses can override this method to return + * a subclass of MimeMessage. This implementation simply constructs + * and returns a MimeMessage object using the supplied Session. + * + * @param session the Session to use for the new message + * @return the new MimeMessage object + * @exception MessagingException for failures + * @since JavaMail 1.4 + */ + protected MimeMessage createMimeMessage(Session session) + throws MessagingException { + return new MimeMessage(session); + } +}
diff --git a/mail/src/main/java/javax/mail/internet/MimeMultipart.java b/mail/src/main/java/javax/mail/internet/MimeMultipart.java new file mode 100644 index 0000000..04090ee --- /dev/null +++ b/mail/src/main/java/javax/mail/internet/MimeMultipart.java
@@ -0,0 +1,1009 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import javax.mail.*; +import javax.activation.*; +import java.util.*; +import java.io.*; +import com.sun.mail.util.LineOutputStream; +import com.sun.mail.util.LineInputStream; +import com.sun.mail.util.ASCIIUtility; +import com.sun.mail.util.PropUtil; + +/** + * The MimeMultipart class is an implementation of the abstract Multipart + * class that uses MIME conventions for the multipart data. <p> + * + * A MimeMultipart is obtained from a MimePart whose primary type + * is "multipart" (by invoking the part's <code>getContent()</code> method) + * or it can be created by a client as part of creating a new MimeMessage. <p> + * + * The default multipart subtype is "mixed". The other multipart + * subtypes, such as "alternative", "related", and so on, can be + * implemented as subclasses of MimeMultipart with additional methods + * to implement the additional semantics of that type of multipart + * content. The intent is that service providers, mail JavaBean writers + * and mail clients will write many such subclasses and their Command + * Beans, and will install them into the JavaBeans Activation + * Framework, so that any JavaMail implementation and its clients can + * transparently find and use these classes. Thus, a MIME multipart + * handler is treated just like any other type handler, thereby + * decoupling the process of providing multipart handlers from the + * JavaMail API. Lacking these additional MimeMultipart subclasses, + * all subtypes of MIME multipart data appear as MimeMultipart objects. <p> + * + * An application can directly construct a MIME multipart object of any + * subtype by using the <code>MimeMultipart(String subtype)</code> + * constructor. For example, to create a "multipart/alternative" object, + * use <code>new MimeMultipart("alternative")</code>. <p> + * + * The <code>mail.mime.multipart.ignoremissingendboundary</code> + * property may be set to <code>false</code> to cause a + * <code>MessagingException</code> to be thrown if the multipart + * data does not end with the required end boundary line. If this + * property is set to <code>true</code> or not set, missing end + * boundaries are not considered an error and the final body part + * ends at the end of the data. <p> + * + * The <code>mail.mime.multipart.ignoremissingboundaryparameter</code> + * System property may be set to <code>false</code> to cause a + * <code>MessagingException</code> to be thrown if the Content-Type + * of the MimeMultipart does not include a <code>boundary</code> parameter. + * If this property is set to <code>true</code> or not set, the multipart + * parsing code will look for a line that looks like a bounary line and + * use that as the boundary separating the parts. <p> + * + * The <code>mail.mime.multipart.ignoreexistingboundaryparameter</code> + * System property may be set to <code>true</code> to cause any boundary + * to be ignored and instead search for a boundary line in the message + * as with <code>mail.mime.multipart.ignoremissingboundaryparameter</code>. <p> + * + * Normally, when writing out a MimeMultipart that contains no body + * parts, or when trying to parse a multipart message with no body parts, + * a <code>MessagingException</code> is thrown. The MIME spec does not allow + * multipart content with no body parts. The + * <code>mail.mime.multipart.allowempty</code> System property may be set to + * <code>true</code> to override this behavior. + * When writing out such a MimeMultipart, a single empty part will be + * included. When reading such a multipart, a MimeMultipart will be created + * with no body parts. + * + * @author John Mani + * @author Bill Shannon + * @author Max Spivak + */ + +public class MimeMultipart extends Multipart { + + /** + * The DataSource supplying our InputStream. + */ + protected DataSource ds = null; + + /** + * Have we parsed the data from our InputStream yet? + * Defaults to true; set to false when our constructor is + * given a DataSource with an InputStream that we need to + * parse. + */ + protected boolean parsed = true; + + /** + * Have we seen the final bounary line? + * + * @since JavaMail 1.5 + */ + protected boolean complete = true; + + /** + * The MIME multipart preamble text, the text that + * occurs before the first boundary line. + * + * @since JavaMail 1.5 + */ + protected String preamble = null; + + /** + * Flag corresponding to the "mail.mime.multipart.ignoremissingendboundary" + * property, set in the {@link #initializeProperties} method called from + * constructors and the parse method. + * + * @since JavaMail 1.5 + */ + protected boolean ignoreMissingEndBoundary = true; + + /** + * Flag corresponding to the + * "mail.mime.multipart.ignoremissingboundaryparameter" + * property, set in the {@link #initializeProperties} method called from + * constructors and the parse method. + * + * @since JavaMail 1.5 + */ + protected boolean ignoreMissingBoundaryParameter = true; + + /** + * Flag corresponding to the + * "mail.mime.multipart.ignoreexistingboundaryparameter" + * property, set in the {@link #initializeProperties} method called from + * constructors and the parse method. + * + * @since JavaMail 1.5 + */ + protected boolean ignoreExistingBoundaryParameter = false; + + /** + * Flag corresponding to the "mail.mime.multipart.allowempty" + * property, set in the {@link #initializeProperties} method called from + * constructors and the parse method. + * + * @since JavaMail 1.5 + */ + protected boolean allowEmpty = false; + + /** + * Default constructor. An empty MimeMultipart object + * is created. Its content type is set to "multipart/mixed". + * A unique boundary string is generated and this string is + * setup as the "boundary" parameter for the + * <code>contentType</code> field. <p> + * + * MimeBodyParts may be added later. + */ + public MimeMultipart() { + this("mixed"); + } + + /** + * Construct a MimeMultipart object of the given subtype. + * A unique boundary string is generated and this string is + * setup as the "boundary" parameter for the + * <code>contentType</code> field. + * Calls the {@link #initializeProperties} method.<p> + * + * MimeBodyParts may be added later. + * + * @param subtype the MIME content subtype + */ + public MimeMultipart(String subtype) { + super(); + /* + * Compute a boundary string. + */ + String boundary = UniqueValue.getUniqueBoundaryValue(); + ContentType cType = new ContentType("multipart", subtype, null); + cType.setParameter("boundary", boundary); + contentType = cType.toString(); + initializeProperties(); + } + + /** + * Construct a MimeMultipart object of the default "mixed" subtype, + * and with the given body parts. More body parts may be added later. + * + * @param parts the body parts + * @exception MessagingException for failures + * @since JavaMail 1.5 + */ + public MimeMultipart(BodyPart... parts) throws MessagingException { + this(); + for (BodyPart bp : parts) + super.addBodyPart(bp); + } + + /** + * Construct a MimeMultipart object of the given subtype + * and with the given body parts. More body parts may be added later. + * + * @param subtype the MIME content subtype + * @param parts the body parts + * @exception MessagingException for failures + * @since JavaMail 1.5 + */ + public MimeMultipart(String subtype, BodyPart... parts) + throws MessagingException { + this(subtype); + for (BodyPart bp : parts) + super.addBodyPart(bp); + } + + /** + * Constructs a MimeMultipart object and its bodyparts from the + * given DataSource. <p> + * + * This constructor handles as a special case the situation where the + * given DataSource is a MultipartDataSource object. In this case, this + * method just invokes the superclass (i.e., Multipart) constructor + * that takes a MultipartDataSource object. <p> + * + * Otherwise, the DataSource is assumed to provide a MIME multipart + * byte stream. The <code>parsed</code> flag is set to false. When + * the data for the body parts are needed, the parser extracts the + * "boundary" parameter from the content type of this DataSource, + * skips the 'preamble' and reads bytes till the terminating + * boundary and creates MimeBodyParts for each part of the stream. + * + * @param ds DataSource, can be a MultipartDataSource + * @exception ParseException for failures parsing the message + * @exception MessagingException for other failures + */ + public MimeMultipart(DataSource ds) throws MessagingException { + super(); + + if (ds instanceof MessageAware) { + MessageContext mc = ((MessageAware)ds).getMessageContext(); + setParent(mc.getPart()); + } + + if (ds instanceof MultipartDataSource) { + // ask super to do this for us. + setMultipartDataSource((MultipartDataSource)ds); + return; + } + + // 'ds' was not a MultipartDataSource, we have + // to parse this ourself. + parsed = false; + this.ds = ds; + contentType = ds.getContentType(); + } + + /** + * Initialize flags that control parsing behavior, + * based on System properties described above in + * the class documentation. + * + * @since JavaMail 1.5 + */ + protected void initializeProperties() { + // read properties that control parsing + + // default to true + ignoreMissingEndBoundary = PropUtil.getBooleanSystemProperty( + "mail.mime.multipart.ignoremissingendboundary", true); + // default to true + ignoreMissingBoundaryParameter = PropUtil.getBooleanSystemProperty( + "mail.mime.multipart.ignoremissingboundaryparameter", true); + // default to false + ignoreExistingBoundaryParameter = PropUtil.getBooleanSystemProperty( + "mail.mime.multipart.ignoreexistingboundaryparameter", false); + // default to false + allowEmpty = PropUtil.getBooleanSystemProperty( + "mail.mime.multipart.allowempty", false); + } + + /** + * Set the subtype. This method should be invoked only on a new + * MimeMultipart object created by the client. The default subtype + * of such a multipart object is "mixed". <p> + * + * @param subtype Subtype + * @exception MessagingException for failures + */ + public synchronized void setSubType(String subtype) + throws MessagingException { + ContentType cType = new ContentType(contentType); + cType.setSubType(subtype); + contentType = cType.toString(); + } + + /** + * Return the number of enclosed BodyPart objects. + * + * @return number of parts + */ + @Override + public synchronized int getCount() throws MessagingException { + parse(); + return super.getCount(); + } + + /** + * Get the specified BodyPart. BodyParts are numbered starting at 0. + * + * @param index the index of the desired BodyPart + * @return the Part + * @exception MessagingException if no such BodyPart exists + */ + @Override + public synchronized BodyPart getBodyPart(int index) + throws MessagingException { + parse(); + return super.getBodyPart(index); + } + + /** + * Get the MimeBodyPart referred to by the given ContentID (CID). + * Returns null if the part is not found. + * + * @param CID the ContentID of the desired part + * @return the Part + * @exception MessagingException for failures + */ + public synchronized BodyPart getBodyPart(String CID) + throws MessagingException { + parse(); + + int count = getCount(); + for (int i = 0; i < count; i++) { + MimeBodyPart part = (MimeBodyPart)getBodyPart(i); + String s = part.getContentID(); + if (s != null && s.equals(CID)) + return part; + } + return null; + } + + /** + * Remove the specified part from the multipart message. + * Shifts all the parts after the removed part down one. + * + * @param part The part to remove + * @return true if part removed, false otherwise + * @exception MessagingException if no such Part exists + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + */ + @Override + public boolean removeBodyPart(BodyPart part) throws MessagingException { + parse(); + return super.removeBodyPart(part); + } + + /** + * Remove the part at specified location (starting from 0). + * Shifts all the parts after the removed part down one. + * + * @param index Index of the part to remove + * @exception IndexOutOfBoundsException if the given index + * is out of range. + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception MessagingException for other failures + */ + @Override + public void removeBodyPart(int index) throws MessagingException { + parse(); + super.removeBodyPart(index); + } + + /** + * Adds a Part to the multipart. The BodyPart is appended to + * the list of existing Parts. + * + * @param part The Part to be appended + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception MessagingException for other failures + */ + @Override + public synchronized void addBodyPart(BodyPart part) + throws MessagingException { + parse(); + super.addBodyPart(part); + } + + /** + * Adds a BodyPart at position <code>index</code>. + * If <code>index</code> is not the last one in the list, + * the subsequent parts are shifted up. If <code>index</code> + * is larger than the number of parts present, the + * BodyPart is appended to the end. + * + * @param part The BodyPart to be inserted + * @param index Location where to insert the part + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception MessagingException for other failures + */ + @Override + public synchronized void addBodyPart(BodyPart part, int index) + throws MessagingException { + parse(); + super.addBodyPart(part, index); + } + + /** + * Return true if the final boundary line for this + * multipart was seen. When parsing multipart content, + * this class will (by default) terminate parsing with + * no error if the end of input is reached before seeing + * the final multipart boundary line. In such a case, + * this method will return false. (If the System property + * "mail.mime.multipart.ignoremissingendboundary" is set to + * false, parsing such a message will instead throw a + * MessagingException.) + * + * @return true if the final boundary line was seen + * @exception MessagingException for failures + * @since JavaMail 1.4 + */ + public synchronized boolean isComplete() throws MessagingException { + parse(); + return complete; + } + + /** + * Get the preamble text, if any, that appears before the + * first body part of this multipart. Some protocols, + * such as IMAP, will not allow access to the preamble text. + * + * @return the preamble text, or null if no preamble + * @exception MessagingException for failures + * @since JavaMail 1.4 + */ + public synchronized String getPreamble() throws MessagingException { + parse(); + return preamble; + } + + /** + * Set the preamble text to be included before the first + * body part. Applications should generally not include + * any preamble text. In some cases it may be helpful to + * include preamble text with instructions for users of + * pre-MIME software. The preamble text should be complete + * lines, including newlines. + * + * @param preamble the preamble text + * @exception MessagingException for failures + * @since JavaMail 1.4 + */ + public synchronized void setPreamble(String preamble) + throws MessagingException { + this.preamble = preamble; + } + + /** + * Update headers. The default implementation here just + * calls the <code>updateHeaders</code> method on each of its + * children BodyParts. <p> + * + * Note that the boundary parameter is already set up when + * a new and empty MimeMultipart object is created. <p> + * + * This method is called when the <code>saveChanges</code> + * method is invoked on the Message object containing this + * Multipart. This is typically done as part of the Message + * send process, however note that a client is free to call + * it any number of times. So if the header updating process is + * expensive for a specific MimeMultipart subclass, then it + * might itself want to track whether its internal state actually + * did change, and do the header updating only if necessary. + * + * @exception MessagingException for failures + */ + protected synchronized void updateHeaders() throws MessagingException { + parse(); + for (int i = 0; i < parts.size(); i++) + ((MimeBodyPart)parts.elementAt(i)).updateHeaders(); + } + + /** + * Iterates through all the parts and outputs each MIME part + * separated by a boundary. + */ + @Override + public synchronized void writeTo(OutputStream os) + throws IOException, MessagingException { + parse(); + + String boundary = "--" + + (new ContentType(contentType)).getParameter("boundary"); + LineOutputStream los = new LineOutputStream(os); + + // if there's a preamble, write it out + if (preamble != null) { + byte[] pb = ASCIIUtility.getBytes(preamble); + los.write(pb); + // make sure it ends with a newline + if (pb.length > 0 && + !(pb[pb.length-1] == '\r' || pb[pb.length-1] == '\n')) { + los.writeln(); + } + // XXX - could force a blank line before start boundary + } + + if (parts.size() == 0) { + if (allowEmpty) { + // write out a single empty body part + los.writeln(boundary); // put out boundary + los.writeln(); // put out empty line + } else { + throw new MessagingException("Empty multipart: " + contentType); + } + } else { + for (int i = 0; i < parts.size(); i++) { + los.writeln(boundary); // put out boundary + ((MimeBodyPart)parts.elementAt(i)).writeTo(os); + los.writeln(); // put out empty line + } + } + + // put out last boundary + los.writeln(boundary + "--"); + } + + /** + * Parse the InputStream from our DataSource, constructing the + * appropriate MimeBodyParts. The <code>parsed</code> flag is + * set to true, and if true on entry nothing is done. This + * method is called by all other methods that need data for + * the body parts, to make sure the data has been parsed. + * The {@link #initializeProperties} method is called before + * parsing the data. + * + * @exception ParseException for failures parsing the message + * @exception MessagingException for other failures + * @since JavaMail 1.2 + */ + protected synchronized void parse() throws MessagingException { + if (parsed) + return; + + initializeProperties(); + + InputStream in = null; + SharedInputStream sin = null; + long start = 0, end = 0; + + try { + in = ds.getInputStream(); + if (!(in instanceof ByteArrayInputStream) && + !(in instanceof BufferedInputStream) && + !(in instanceof SharedInputStream)) + in = new BufferedInputStream(in); + } catch (Exception ex) { + throw new MessagingException("No inputstream from datasource", ex); + } + if (in instanceof SharedInputStream) + sin = (SharedInputStream)in; + + ContentType cType = new ContentType(contentType); + String boundary = null; + if (!ignoreExistingBoundaryParameter) { + String bp = cType.getParameter("boundary"); + if (bp != null) + boundary = "--" + bp; + } + if (boundary == null && !ignoreMissingBoundaryParameter && + !ignoreExistingBoundaryParameter) + throw new ParseException("Missing boundary parameter"); + + try { + // Skip and save the preamble + LineInputStream lin = new LineInputStream(in); + StringBuilder preamblesb = null; + String line; + while ((line = lin.readLine()) != null) { + /* + * Strip trailing whitespace. Can't use trim method + * because it's too aggressive. Some bogus MIME + * messages will include control characters in the + * boundary string. + */ + int i; + for (i = line.length() - 1; i >= 0; i--) { + char c = line.charAt(i); + if (!(c == ' ' || c == '\t')) + break; + } + line = line.substring(0, i + 1); + if (boundary != null) { + if (line.equals(boundary)) + break; + if (line.length() == boundary.length() + 2 && + line.startsWith(boundary) && line.endsWith("--")) { + line = null; // signal end of multipart + break; + } + } else { + /* + * Boundary hasn't been defined, does this line + * look like a boundary? If so, assume it is + * the boundary and save it. + */ + if (line.length() > 2 && line.startsWith("--")) { + if (line.length() > 4 && allDashes(line)) { + /* + * The first boundary-like line we find is + * probably *not* the end-of-multipart boundary + * line. More likely it's a line full of dashes + * in the preamble text. Just keep reading. + */ + } else { + boundary = line; + break; + } + } + } + + // save the preamble after skipping blank lines + if (line.length() > 0) { + // accumulate the preamble + if (preamblesb == null) + preamblesb = new StringBuilder(line.length() + 2); + preamblesb.append(line).append(System.lineSeparator()); + } + } + + if (preamblesb != null) + preamble = preamblesb.toString(); + + if (line == null) { + if (allowEmpty) + return; + else + throw new ParseException("Missing start boundary"); + } + + // save individual boundary bytes for comparison later + byte[] bndbytes = ASCIIUtility.getBytes(boundary); + int bl = bndbytes.length; + + /* + * Compile Boyer-Moore parsing tables. + */ + + // initialize Bad Character Shift table + int[] bcs = new int[256]; + for (int i = 0; i < bl; i++) + bcs[bndbytes[i] & 0xff] = i + 1; + + // initialize Good Suffix Shift table + int[] gss = new int[bl]; + NEXT: + for (int i = bl; i > 0; i--) { + int j; // the beginning index of the suffix being considered + for (j = bl - 1; j >= i; j--) { + // Testing for good suffix + if (bndbytes[j] == bndbytes[j - i]) { + // bndbytes[j..len] is a good suffix + gss[j - 1] = i; + } else { + // No match. The array has already been + // filled up with correct values before. + continue NEXT; + } + } + while (j > 0) + gss[--j] = i; + } + gss[bl - 1] = 1; + + /* + * Read and process body parts until we see the + * terminating boundary line (or EOF). + */ + boolean done = false; + getparts: + while (!done) { + InternetHeaders headers = null; + if (sin != null) { + start = sin.getPosition(); + // skip headers + while ((line = lin.readLine()) != null && line.length() > 0) + ; + if (line == null) { + if (!ignoreMissingEndBoundary) + throw new ParseException( + "missing multipart end boundary"); + // assume there's just a missing end boundary + complete = false; + break getparts; + } + } else { + // collect the headers for this body part + headers = createInternetHeaders(in); + } + + if (!in.markSupported()) + throw new MessagingException("Stream doesn't support mark"); + + ByteArrayOutputStream buf = null; + // if we don't have a shared input stream, we copy the data + if (sin == null) + buf = new ByteArrayOutputStream(); + else + end = sin.getPosition(); + int b; + + /* + * These buffers contain the bytes we're checking + * for a match. inbuf is the current buffer and + * previnbuf is the previous buffer. We need the + * previous buffer to check that we're preceeded + * by an EOL. + */ + // XXX - a smarter algorithm would use a sliding window + // over a larger buffer + byte[] inbuf = new byte[bl]; + byte[] previnbuf = new byte[bl]; + int inSize = 0; // number of valid bytes in inbuf + int prevSize = 0; // number of valid bytes in previnbuf + int eolLen; + boolean first = true; + + /* + * Read and save the content bytes in buf. + */ + for (;;) { + in.mark(bl + 4 + 1000); // bnd + "--\r\n" + lots of LWSP + eolLen = 0; + inSize = readFully(in, inbuf, 0, bl); + if (inSize < bl) { + // hit EOF + if (!ignoreMissingEndBoundary) + throw new ParseException( + "missing multipart end boundary"); + if (sin != null) + end = sin.getPosition(); + complete = false; + done = true; + break; + } + // check whether inbuf contains a boundary string + int i; + for (i = bl - 1; i >= 0; i--) { + if (inbuf[i] != bndbytes[i]) + break; + } + if (i < 0) { // matched all bytes + eolLen = 0; + if (!first) { + // working backwards, find out if we were preceeded + // by an EOL, and if so find its length + b = previnbuf[prevSize - 1]; + if (b == '\r' || b == '\n') { + eolLen = 1; + if (b == '\n' && prevSize >= 2) { + b = previnbuf[prevSize - 2]; + if (b == '\r') + eolLen = 2; + } + } + } + if (first || eolLen > 0) { // yes, preceed by EOL + if (sin != null) { + // update "end", in case this really is + // a valid boundary + end = sin.getPosition() - bl - eolLen; + } + // matched the boundary, check for last boundary + int b2 = in.read(); + if (b2 == '-') { + if (in.read() == '-') { + complete = true; + done = true; + break; // ignore trailing text + } + } + // skip linear whitespace + while (b2 == ' ' || b2 == '\t') + b2 = in.read(); + // check for end of line + if (b2 == '\n') + break; // got it! break out of the loop + if (b2 == '\r') { + in.mark(1); + if (in.read() != '\n') + in.reset(); + break; // got it! break out of the loop + } + } + i = 0; + } + + /* + * Get here if boundary didn't match, + * wasn't preceeded by EOL, or wasn't + * followed by whitespace or EOL. + */ + + // compute how many bytes we can skip + int skip = Math.max(i + 1 - bcs[inbuf[i] & 0x7f], gss[i]); + // want to keep at least two characters + if (skip < 2) { + // only skipping one byte, save one byte + // from previous buffer as well + // first, write out bytes we're done with + if (sin == null && prevSize > 1) + buf.write(previnbuf, 0, prevSize - 1); + in.reset(); + skipFully(in, 1); + if (prevSize >= 1) { // is there a byte to save? + // yes, save one from previous and one from current + previnbuf[0] = previnbuf[prevSize - 1]; + previnbuf[1] = inbuf[0]; + prevSize = 2; + } else { + // no previous bytes to save, can only save current + previnbuf[0] = inbuf[0]; + prevSize = 1; + } + } else { + // first, write out data from previous buffer before + // we dump it + if (prevSize > 0 && sin == null) + buf.write(previnbuf, 0, prevSize); + // all the bytes we're skipping are saved in previnbuf + prevSize = skip; + in.reset(); + skipFully(in, prevSize); + // swap buffers + byte[] tmp = inbuf; + inbuf = previnbuf; + previnbuf = tmp; + } + first = false; + } + + /* + * Create a MimeBody element to represent this body part. + */ + MimeBodyPart part; + if (sin != null) { + part = createMimeBodyPartIs(sin.newStream(start, end)); + } else { + // write out data from previous buffer, not including EOL + if (prevSize - eolLen > 0) + buf.write(previnbuf, 0, prevSize - eolLen); + // if we didn't find a trailing boundary, + // the current buffer has data we need too + if (!complete && inSize > 0) + buf.write(inbuf, 0, inSize); + part = createMimeBodyPart(headers, buf.toByteArray()); + } + super.addBodyPart(part); + } + } catch (IOException ioex) { + throw new MessagingException("IO Error", ioex); + } finally { + try { + in.close(); + } catch (IOException cex) { + // ignore + } + } + + parsed = true; + } + + /** + * Is the string all dashes ('-')? + */ + private static boolean allDashes(String s) { + for (int i = 0; i < s.length(); i++) { + if (s.charAt(i) != '-') + return false; + } + return true; + } + + /** + * Read data from the input stream to fill the buffer starting + * at the specified offset with the specified number of bytes. + * If len is zero, return zero. If at EOF, return -1. Otherwise, + * return the number of bytes read. Call the read method on the + * input stream as many times as necessary to read len bytes. + * + * @param in InputStream to read from + * @param buf buffer to read into + * @param off offset in the buffer for first byte + * @param len number of bytes to read + * @return -1 on EOF, otherwise number of bytes read + * @exception IOException on I/O errors + */ + private static int readFully(InputStream in, byte[] buf, int off, int len) + throws IOException { + if (len == 0) + return 0; + int total = 0; + while (len > 0) { + int bsize = in.read(buf, off, len); + if (bsize <= 0) // should never be zero + break; + off += bsize; + total += bsize; + len -= bsize; + } + return total > 0 ? total : -1; + } + + /** + * Skip the specified number of bytes, repeatedly calling + * the skip method as necessary. + */ + private void skipFully(InputStream in, long offset) throws IOException { + while (offset > 0) { + long cur = in.skip(offset); + if (cur <= 0) + throw new EOFException("can't skip"); + offset -= cur; + } + } + + /** + * Create and return an InternetHeaders object that loads the + * headers from the given InputStream. Subclasses can override + * this method to return a subclass of InternetHeaders, if + * necessary. This implementation simply constructs and returns + * an InternetHeaders object. + * + * @param is the InputStream to read the headers from + * @return an InternetHeaders object + * @exception MessagingException for failures + * @since JavaMail 1.2 + */ + protected InternetHeaders createInternetHeaders(InputStream is) + throws MessagingException { + return new InternetHeaders(is); + } + + /** + * Create and return a MimeBodyPart object to represent a + * body part parsed from the InputStream. Subclasses can override + * this method to return a subclass of MimeBodyPart, if + * necessary. This implementation simply constructs and returns + * a MimeBodyPart object. + * + * @param headers the headers for the body part + * @param content the content of the body part + * @return a MimeBodyPart + * @exception MessagingException for failures + * @since JavaMail 1.2 + */ + protected MimeBodyPart createMimeBodyPart(InternetHeaders headers, + byte[] content) throws MessagingException { + return new MimeBodyPart(headers, content); + } + + /** + * Create and return a MimeBodyPart object to represent a + * body part parsed from the InputStream. Subclasses can override + * this method to return a subclass of MimeBodyPart, if + * necessary. This implementation simply constructs and returns + * a MimeBodyPart object. + * + * @param is InputStream containing the body part + * @return a MimeBodyPart + * @exception MessagingException for failures + * @since JavaMail 1.2 + */ + protected MimeBodyPart createMimeBodyPart(InputStream is) + throws MessagingException { + return new MimeBodyPart(is); + } + + private MimeBodyPart createMimeBodyPartIs(InputStream is) + throws MessagingException { + try { + return createMimeBodyPart(is); + } finally { + try { + is.close(); + } catch (IOException ex) { + // ignore it + } + } + } +}
diff --git a/mail/src/main/java/javax/mail/internet/MimePart.java b/mail/src/main/java/javax/mail/internet/MimePart.java new file mode 100644 index 0000000..b43fc4a --- /dev/null +++ b/mail/src/main/java/javax/mail/internet/MimePart.java
@@ -0,0 +1,224 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import javax.mail.*; +import java.io.*; +import java.util.Enumeration; + +/** + * The MimePart interface models an <strong>Entity</strong> as defined + * by MIME (RFC2045, Section 2.4). <p> + * + * MimePart extends the Part interface to add additional RFC822 and MIME + * specific semantics and attributes. It provides the base interface for + * the MimeMessage and MimeBodyPart classes + * + * <hr> <strong>A note on RFC822 and MIME headers</strong><p> + * + * RFC822 and MIME header fields <strong>must</strong> contain only + * US-ASCII characters. If a header contains non US-ASCII characters, + * it must be encoded as per the rules in RFC 2047. The MimeUtility + * class provided in this package can be used to to achieve this. + * Callers of the <code>setHeader</code>, <code>addHeader</code>, and + * <code>addHeaderLine</code> methods are responsible for enforcing + * the MIME requirements for the specified headers. In addition, these + * header fields must be folded (wrapped) before being sent if they + * exceed the line length limitation for the transport (1000 bytes for + * SMTP). Received headers may have been folded. The application is + * responsible for folding and unfolding headers as appropriate. <p> + * + * @see MimeUtility + * @see javax.mail.Part + * @author John Mani + */ + +public interface MimePart extends Part { + + /** + * Get the values of all header fields available for this header, + * returned as a single String, with the values separated by the + * delimiter. If the delimiter is <code>null</code>, only the + * first value is returned. + * + * @param name the name of this header + * @param delimiter delimiter between fields in returned string + * @return the value fields for all headers with + * this name + * @exception MessagingException for failures + */ + public String getHeader(String name, String delimiter) + throws MessagingException; + + /** + * Add a raw RFC822 header-line. + * + * @param line the line to add + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this Part is + * obtained from a READ_ONLY folder + * @exception MessagingException for other failures + */ + public void addHeaderLine(String line) throws MessagingException; + + /** + * Get all header lines as an Enumeration of Strings. A Header + * line is a raw RFC822 header-line, containing both the "name" + * and "value" field. + * + * @return an Enumeration of Strings + * @exception MessagingException for failures + */ + public Enumeration<String> getAllHeaderLines() throws MessagingException; + + /** + * Get matching header lines as an Enumeration of Strings. + * A Header line is a raw RFC822 header-line, containing both + * the "name" and "value" field. + * + * @param names the headers to return + * @return an Enumeration of Strings + * @exception MessagingException for failures + */ + public Enumeration<String> getMatchingHeaderLines(String[] names) + throws MessagingException; + + /** + * Get non-matching header lines as an Enumeration of Strings. + * A Header line is a raw RFC822 header-line, containing both + * the "name" and "value" field. + * + * @param names the headers to not return + * @return an Enumeration of Strings + * @exception MessagingException for failures + */ + public Enumeration<String> getNonMatchingHeaderLines(String[] names) + throws MessagingException; + + /** + * Get the transfer encoding of this part. + * + * @return content-transfer-encoding + * @exception MessagingException for failures + */ + public String getEncoding() throws MessagingException; + + /** + * Get the Content-ID of this part. Returns null if none present. + * + * @return content-ID + * @exception MessagingException for failures + */ + public String getContentID() throws MessagingException; + + /** + * Get the Content-MD5 digest of this part. Returns null if + * none present. + * + * @return content-MD5 + * @exception MessagingException for failures + */ + public String getContentMD5() throws MessagingException; + + /** + * Set the Content-MD5 of this part. + * + * @param md5 the MD5 value + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this Part is + * obtained from a READ_ONLY folder + */ + public void setContentMD5(String md5) throws MessagingException; + + /** + * Get the language tags specified in the Content-Language header + * of this MimePart. The Content-Language header is defined by + * RFC 1766. Returns <code>null</code> if this header is not + * available. + * + * @return array of content language strings + * @exception MessagingException for failures + */ + public String[] getContentLanguage() throws MessagingException; + + /** + * Set the Content-Language header of this MimePart. The + * Content-Language header is defined by RFC1766. + * + * @param languages array of language tags + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this Part is + * obtained from a READ_ONLY folder + */ + public void setContentLanguage(String[] languages) + throws MessagingException; + + /** + * Convenience method that sets the given String as this + * part's content, with a MIME type of "text/plain". If the + * string contains non US-ASCII characters. it will be encoded + * using the platform's default charset. The charset is also + * used to set the "charset" parameter. <p> + * + * Note that there may be a performance penalty if + * <code>text</code> is large, since this method may have + * to scan all the characters to determine what charset to + * use. <p> + * + * If the charset is already known, use the + * <code>setText</code> method that takes the charset parameter. + * + * @param text the text content to set + * @exception MessagingException if an error occurs + * @see #setText(String text, String charset) + */ + @Override + public void setText(String text) throws MessagingException; + + /** + * Convenience method that sets the given String as this part's + * content, with a MIME type of "text/plain" and the specified + * charset. The given Unicode string will be charset-encoded + * using the specified charset. The charset is also used to set + * "charset" parameter. + * + * @param text the text content to set + * @param charset the charset to use for the text + * @exception MessagingException if an error occurs + */ + public void setText(String text, String charset) + throws MessagingException; + + /** + * Convenience method that sets the given String as this part's + * content, with a primary MIME type of "text" and the specified + * MIME subtype. The given Unicode string will be charset-encoded + * using the specified charset. The charset is also used to set + * the "charset" parameter. + * + * @param text the text content to set + * @param charset the charset to use for the text + * @param subtype the MIME subtype to use (e.g., "html") + * @exception MessagingException if an error occurs + * @since JavaMail 1.4 + */ + public void setText(String text, String charset, String subtype) + throws MessagingException; +}
diff --git a/mail/src/main/java/javax/mail/internet/MimePartDataSource.java b/mail/src/main/java/javax/mail/internet/MimePartDataSource.java new file mode 100644 index 0000000..0e481ae --- /dev/null +++ b/mail/src/main/java/javax/mail/internet/MimePartDataSource.java
@@ -0,0 +1,153 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import javax.mail.*; +import javax.activation.*; +import java.io.*; +import java.net.UnknownServiceException; +import com.sun.mail.util.PropUtil; +import com.sun.mail.util.FolderClosedIOException; + +/** + * A utility class that implements a DataSource out of + * a MimePart. This class is primarily meant for service providers. + * + * @see javax.mail.internet.MimePart + * @see javax.activation.DataSource + * @author John Mani + */ + +public class MimePartDataSource implements DataSource, MessageAware { + /** + * The MimePart that provides the data for this DataSource. + * + * @since JavaMail 1.4 + */ + protected MimePart part; + + private MessageContext context; + + /** + * Constructor, that constructs a DataSource from a MimePart. + * + * @param part the MimePart + */ + public MimePartDataSource(MimePart part) { + this.part = part; + } + + /** + * Returns an input stream from this MimePart. <p> + * + * This method applies the appropriate transfer-decoding, based + * on the Content-Transfer-Encoding attribute of this MimePart. + * Thus the returned input stream is a decoded stream of bytes.<p> + * + * This implementation obtains the raw content from the Part + * using the <code>getContentStream()</code> method and decodes + * it using the <code>MimeUtility.decode()</code> method. + * + * @see javax.mail.internet.MimeMessage#getContentStream + * @see javax.mail.internet.MimeBodyPart#getContentStream + * @see javax.mail.internet.MimeUtility#decode + * @return decoded input stream + */ + @Override + public InputStream getInputStream() throws IOException { + InputStream is; + + try { + if (part instanceof MimeBodyPart) + is = ((MimeBodyPart)part).getContentStream(); + else if (part instanceof MimeMessage) + is = ((MimeMessage)part).getContentStream(); + else + throw new MessagingException("Unknown part"); + + String encoding = + MimeBodyPart.restrictEncoding(part, part.getEncoding()); + if (encoding != null) + return MimeUtility.decode(is, encoding); + else + return is; + } catch (FolderClosedException fex) { + throw new FolderClosedIOException(fex.getFolder(), + fex.getMessage()); + } catch (MessagingException mex) { + IOException ioex = new IOException(mex.getMessage()); + ioex.initCause(mex); + throw ioex; + } + } + + /** + * DataSource method to return an output stream. <p> + * + * This implementation throws the UnknownServiceException. + */ + @Override + public OutputStream getOutputStream() throws IOException { + throw new UnknownServiceException("Writing not supported"); + } + + /** + * Returns the content-type of this DataSource. <p> + * + * This implementation just invokes the <code>getContentType</code> + * method on the MimePart. + */ + @Override + public String getContentType() { + try { + return part.getContentType(); + } catch (MessagingException mex) { + // would like to be able to reflect the exception to the + // application, but since we can't do that we return a + // generic "unknown" value here and hope for another + // exception later. + return "application/octet-stream"; + } + } + + /** + * DataSource method to return a name. <p> + * + * This implementation just returns an empty string. + */ + @Override + public String getName() { + try { + if (part instanceof MimeBodyPart) + return ((MimeBodyPart)part).getFileName(); + } catch (MessagingException mex) { + // ignore it + } + return ""; + } + + /** + * Return the <code>MessageContext</code> for the current part. + * @since JavaMail 1.1 + */ + @Override + public synchronized MessageContext getMessageContext() { + if (context == null) + context = new MessageContext(part); + return context; + } +}
diff --git a/mail/src/main/java/javax/mail/internet/MimeUtility.java b/mail/src/main/java/javax/mail/internet/MimeUtility.java new file mode 100644 index 0000000..2bb043b --- /dev/null +++ b/mail/src/main/java/javax/mail/internet/MimeUtility.java
@@ -0,0 +1,1708 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import javax.mail.MessagingException; +import javax.mail.EncodingAware; +import javax.activation.*; +import java.util.*; +import java.io.*; +import java.nio.charset.Charset; +import com.sun.mail.util.PropUtil; +import com.sun.mail.util.ASCIIUtility; +import com.sun.mail.util.BASE64DecoderStream; +import com.sun.mail.util.BASE64EncoderStream; +import com.sun.mail.util.BEncoderStream; +import com.sun.mail.util.LineInputStream; +import com.sun.mail.util.LineOutputStream; +import com.sun.mail.util.LogOutputStream; +import com.sun.mail.util.QDecoderStream; +import com.sun.mail.util.QEncoderStream; +import com.sun.mail.util.QPDecoderStream; +import com.sun.mail.util.QPEncoderStream; +import com.sun.mail.util.UUDecoderStream; +import com.sun.mail.util.UUEncoderStream; + +/** + * This is a utility class that provides various MIME related + * functionality. <p> + * + * There are a set of methods to encode and decode MIME headers as + * per RFC 2047. Note that, in general, these methods are + * <strong>not</strong> needed when using methods such as + * <code>setSubject</code> and <code>setRecipients</code>; JavaMail + * will automatically encode and decode data when using these "higher + * level" methods. The methods below are only needed when maniuplating + * raw MIME headers using <code>setHeader</code> and <code>getHeader</code> + * methods. A brief description on handling such headers is given below: <p> + * + * RFC 822 mail headers <strong>must</strong> contain only US-ASCII + * characters. Headers that contain non US-ASCII characters must be + * encoded so that they contain only US-ASCII characters. Basically, + * this process involves using either BASE64 or QP to encode certain + * characters. RFC 2047 describes this in detail. <p> + * + * In Java, Strings contain (16 bit) Unicode characters. ASCII is a + * subset of Unicode (and occupies the range 0 - 127). A String + * that contains only ASCII characters is already mail-safe. If the + * String contains non US-ASCII characters, it must be encoded. An + * additional complexity in this step is that since Unicode is not + * yet a widely used charset, one might want to first charset-encode + * the String into another charset and then do the transfer-encoding. + * <p> + * Note that to get the actual bytes of a mail-safe String (say, + * for sending over SMTP), one must do + * <blockquote><pre> + * + * byte[] bytes = string.getBytes("iso-8859-1"); + * + * </pre></blockquote><p> + * + * The <code>setHeader</code> and <code>addHeader</code> methods + * on MimeMessage and MimeBodyPart assume that the given header values + * are Unicode strings that contain only US-ASCII characters. Hence + * the callers of those methods must insure that the values they pass + * do not contain non US-ASCII characters. The methods in this class + * help do this. <p> + * + * The <code>getHeader</code> family of methods on MimeMessage and + * MimeBodyPart return the raw header value. These might be encoded + * as per RFC 2047, and if so, must be decoded into Unicode Strings. + * The methods in this class help to do this. <p> + * + * Several System properties control strict conformance to the MIME + * spec. Note that these are not session properties but must be set + * globally as System properties. <p> + * + * The <code>mail.mime.decodetext.strict</code> property controls + * decoding of MIME encoded words. The MIME spec requires that encoded + * words start at the beginning of a whitespace separated word. Some + * mailers incorrectly include encoded words in the middle of a word. + * If the <code>mail.mime.decodetext.strict</code> System property is + * set to <code>"false"</code>, an attempt will be made to decode these + * illegal encoded words. The default is true. <p> + * + * The <code>mail.mime.encodeeol.strict</code> property controls the + * choice of Content-Transfer-Encoding for MIME parts that are not of + * type "text". Often such parts will contain textual data for which + * an encoding that allows normal end of line conventions is appropriate. + * In rare cases, such a part will appear to contain entirely textual + * data, but will require an encoding that preserves CR and LF characters + * without change. If the <code>mail.mime.encodeeol.strict</code> + * System property is set to <code>"true"</code>, such an encoding will + * be used when necessary. The default is false. <p> + * + * In addition, the <code>mail.mime.charset</code> System property can + * be used to specify the default MIME charset to use for encoded words + * and text parts that don't otherwise specify a charset. Normally, the + * default MIME charset is derived from the default Java charset, as + * specified in the <code>file.encoding</code> System property. Most + * applications will have no need to explicitly set the default MIME + * charset. In cases where the default MIME charset to be used for + * mail messages is different than the charset used for files stored on + * the system, this property should be set. <p> + * + * The current implementation also supports the following System property. + * <p> + * The <code>mail.mime.ignoreunknownencoding</code> property controls + * whether unknown values in the <code>Content-Transfer-Encoding</code> + * header, as passed to the <code>decode</code> method, cause an exception. + * If set to <code>"true"</code>, unknown values are ignored and 8bit + * encoding is assumed. Otherwise, unknown values cause a MessagingException + * to be thrown. + * + * @author John Mani + * @author Bill Shannon + */ + +public class MimeUtility { + + // This class cannot be instantiated + private MimeUtility() { } + + public static final int ALL = -1; + + // cached map of whether a charset is compatible with ASCII + // Map<String,Boolean> + private static final Map<String, Boolean> nonAsciiCharsetMap + = new HashMap<>(); + + private static final boolean decodeStrict = + PropUtil.getBooleanSystemProperty("mail.mime.decodetext.strict", true); + private static final boolean encodeEolStrict = + PropUtil.getBooleanSystemProperty("mail.mime.encodeeol.strict", false); + private static final boolean ignoreUnknownEncoding = + PropUtil.getBooleanSystemProperty( + "mail.mime.ignoreunknownencoding", false); + private static final boolean allowUtf8 = + PropUtil.getBooleanSystemProperty("mail.mime.allowutf8", false); + /* + * The following two properties allow disabling the fold() + * and unfold() methods and reverting to the previous behavior. + * They should never need to be changed and are here only because + * of my paranoid concern with compatibility. + */ + private static final boolean foldEncodedWords = + PropUtil.getBooleanSystemProperty("mail.mime.foldencodedwords", false); + private static final boolean foldText = + PropUtil.getBooleanSystemProperty("mail.mime.foldtext", true); + + + /** + * Get the Content-Transfer-Encoding that should be applied + * to the input stream of this DataSource, to make it mail-safe. <p> + * + * The algorithm used here is: <br> + * <ul> + * <li> + * If the DataSource implements {@link EncodingAware}, ask it + * what encoding to use. If it returns non-null, return that value. + * <li> + * If the primary type of this datasource is "text" and if all + * the bytes in its input stream are US-ASCII, then the encoding + * is "7bit". If more than half of the bytes are non-US-ASCII, then + * the encoding is "base64". If less than half of the bytes are + * non-US-ASCII, then the encoding is "quoted-printable". + * <li> + * If the primary type of this datasource is not "text", then if + * all the bytes of its input stream are US-ASCII, the encoding + * is "7bit". If there is even one non-US-ASCII character, the + * encoding is "base64". + * </ul> + * + * @param ds the DataSource + * @return the encoding. This is either "7bit", + * "quoted-printable" or "base64" + */ + public static String getEncoding(DataSource ds) { + ContentType cType = null; + InputStream is = null; + String encoding = null; + + if (ds instanceof EncodingAware) { + encoding = ((EncodingAware)ds).getEncoding(); + if (encoding != null) + return encoding; + } + try { + cType = new ContentType(ds.getContentType()); + is = ds.getInputStream(); + + boolean isText = cType.match("text/*"); + // if not text, stop processing when we see non-ASCII + int i = checkAscii(is, ALL, !isText); + switch (i) { + case ALL_ASCII: + encoding = "7bit"; // all ASCII + break; + case MOSTLY_ASCII: + if (isText && nonAsciiCharset(cType)) + encoding = "base64"; // charset isn't compatible with ASCII + else + encoding = "quoted-printable"; // mostly ASCII + break; + default: + encoding = "base64"; // mostly binary + break; + } + + } catch (Exception ex) { + return "base64"; // what else ?! + } finally { + // Close the input stream + try { + if (is != null) + is.close(); + } catch (IOException ioex) { } + } + + return encoding; + } + + /** + * Determine whether the charset in the Content-Type is compatible + * with ASCII or not. A charset is compatible with ASCII if the + * encoded byte stream representing the Unicode string "\r\n" is + * the ASCII characters CR and LF. For example, the utf-16be + * charset is not compatible with ASCII. + * + * For performance, we keep a static map that caches the results. + */ + private static boolean nonAsciiCharset(ContentType ct) { + String charset = ct.getParameter("charset"); + if (charset == null) + return false; + charset = charset.toLowerCase(Locale.ENGLISH); + Boolean bool; + synchronized (nonAsciiCharsetMap) { + bool = nonAsciiCharsetMap.get(charset); + } + if (bool == null) { + try { + byte[] b = "\r\n".getBytes(charset); + bool = Boolean.valueOf( + b.length != 2 || b[0] != 015 || b[1] != 012); + } catch (UnsupportedEncodingException uex) { + bool = Boolean.FALSE; // a guess + } catch (RuntimeException ex) { + bool = Boolean.TRUE; // one of the weird ones? + } + synchronized (nonAsciiCharsetMap) { + nonAsciiCharsetMap.put(charset, bool); + } + } + return bool.booleanValue(); + } + + /** + * Same as <code>getEncoding(DataSource)</code> except that instead + * of reading the data from an <code>InputStream</code> it uses the + * <code>writeTo</code> method to examine the data. This is more + * efficient in the common case of a <code>DataHandler</code> + * created with an object and a MIME type (for example, a + * "text/plain" String) because all the I/O is done in this + * thread. In the case requiring an <code>InputStream</code> the + * <code>DataHandler</code> uses a thread, a pair of pipe streams, + * and the <code>writeTo</code> method to produce the data. <p> + * + * @param dh the DataHandler + * @return the Content-Transfer-Encoding + * @since JavaMail 1.2 + */ + public static String getEncoding(DataHandler dh) { + ContentType cType = null; + String encoding = null; + + /* + * Try to pick the most efficient means of determining the + * encoding. If this DataHandler was created using a DataSource, + * the getEncoding(DataSource) method is typically faster. If + * the DataHandler was created with an object, this method is + * much faster. To distinguish the two cases, we use a heuristic. + * A DataHandler created with an object will always have a null name. + * A DataHandler created with a DataSource will usually have a + * non-null name. + * + * XXX - This is actually quite a disgusting hack, but it makes + * a common case run over twice as fast. + */ + if (dh.getName() != null) + return getEncoding(dh.getDataSource()); + + try { + cType = new ContentType(dh.getContentType()); + } catch (Exception ex) { + return "base64"; // what else ?! + } + + if (cType.match("text/*")) { + // Check all of the available bytes + AsciiOutputStream aos = new AsciiOutputStream(false, false); + try { + dh.writeTo(aos); + } catch (IOException ex) { + // ignore it, can't happen + } + switch (aos.getAscii()) { + case ALL_ASCII: + encoding = "7bit"; // all ascii + break; + case MOSTLY_ASCII: + encoding = "quoted-printable"; // mostly ascii + break; + default: + encoding = "base64"; // mostly binary + break; + } + } else { // not "text" + // Check all of available bytes, break out if we find + // at least one non-US-ASCII character + AsciiOutputStream aos = + new AsciiOutputStream(true, encodeEolStrict); + try { + dh.writeTo(aos); + } catch (IOException ex) { } // ignore it + if (aos.getAscii() == ALL_ASCII) // all ascii + encoding = "7bit"; + else // found atleast one non-ascii character, use b64 + encoding = "base64"; + } + + return encoding; + } + + /** + * Decode the given input stream. The Input stream returned is + * the decoded input stream. All the encodings defined in RFC 2045 + * are supported here. They include "base64", "quoted-printable", + * "7bit", "8bit", and "binary". In addition, "uuencode" is also + * supported. <p> + * + * In the current implementation, if the + * <code>mail.mime.ignoreunknownencoding</code> system property is set to + * <code>"true"</code>, unknown encoding values are ignored and the + * original InputStream is returned. + * + * @param is input stream + * @param encoding the encoding of the stream. + * @return decoded input stream. + * @exception MessagingException if the encoding is unknown + */ + public static InputStream decode(InputStream is, String encoding) + throws MessagingException { + if (encoding.equalsIgnoreCase("base64")) + return new BASE64DecoderStream(is); + else if (encoding.equalsIgnoreCase("quoted-printable")) + return new QPDecoderStream(is); + else if (encoding.equalsIgnoreCase("uuencode") || + encoding.equalsIgnoreCase("x-uuencode") || + encoding.equalsIgnoreCase("x-uue")) + return new UUDecoderStream(is); + else if (encoding.equalsIgnoreCase("binary") || + encoding.equalsIgnoreCase("7bit") || + encoding.equalsIgnoreCase("8bit")) + return is; + else { + if (!ignoreUnknownEncoding) + throw new MessagingException("Unknown encoding: " + encoding); + return is; + } + } + + /** + * Wrap an encoder around the given output stream. + * All the encodings defined in RFC 2045 are supported here. + * They include "base64", "quoted-printable", "7bit", "8bit" and + * "binary". In addition, "uuencode" is also supported. + * + * @param os output stream + * @param encoding the encoding of the stream. + * @return output stream that applies the + * specified encoding. + * @exception MessagingException if the encoding is unknown + */ + public static OutputStream encode(OutputStream os, String encoding) + throws MessagingException { + if (encoding == null) + return os; + else if (encoding.equalsIgnoreCase("base64")) + return new BASE64EncoderStream(os); + else if (encoding.equalsIgnoreCase("quoted-printable")) + return new QPEncoderStream(os); + else if (encoding.equalsIgnoreCase("uuencode") || + encoding.equalsIgnoreCase("x-uuencode") || + encoding.equalsIgnoreCase("x-uue")) + return new UUEncoderStream(os); + else if (encoding.equalsIgnoreCase("binary") || + encoding.equalsIgnoreCase("7bit") || + encoding.equalsIgnoreCase("8bit")) + return os; + else + throw new MessagingException("Unknown encoding: " +encoding); + } + + /** + * Wrap an encoder around the given output stream. + * All the encodings defined in RFC 2045 are supported here. + * They include "base64", "quoted-printable", "7bit", "8bit" and + * "binary". In addition, "uuencode" is also supported. + * The <code>filename</code> parameter is used with the "uuencode" + * encoding and is included in the encoded output. + * + * @param os output stream + * @param encoding the encoding of the stream. + * @param filename name for the file being encoded (only used + * with uuencode) + * @return output stream that applies the + * specified encoding. + * @exception MessagingException for unknown encodings + * @since JavaMail 1.2 + */ + public static OutputStream encode(OutputStream os, String encoding, + String filename) + throws MessagingException { + if (encoding == null) + return os; + else if (encoding.equalsIgnoreCase("base64")) + return new BASE64EncoderStream(os); + else if (encoding.equalsIgnoreCase("quoted-printable")) + return new QPEncoderStream(os); + else if (encoding.equalsIgnoreCase("uuencode") || + encoding.equalsIgnoreCase("x-uuencode") || + encoding.equalsIgnoreCase("x-uue")) + return new UUEncoderStream(os, filename); + else if (encoding.equalsIgnoreCase("binary") || + encoding.equalsIgnoreCase("7bit") || + encoding.equalsIgnoreCase("8bit")) + return os; + else + throw new MessagingException("Unknown encoding: " +encoding); + } + + /** + * Encode a RFC 822 "text" token into mail-safe form as per + * RFC 2047. <p> + * + * The given Unicode string is examined for non US-ASCII + * characters. If the string contains only US-ASCII characters, + * it is returned as-is. If the string contains non US-ASCII + * characters, it is first character-encoded using the platform's + * default charset, then transfer-encoded using either the B or + * Q encoding. The resulting bytes are then returned as a Unicode + * string containing only ASCII characters. <p> + * + * Note that this method should be used to encode only + * "unstructured" RFC 822 headers. <p> + * + * Example of usage: + * <blockquote><pre> + * + * MimePart part = ... + * String rawvalue = "FooBar Mailer, Japanese version 1.1" + * try { + * // If we know for sure that rawvalue contains only US-ASCII + * // characters, we can skip the encoding part + * part.setHeader("X-mailer", MimeUtility.encodeText(rawvalue)); + * } catch (UnsupportedEncodingException e) { + * // encoding failure + * } catch (MessagingException me) { + * // setHeader() failure + * } + * + * </pre></blockquote><p> + * + * @param text Unicode string + * @return Unicode string containing only US-ASCII characters + * @exception UnsupportedEncodingException if the encoding fails + */ + public static String encodeText(String text) + throws UnsupportedEncodingException { + return encodeText(text, null, null); + } + + /** + * Encode a RFC 822 "text" token into mail-safe form as per + * RFC 2047. <p> + * + * The given Unicode string is examined for non US-ASCII + * characters. If the string contains only US-ASCII characters, + * it is returned as-is. If the string contains non US-ASCII + * characters, it is first character-encoded using the specified + * charset, then transfer-encoded using either the B or Q encoding. + * The resulting bytes are then returned as a Unicode string + * containing only ASCII characters. <p> + * + * Note that this method should be used to encode only + * "unstructured" RFC 822 headers. + * + * @param text the header value + * @param charset the charset. If this parameter is null, the + * platform's default chatset is used. + * @param encoding the encoding to be used. Currently supported + * values are "B" and "Q". If this parameter is null, then + * the "Q" encoding is used if most of characters to be + * encoded are in the ASCII charset, otherwise "B" encoding + * is used. + * @return Unicode string containing only US-ASCII characters + * @exception UnsupportedEncodingException if the charset + * conversion failed. + */ + public static String encodeText(String text, String charset, + String encoding) + throws UnsupportedEncodingException { + return encodeWord(text, charset, encoding, false); + } + + /** + * Decode "unstructured" headers, that is, headers that are defined + * as '*text' as per RFC 822. <p> + * + * The string is decoded using the algorithm specified in + * RFC 2047, Section 6.1. If the charset-conversion fails + * for any sequence, an UnsupportedEncodingException is thrown. + * If the String is not an RFC 2047 style encoded header, it is + * returned as-is <p> + * + * Example of usage: + * <blockquote><pre> + * + * MimePart part = ... + * String rawvalue = null; + * String value = null; + * try { + * if ((rawvalue = part.getHeader("X-mailer")[0]) != null) + * value = MimeUtility.decodeText(rawvalue); + * } catch (UnsupportedEncodingException e) { + * // Don't care + * value = rawvalue; + * } catch (MessagingException me) { } + * + * return value; + * + * </pre></blockquote><p> + * + * @param etext the possibly encoded value + * @return the decoded text + * @exception UnsupportedEncodingException if the charset + * conversion failed. + */ + public static String decodeText(String etext) + throws UnsupportedEncodingException { + /* + * We look for sequences separated by "linear-white-space". + * (as per RFC 2047, Section 6.1) + * RFC 822 defines "linear-white-space" as SPACE | HT | CR | NL. + */ + String lwsp = " \t\n\r"; + StringTokenizer st; + + /* + * First, lets do a quick run thru the string and check + * whether the sequence "=?" exists at all. If none exists, + * we know there are no encoded-words in here and we can just + * return the string as-is, without suffering thru the later + * decoding logic. + * This handles the most common case of unencoded headers + * efficiently. + */ + if (etext.indexOf("=?") == -1) + return etext; + + // Encoded words found. Start decoding ... + + st = new StringTokenizer(etext, lwsp, true); + StringBuilder sb = new StringBuilder(); // decode buffer + StringBuilder wsb = new StringBuilder(); // white space buffer + boolean prevWasEncoded = false; + + while (st.hasMoreTokens()) { + char c; + String s = st.nextToken(); + // If whitespace, append it to the whitespace buffer + if (((c = s.charAt(0)) == ' ') || (c == '\t') || + (c == '\r') || (c == '\n')) + wsb.append(c); + else { + // Check if token is an 'encoded-word' .. + String word; + try { + word = decodeWord(s); + // Yes, this IS an 'encoded-word'. + if (!prevWasEncoded && wsb.length() > 0) { + // if the previous word was also encoded, we + // should ignore the collected whitespace. Else + // we include the whitespace as well. + sb.append(wsb); + } + prevWasEncoded = true; + } catch (ParseException pex) { + // This is NOT an 'encoded-word'. + word = s; + // possibly decode inner encoded words + if (!decodeStrict) { + String dword = decodeInnerWords(word); + if (dword != word) { + // if a different String object was returned, + // decoding was done. + if (prevWasEncoded && word.startsWith("=?")) { + // encoded followed by encoded, + // throw away whitespace between + } else { + // include collected whitespace .. + if (wsb.length() > 0) + sb.append(wsb); + } + // did original end with encoded? + prevWasEncoded = word.endsWith("?="); + word = dword; + } else { + // include collected whitespace .. + if (wsb.length() > 0) + sb.append(wsb); + prevWasEncoded = false; + } + } else { + // include collected whitespace .. + if (wsb.length() > 0) + sb.append(wsb); + prevWasEncoded = false; + } + } + sb.append(word); // append the actual word + wsb.setLength(0); // reset wsb for reuse + } + } + sb.append(wsb); // append trailing whitespace + return sb.toString(); + } + + /** + * Encode a RFC 822 "word" token into mail-safe form as per + * RFC 2047. <p> + * + * The given Unicode string is examined for non US-ASCII + * characters. If the string contains only US-ASCII characters, + * it is returned as-is. If the string contains non US-ASCII + * characters, it is first character-encoded using the platform's + * default charset, then transfer-encoded using either the B or + * Q encoding. The resulting bytes are then returned as a Unicode + * string containing only ASCII characters. <p> + * + * This method is meant to be used when creating RFC 822 "phrases". + * The InternetAddress class, for example, uses this to encode + * it's 'phrase' component. + * + * @param word Unicode string + * @return Array of Unicode strings containing only US-ASCII + * characters. + * @exception UnsupportedEncodingException if the encoding fails + */ + public static String encodeWord(String word) + throws UnsupportedEncodingException { + return encodeWord(word, null, null); + } + + /** + * Encode a RFC 822 "word" token into mail-safe form as per + * RFC 2047. <p> + * + * The given Unicode string is examined for non US-ASCII + * characters. If the string contains only US-ASCII characters, + * it is returned as-is. If the string contains non US-ASCII + * characters, it is first character-encoded using the specified + * charset, then transfer-encoded using either the B or Q encoding. + * The resulting bytes are then returned as a Unicode string + * containing only ASCII characters. <p> + * + * @param word Unicode string + * @param charset the MIME charset + * @param encoding the encoding to be used. Currently supported + * values are "B" and "Q". If this parameter is null, then + * the "Q" encoding is used if most of characters to be + * encoded are in the ASCII charset, otherwise "B" encoding + * is used. + * @return Unicode string containing only US-ASCII characters + * @exception UnsupportedEncodingException if the encoding fails + */ + public static String encodeWord(String word, String charset, + String encoding) + throws UnsupportedEncodingException { + return encodeWord(word, charset, encoding, true); + } + + /* + * Encode the given string. The parameter 'encodingWord' should + * be true if a RFC 822 "word" token is being encoded and false if a + * RFC 822 "text" token is being encoded. This is because the + * "Q" encoding defined in RFC 2047 has more restrictions when + * encoding "word" tokens. (Sigh) + */ + private static String encodeWord(String string, String charset, + String encoding, boolean encodingWord) + throws UnsupportedEncodingException { + + // If 'string' contains only US-ASCII characters, just + // return it. + int ascii = checkAscii(string); + if (ascii == ALL_ASCII) + return string; + + // Else, apply the specified charset conversion. + String jcharset; + if (charset == null) { // use default charset + jcharset = getDefaultJavaCharset(); // the java charset + charset = getDefaultMIMECharset(); // the MIME equivalent + } else // MIME charset -> java charset + jcharset = javaCharset(charset); + + // If no transfer-encoding is specified, figure one out. + if (encoding == null) { + if (ascii != MOSTLY_NONASCII) + encoding = "Q"; + else + encoding = "B"; + } + + boolean b64; + if (encoding.equalsIgnoreCase("B")) + b64 = true; + else if (encoding.equalsIgnoreCase("Q")) + b64 = false; + else + throw new UnsupportedEncodingException( + "Unknown transfer encoding: " + encoding); + + StringBuilder outb = new StringBuilder(); // the output buffer + doEncode(string, b64, jcharset, + // As per RFC 2047, size of an encoded string should not + // exceed 75 bytes. + // 7 = size of "=?", '?', 'B'/'Q', '?', "?=" + 75 - 7 - charset.length(), // the available space + "=?" + charset + "?" + encoding + "?", // prefix + true, encodingWord, outb); + + return outb.toString(); + } + + private static void doEncode(String string, boolean b64, + String jcharset, int avail, String prefix, + boolean first, boolean encodingWord, StringBuilder buf) + throws UnsupportedEncodingException { + + // First find out what the length of the encoded version of + // 'string' would be. + byte[] bytes = string.getBytes(jcharset); + int len; + if (b64) // "B" encoding + len = BEncoderStream.encodedLength(bytes); + else // "Q" + len = QEncoderStream.encodedLength(bytes, encodingWord); + + int size; + if ((len > avail) && ((size = string.length()) > 1)) { + // If the length is greater than 'avail', split 'string' + // into two and recurse. + // Have to make sure not to split a Unicode surrogate pair. + int split = size / 2; + if (Character.isHighSurrogate(string.charAt(split-1))) + split--; + if (split > 0) + doEncode(string.substring(0, split), b64, jcharset, + avail, prefix, first, encodingWord, buf); + doEncode(string.substring(split, size), b64, jcharset, + avail, prefix, false, encodingWord, buf); + } else { + // length <= than 'avail'. Encode the given string + ByteArrayOutputStream os = new ByteArrayOutputStream(); + OutputStream eos; // the encoder + if (b64) // "B" encoding + eos = new BEncoderStream(os); + else // "Q" encoding + eos = new QEncoderStream(os, encodingWord); + + try { // do the encoding + eos.write(bytes); + eos.close(); + } catch (IOException ioex) { } + + byte[] encodedBytes = os.toByteArray(); // the encoded stuff + // Now write out the encoded (all ASCII) bytes into our + // StringBuilder + if (!first) // not the first line of this sequence + if (foldEncodedWords) + buf.append("\r\n "); // start a continuation line + else + buf.append(" "); // line will be folded later + + buf.append(prefix); + for (int i = 0; i < encodedBytes.length; i++) + buf.append((char)encodedBytes[i]); + buf.append("?="); // terminate the current sequence + } + } + + /** + * The string is parsed using the rules in RFC 2047 and RFC 2231 for + * parsing an "encoded-word". If the parse fails, a ParseException is + * thrown. Otherwise, it is transfer-decoded, and then + * charset-converted into Unicode. If the charset-conversion + * fails, an UnsupportedEncodingException is thrown.<p> + * + * @param eword the encoded value + * @return the decoded word + * @exception ParseException if the string is not an + * encoded-word as per RFC 2047 and RFC 2231. + * @exception UnsupportedEncodingException if the charset + * conversion failed. + */ + public static String decodeWord(String eword) + throws ParseException, UnsupportedEncodingException { + + if (!eword.startsWith("=?")) // not an encoded word + throw new ParseException( + "encoded word does not start with \"=?\": " + eword); + + // get charset + int start = 2; int pos; + if ((pos = eword.indexOf('?', start)) == -1) + throw new ParseException( + "encoded word does not include charset: " + eword); + String charset = eword.substring(start, pos); + int lpos = charset.indexOf('*'); // RFC 2231 language specified? + if (lpos >= 0) // yes, throw it away + charset = charset.substring(0, lpos); + charset = javaCharset(charset); + + // get encoding + start = pos+1; + if ((pos = eword.indexOf('?', start)) == -1) + throw new ParseException( + "encoded word does not include encoding: " + eword); + String encoding = eword.substring(start, pos); + + // get encoded-sequence + start = pos+1; + if ((pos = eword.indexOf("?=", start)) == -1) + throw new ParseException( + "encoded word does not end with \"?=\": " + eword); + /* + * XXX - should include this, but leaving it out for compatibility... + * + if (decodeStrict && pos != eword.length() - 2) + throw new ParseException( + "encoded word does not end with \"?=\": " + eword);); + */ + String word = eword.substring(start, pos); + + try { + String decodedWord; + if (word.length() > 0) { + // Extract the bytes from word + ByteArrayInputStream bis = + new ByteArrayInputStream(ASCIIUtility.getBytes(word)); + + // Get the appropriate decoder + InputStream is; + if (encoding.equalsIgnoreCase("B")) + is = new BASE64DecoderStream(bis); + else if (encoding.equalsIgnoreCase("Q")) + is = new QDecoderStream(bis); + else + throw new UnsupportedEncodingException( + "unknown encoding: " + encoding); + + // For b64 & q, size of decoded word <= size of word. So + // the decoded bytes must fit into the 'bytes' array. This + // is certainly more efficient than writing bytes into a + // ByteArrayOutputStream and then pulling out the byte[] + // from it. + int count = bis.available(); + byte[] bytes = new byte[count]; + // count is set to the actual number of decoded bytes + count = is.read(bytes, 0, count); + + // Finally, convert the decoded bytes into a String using + // the specified charset + decodedWord = count <= 0 ? "" : + new String(bytes, 0, count, charset); + } else { + // no characters to decode, return empty string + decodedWord = ""; + } + if (pos + 2 < eword.length()) { + // there's still more text in the string + String rest = eword.substring(pos + 2); + if (!decodeStrict) + rest = decodeInnerWords(rest); + decodedWord += rest; + } + return decodedWord; + } catch (UnsupportedEncodingException uex) { + // explicitly catch and rethrow this exception, otherwise + // the below IOException catch will swallow this up! + throw uex; + } catch (IOException ioex) { + // Shouldn't happen. + throw new ParseException(ioex.toString()); + } catch (IllegalArgumentException iex) { + /* An unknown charset of the form ISO-XXX-XXX, will cause + * the JDK to throw an IllegalArgumentException ... Since the + * JDK will attempt to create a classname using this string, + * but valid classnames must not contain the character '-', + * and this results in an IllegalArgumentException, rather than + * the expected UnsupportedEncodingException. Yikes + */ + throw new UnsupportedEncodingException(charset); + } + } + + /** + * Look for encoded words within a word. The MIME spec doesn't + * allow this, but many broken mailers, especially Japanese mailers, + * produce such incorrect encodings. + */ + private static String decodeInnerWords(String word) + throws UnsupportedEncodingException { + int start = 0, i; + StringBuilder buf = new StringBuilder(); + while ((i = word.indexOf("=?", start)) >= 0) { + buf.append(word.substring(start, i)); + // find first '?' after opening '=?' - end of charset + int end = word.indexOf('?', i + 2); + if (end < 0) + break; + // find next '?' after that - end of encoding + end = word.indexOf('?', end + 1); + if (end < 0) + break; + // find terminating '?=' + end = word.indexOf("?=", end + 1); + if (end < 0) + break; + String s = word.substring(i, end + 2); + try { + s = decodeWord(s); + } catch (ParseException pex) { + // ignore it, just use the original string + } + buf.append(s); + start = end + 2; + } + if (start == 0) + return word; + if (start < word.length()) + buf.append(word.substring(start)); + return buf.toString(); + } + + /** + * A utility method to quote a word, if the word contains any + * characters from the specified 'specials' list.<p> + * + * The <code>HeaderTokenizer</code> class defines two special + * sets of delimiters - MIME and RFC 822. <p> + * + * This method is typically used during the generation of + * RFC 822 and MIME header fields. + * + * @param word word to be quoted + * @param specials the set of special characters + * @return the possibly quoted word + * @see javax.mail.internet.HeaderTokenizer#MIME + * @see javax.mail.internet.HeaderTokenizer#RFC822 + */ + public static String quote(String word, String specials) { + int len = word == null ? 0 : word.length(); + if (len == 0) + return "\"\""; // an empty string is handled specially + + /* + * Look for any "bad" characters, Escape and + * quote the entire string if necessary. + */ + boolean needQuoting = false; + for (int i = 0; i < len; i++) { + char c = word.charAt(i); + if (c == '"' || c == '\\' || c == '\r' || c == '\n') { + // need to escape them and then quote the whole string + StringBuilder sb = new StringBuilder(len + 3); + sb.append('"'); + sb.append(word.substring(0, i)); + int lastc = 0; + for (int j = i; j < len; j++) { + char cc = word.charAt(j); + if ((cc == '"') || (cc == '\\') || + (cc == '\r') || (cc == '\n')) + if (cc == '\n' && lastc == '\r') + ; // do nothing, CR was already escaped + else + sb.append('\\'); // Escape the character + sb.append(cc); + lastc = cc; + } + sb.append('"'); + return sb.toString(); + } else if (c < 040 || (c >= 0177 && !allowUtf8) || + specials.indexOf(c) >= 0) + // These characters cause the string to be quoted + needQuoting = true; + } + + if (needQuoting) { + StringBuilder sb = new StringBuilder(len + 2); + sb.append('"').append(word).append('"'); + return sb.toString(); + } else + return word; + } + + /** + * Fold a string at linear whitespace so that each line is no longer + * than 76 characters, if possible. If there are more than 76 + * non-whitespace characters consecutively, the string is folded at + * the first whitespace after that sequence. The parameter + * <code>used</code> indicates how many characters have been used in + * the current line; it is usually the length of the header name. <p> + * + * Note that line breaks in the string aren't escaped; they probably + * should be. + * + * @param used characters used in line so far + * @param s the string to fold + * @return the folded string + * @since JavaMail 1.4 + */ + public static String fold(int used, String s) { + if (!foldText) + return s; + + int end; + char c; + // Strip trailing spaces and newlines + for (end = s.length() - 1; end >= 0; end--) { + c = s.charAt(end); + if (c != ' ' && c != '\t' && c != '\r' && c != '\n') + break; + } + if (end != s.length() - 1) + s = s.substring(0, end + 1); + + // if the string fits now, just return it + if (used + s.length() <= 76) + return makesafe(s); + + // have to actually fold the string + StringBuilder sb = new StringBuilder(s.length() + 4); + char lastc = 0; + while (used + s.length() > 76) { + int lastspace = -1; + for (int i = 0; i < s.length(); i++) { + if (lastspace != -1 && used + i > 76) + break; + c = s.charAt(i); + if (c == ' ' || c == '\t') + if (!(lastc == ' ' || lastc == '\t')) + lastspace = i; + lastc = c; + } + if (lastspace == -1) { + // no space, use the whole thing + sb.append(s); + s = ""; + used = 0; + break; + } + sb.append(s.substring(0, lastspace)); + sb.append("\r\n"); + lastc = s.charAt(lastspace); + sb.append(lastc); + s = s.substring(lastspace + 1); + used = 1; + } + sb.append(s); + return makesafe(sb); + } + + /** + * If the String or StringBuilder has any embedded newlines, + * make sure they're followed by whitespace, to prevent header + * injection errors. + */ + private static String makesafe(CharSequence s) { + int i; + for (i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c == '\r' || c == '\n') + break; + } + if (i == s.length()) // went through whole string with no CR or LF + return s.toString(); + + // read the lines in the string and reassemble them, + // eliminating blank lines and inserting whitespace as necessary + StringBuilder sb = new StringBuilder(s.length() + 1); + BufferedReader r = new BufferedReader(new StringReader(s.toString())); + String line; + try { + while ((line = r.readLine()) != null) { + if (line.trim().length() == 0) + continue; // ignore empty lines + if (sb.length() > 0) { + sb.append("\r\n"); + assert line.length() > 0; // proven above + char c = line.charAt(0); + if (c != ' ' && c != '\t') + sb.append(' '); + } + sb.append(line); + } + } catch (IOException ex) { + // XXX - should never happen when reading from a string + return s.toString(); + } + return sb.toString(); + } + + /** + * Unfold a folded header. Any line breaks that aren't escaped and + * are followed by whitespace are removed. + * + * @param s the string to unfold + * @return the unfolded string + * @since JavaMail 1.4 + */ + public static String unfold(String s) { + if (!foldText) + return s; + + StringBuilder sb = null; + int i; + while ((i = indexOfAny(s, "\r\n")) >= 0) { + int start = i; + int slen = s.length(); + i++; // skip CR or NL + if (i < slen && s.charAt(i - 1) == '\r' && s.charAt(i) == '\n') + i++; // skip LF + if (start > 0 && s.charAt(start - 1) == '\\') { + // there's a backslash before the line break + // strip it out, but leave in the line break + if (sb == null) + sb = new StringBuilder(s.length()); + sb.append(s.substring(0, start - 1)); + sb.append(s.substring(start, i)); + s = s.substring(i); + } else { + char c; + // if next line starts with whitespace, + // or at the end of the string, remove the line break + // XXX - next line should always start with whitespace + if (i >= slen || (c = s.charAt(i)) == ' ' || c == '\t') { + if (sb == null) + sb = new StringBuilder(s.length()); + sb.append(s.substring(0, start)); + s = s.substring(i); + } else { + // it's not a continuation line, just leave in the newline + if (sb == null) + sb = new StringBuilder(s.length()); + sb.append(s.substring(0, i)); + s = s.substring(i); + } + } + } + if (sb != null) { + sb.append(s); + return sb.toString(); + } else + return s; + } + + /** + * Return the first index of any of the characters in "any" in "s", + * or -1 if none are found. + * + * This should be a method on String. + */ + private static int indexOfAny(String s, String any) { + return indexOfAny(s, any, 0); + } + + private static int indexOfAny(String s, String any, int start) { + try { + int len = s.length(); + for (int i = start; i < len; i++) { + if (any.indexOf(s.charAt(i)) >= 0) + return i; + } + return -1; + } catch (StringIndexOutOfBoundsException e) { + return -1; + } + } + + /** + * Convert a MIME charset name into a valid Java charset name. <p> + * + * @param charset the MIME charset name + * @return the Java charset equivalent. If a suitable mapping is + * not available, the passed in charset is itself returned. + */ + public static String javaCharset(String charset) { + if (mime2java == null || charset == null) + // no mapping table, or charset parameter is null + return charset; + + String alias = mime2java.get(charset.toLowerCase(Locale.ENGLISH)); + if (alias != null) { + // verify that the mapped name is valid before trying to use it + try { + Charset.forName(alias); + } catch (Exception ex) { + alias = null; // charset alias not valid, use original name + } + } + return alias == null ? charset : alias; + } + + /** + * Convert a java charset into its MIME charset name. <p> + * + * Note that a future version of JDK (post 1.2) might provide + * this functionality, in which case, we may deprecate this + * method then. + * + * @param charset the JDK charset + * @return the MIME/IANA equivalent. If a mapping + * is not possible, the passed in charset itself + * is returned. + * @since JavaMail 1.1 + */ + public static String mimeCharset(String charset) { + if (java2mime == null || charset == null) + // no mapping table or charset param is null + return charset; + + String alias = java2mime.get(charset.toLowerCase(Locale.ENGLISH)); + return alias == null ? charset : alias; + } + + private static String defaultJavaCharset; + private static String defaultMIMECharset; + + /** + * Get the default charset corresponding to the system's current + * default locale. If the System property <code>mail.mime.charset</code> + * is set, a system charset corresponding to this MIME charset will be + * returned. <p> + * + * @return the default charset of the system's default locale, + * as a Java charset. (NOT a MIME charset) + * @since JavaMail 1.1 + */ + public static String getDefaultJavaCharset() { + if (defaultJavaCharset == null) { + /* + * If mail.mime.charset is set, it controls the default + * Java charset as well. + */ + String mimecs = null; + try { + mimecs = System.getProperty("mail.mime.charset"); + } catch (SecurityException ex) { } // ignore it + if (mimecs != null && mimecs.length() > 0) { + defaultJavaCharset = javaCharset(mimecs); + return defaultJavaCharset; + } + + try { + defaultJavaCharset = System.getProperty("file.encoding", + "8859_1"); + } catch (SecurityException sex) { + + class NullInputStream extends InputStream { + @Override + public int read() { + return 0; + } + } + InputStreamReader reader = + new InputStreamReader(new NullInputStream()); + defaultJavaCharset = reader.getEncoding(); + if (defaultJavaCharset == null) + defaultJavaCharset = "8859_1"; + } + } + + return defaultJavaCharset; + } + + /* + * Get the default MIME charset for this locale. + */ + static String getDefaultMIMECharset() { + if (defaultMIMECharset == null) { + try { + defaultMIMECharset = System.getProperty("mail.mime.charset"); + } catch (SecurityException ex) { } // ignore it + } + if (defaultMIMECharset == null) + defaultMIMECharset = mimeCharset(getDefaultJavaCharset()); + return defaultMIMECharset; + } + + // Tables to map MIME charset names to Java names and vice versa. + // XXX - Should eventually use J2SE 1.4 java.nio.charset.Charset + private static Map<String, String> mime2java; + private static Map<String, String> java2mime; + + static { + java2mime = new HashMap<>(40); + mime2java = new HashMap<>(14); + + try { + // Use this class's classloader to load the mapping file + // XXX - we should use SecuritySupport, but it's in another package + InputStream is = + javax.mail.internet.MimeUtility.class.getResourceAsStream( + "/META-INF/javamail.charset.map"); + + if (is != null) { + try { + is = new LineInputStream(is); + + // Load the JDK-to-MIME charset mapping table + loadMappings((LineInputStream)is, java2mime); + + // Load the MIME-to-JDK charset mapping table + loadMappings((LineInputStream)is, mime2java); + } finally { + try { + is.close(); + } catch (Exception cex) { + // ignore + } + } + } + } catch (Exception ex) { } + + // If we didn't load the tables, e.g., because we didn't have + // permission, load them manually. The entries here should be + // the same as the default javamail.charset.map. + if (java2mime.isEmpty()) { + java2mime.put("8859_1", "ISO-8859-1"); + java2mime.put("iso8859_1", "ISO-8859-1"); + java2mime.put("iso8859-1", "ISO-8859-1"); + + java2mime.put("8859_2", "ISO-8859-2"); + java2mime.put("iso8859_2", "ISO-8859-2"); + java2mime.put("iso8859-2", "ISO-8859-2"); + + java2mime.put("8859_3", "ISO-8859-3"); + java2mime.put("iso8859_3", "ISO-8859-3"); + java2mime.put("iso8859-3", "ISO-8859-3"); + + java2mime.put("8859_4", "ISO-8859-4"); + java2mime.put("iso8859_4", "ISO-8859-4"); + java2mime.put("iso8859-4", "ISO-8859-4"); + + java2mime.put("8859_5", "ISO-8859-5"); + java2mime.put("iso8859_5", "ISO-8859-5"); + java2mime.put("iso8859-5", "ISO-8859-5"); + + java2mime.put("8859_6", "ISO-8859-6"); + java2mime.put("iso8859_6", "ISO-8859-6"); + java2mime.put("iso8859-6", "ISO-8859-6"); + + java2mime.put("8859_7", "ISO-8859-7"); + java2mime.put("iso8859_7", "ISO-8859-7"); + java2mime.put("iso8859-7", "ISO-8859-7"); + + java2mime.put("8859_8", "ISO-8859-8"); + java2mime.put("iso8859_8", "ISO-8859-8"); + java2mime.put("iso8859-8", "ISO-8859-8"); + + java2mime.put("8859_9", "ISO-8859-9"); + java2mime.put("iso8859_9", "ISO-8859-9"); + java2mime.put("iso8859-9", "ISO-8859-9"); + + java2mime.put("sjis", "Shift_JIS"); + java2mime.put("jis", "ISO-2022-JP"); + java2mime.put("iso2022jp", "ISO-2022-JP"); + java2mime.put("euc_jp", "euc-jp"); + java2mime.put("koi8_r", "koi8-r"); + java2mime.put("euc_cn", "euc-cn"); + java2mime.put("euc_tw", "euc-tw"); + java2mime.put("euc_kr", "euc-kr"); + } + if (mime2java.isEmpty()) { + mime2java.put("iso-2022-cn", "ISO2022CN"); + mime2java.put("iso-2022-kr", "ISO2022KR"); + mime2java.put("utf-8", "UTF8"); + mime2java.put("utf8", "UTF8"); + mime2java.put("ja_jp.iso2022-7", "ISO2022JP"); + mime2java.put("ja_jp.eucjp", "EUCJIS"); + mime2java.put("euc-kr", "KSC5601"); + mime2java.put("euckr", "KSC5601"); + mime2java.put("us-ascii", "ISO-8859-1"); + mime2java.put("x-us-ascii", "ISO-8859-1"); + mime2java.put("gb2312", "GB18030"); + mime2java.put("cp936", "GB18030"); + mime2java.put("ms936", "GB18030"); + mime2java.put("gbk", "GB18030"); + } + } + + private static void loadMappings(LineInputStream is, + Map<String, String> table) { + String currLine; + + while (true) { + try { + currLine = is.readLine(); + } catch (IOException ioex) { + break; // error in reading, stop + } + + if (currLine == null) // end of file, stop + break; + if (currLine.startsWith("--") && currLine.endsWith("--")) + // end of this table + break; + + // ignore empty lines and comments + if (currLine.trim().length() == 0 || currLine.startsWith("#")) + continue; + + // A valid entry is of the form <key><separator><value> + // where, <separator> := SPACE | HT. Parse this + StringTokenizer tk = new StringTokenizer(currLine, " \t"); + try { + String key = tk.nextToken(); + String value = tk.nextToken(); + table.put(key.toLowerCase(Locale.ENGLISH), value); + } catch (NoSuchElementException nex) { } + } + } + + static final int ALL_ASCII = 1; + static final int MOSTLY_ASCII = 2; + static final int MOSTLY_NONASCII = 3; + + /** + * Check if the given string contains non US-ASCII characters. + * @param s string + * @return ALL_ASCII if all characters in the string + * belong to the US-ASCII charset. MOSTLY_ASCII + * if more than half of the available characters + * are US-ASCII characters. Else MOSTLY_NONASCII. + */ + static int checkAscii(String s) { + int ascii = 0, non_ascii = 0; + int l = s.length(); + + for (int i = 0; i < l; i++) { + if (nonascii((int)s.charAt(i))) // non-ascii + non_ascii++; + else + ascii++; + } + + if (non_ascii == 0) + return ALL_ASCII; + if (ascii > non_ascii) + return MOSTLY_ASCII; + + return MOSTLY_NONASCII; + } + + /** + * Check if the given byte array contains non US-ASCII characters. + * @param b byte array + * @return ALL_ASCII if all characters in the string + * belong to the US-ASCII charset. MOSTLY_ASCII + * if more than half of the available characters + * are US-ASCII characters. Else MOSTLY_NONASCII. + * + * XXX - this method is no longer used + */ + static int checkAscii(byte[] b) { + int ascii = 0, non_ascii = 0; + + for (int i=0; i < b.length; i++) { + // The '&' operator automatically causes b[i] to be promoted + // to an int, and we mask out the higher bytes in the int + // so that the resulting value is not a negative integer. + if (nonascii(b[i] & 0xff)) // non-ascii + non_ascii++; + else + ascii++; + } + + if (non_ascii == 0) + return ALL_ASCII; + if (ascii > non_ascii) + return MOSTLY_ASCII; + + return MOSTLY_NONASCII; + } + + /** + * Check if the given input stream contains non US-ASCII characters. + * Upto <code>max</code> bytes are checked. If <code>max</code> is + * set to <code>ALL</code>, then all the bytes available in this + * input stream are checked. If <code>breakOnNonAscii</code> is true + * the check terminates when the first non-US-ASCII character is + * found and MOSTLY_NONASCII is returned. Else, the check continues + * till <code>max</code> bytes or till the end of stream. + * + * @param is the input stream + * @param max maximum bytes to check for. The special value + * ALL indicates that all the bytes in this input + * stream must be checked. + * @param breakOnNonAscii if <code>true</code>, then terminate the + * the check when the first non-US-ASCII character + * is found. + * @return ALL_ASCII if all characters in the string + * belong to the US-ASCII charset. MOSTLY_ASCII + * if more than half of the available characters + * are US-ASCII characters. Else MOSTLY_NONASCII. + */ + static int checkAscii(InputStream is, int max, boolean breakOnNonAscii) { + int ascii = 0, non_ascii = 0; + int len; + int block = 4096; + int linelen = 0; + boolean longLine = false, badEOL = false; + boolean checkEOL = encodeEolStrict && breakOnNonAscii; + byte buf[] = null; + if (max != 0) { + block = (max == ALL) ? 4096 : Math.min(max, 4096); + buf = new byte[block]; + } + while (max != 0) { + try { + if ((len = is.read(buf, 0, block)) == -1) + break; + int lastb = 0; + for (int i = 0; i < len; i++) { + // The '&' operator automatically causes b[i] to + // be promoted to an int, and we mask out the higher + // bytes in the int so that the resulting value is + // not a negative integer. + int b = buf[i] & 0xff; + if (checkEOL && + ((lastb == '\r' && b != '\n') || + (lastb != '\r' && b == '\n'))) + badEOL = true; + if (b == '\r' || b == '\n') + linelen = 0; + else { + linelen++; + if (linelen > 998) // 1000 - CRLF + longLine = true; + } + if (nonascii(b)) { // non-ascii + if (breakOnNonAscii) // we are done + return MOSTLY_NONASCII; + else + non_ascii++; + } else + ascii++; + lastb = b; + } + } catch (IOException ioex) { + break; + } + if (max != ALL) + max -= len; + } + + if (max == 0 && breakOnNonAscii) + // We have been told to break on the first non-ascii character. + // We haven't got any non-ascii character yet, but then we + // have not checked all of the available bytes either. So we + // cannot say for sure that this input stream is ALL_ASCII, + // and hence we must play safe and return MOSTLY_NONASCII + + return MOSTLY_NONASCII; + + if (non_ascii == 0) { // no non-us-ascii characters so far + // If we're looking at non-text data, and we saw CR without LF + // or vice versa, consider this mostly non-ASCII so that it + // will be base64 encoded (since the quoted-printable encoder + // doesn't encode this case properly). + if (badEOL) + return MOSTLY_NONASCII; + // if we've seen a long line, we degrade to mostly ascii + else if (longLine) + return MOSTLY_ASCII; + else + return ALL_ASCII; + } + if (ascii > non_ascii) // mostly ascii + return MOSTLY_ASCII; + return MOSTLY_NONASCII; + } + + static final boolean nonascii(int b) { + return b >= 0177 || (b < 040 && b != '\r' && b != '\n' && b != '\t'); + } +} + +/** + * An OutputStream that determines whether the data written to + * it is all ASCII, mostly ASCII, or mostly non-ASCII. + */ +class AsciiOutputStream extends OutputStream { + private boolean breakOnNonAscii; + private int ascii = 0, non_ascii = 0; + private int linelen = 0; + private boolean longLine = false; + private boolean badEOL = false; + private boolean checkEOL = false; + private int lastb = 0; + private int ret = 0; + + public AsciiOutputStream(boolean breakOnNonAscii, boolean encodeEolStrict) { + this.breakOnNonAscii = breakOnNonAscii; + checkEOL = encodeEolStrict && breakOnNonAscii; + } + + @Override + public void write(int b) throws IOException { + check(b); + } + + @Override + public void write(byte b[]) throws IOException { + write(b, 0, b.length); + } + + @Override + public void write(byte b[], int off, int len) throws IOException { + len += off; + for (int i = off; i < len ; i++) + check(b[i]); + } + + private final void check(int b) throws IOException { + b &= 0xff; + if (checkEOL && + ((lastb == '\r' && b != '\n') || (lastb != '\r' && b == '\n'))) + badEOL = true; + if (b == '\r' || b == '\n') + linelen = 0; + else { + linelen++; + if (linelen > 998) // 1000 - CRLF + longLine = true; + } + if (MimeUtility.nonascii(b)) { // non-ascii + non_ascii++; + if (breakOnNonAscii) { // we are done + ret = MimeUtility.MOSTLY_NONASCII; + throw new EOFException(); + } + } else + ascii++; + lastb = b; + } + + /** + * Return ASCII-ness of data stream. + */ + public int getAscii() { + if (ret != 0) + return ret; + // If we're looking at non-text data, and we saw CR without LF + // or vice versa, consider this mostly non-ASCII so that it + // will be base64 encoded (since the quoted-printable encoder + // doesn't encode this case properly). + if (badEOL) + return MimeUtility.MOSTLY_NONASCII; + else if (non_ascii == 0) { // no non-us-ascii characters so far + // if we've seen a long line, we degrade to mostly ascii + if (longLine) + return MimeUtility.MOSTLY_ASCII; + else + return MimeUtility.ALL_ASCII; + } + if (ascii > non_ascii) // mostly ascii + return MimeUtility.MOSTLY_ASCII; + return MimeUtility.MOSTLY_NONASCII; + } +}
diff --git a/mail/src/main/java/javax/mail/internet/NewsAddress.java b/mail/src/main/java/javax/mail/internet/NewsAddress.java new file mode 100644 index 0000000..3ca148b --- /dev/null +++ b/mail/src/main/java/javax/mail/internet/NewsAddress.java
@@ -0,0 +1,202 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import java.util.ArrayList; +import java.util.List; +import java.util.StringTokenizer; +import java.util.Locale; +import javax.mail.*; + +/** + * This class models an RFC1036 newsgroup address. + * + * @author Bill Shannon + * @author John Mani + */ + +public class NewsAddress extends Address { + + protected String newsgroup; + protected String host; // may be null + + private static final long serialVersionUID = -4203797299824684143L; + + /** + * Default constructor. + */ + public NewsAddress() { } + + /** + * Construct a NewsAddress with the given newsgroup. + * + * @param newsgroup the newsgroup + */ + public NewsAddress(String newsgroup) { + this(newsgroup, null); + } + + /** + * Construct a NewsAddress with the given newsgroup and host. + * + * @param newsgroup the newsgroup + * @param host the host + */ + public NewsAddress(String newsgroup, String host) { + // XXX - this method should throw an exception so we can report + // illegal addresses, but for now just remove whitespace + this.newsgroup = newsgroup.replaceAll("\\s+", ""); + this.host = host; + } + + /** + * Return the type of this address. The type of a NewsAddress + * is "news". + */ + @Override + public String getType() { + return "news"; + } + + /** + * Set the newsgroup. + * + * @param newsgroup the newsgroup + */ + public void setNewsgroup(String newsgroup) { + this.newsgroup = newsgroup; + } + + /** + * Get the newsgroup. + * + * @return newsgroup + */ + public String getNewsgroup() { + return newsgroup; + } + + /** + * Set the host. + * + * @param host the host + */ + public void setHost(String host) { + this.host = host; + } + + /** + * Get the host. + * + * @return host + */ + public String getHost() { + return host; + } + + /** + * Convert this address into a RFC 1036 address. + * + * @return newsgroup + */ + @Override + public String toString() { + return newsgroup; + } + + /** + * The equality operator. + */ + @Override + public boolean equals(Object a) { + if (!(a instanceof NewsAddress)) + return false; + + NewsAddress s = (NewsAddress)a; + return ((newsgroup == null && s.newsgroup == null) || + (newsgroup != null && newsgroup.equals(s.newsgroup))) && + ((host == null && s.host == null) || + (host != null && s.host != null && host.equalsIgnoreCase(s.host))); + } + + /** + * Compute a hash code for the address. + */ + @Override + public int hashCode() { + int hash = 0; + if (newsgroup != null) + hash += newsgroup.hashCode(); + if (host != null) + hash += host.toLowerCase(Locale.ENGLISH).hashCode(); + return hash; + } + + /** + * Convert the given array of NewsAddress objects into + * a comma separated sequence of address strings. The + * resulting string contains only US-ASCII characters, and + * hence is mail-safe. + * + * @param addresses array of NewsAddress objects + * @exception ClassCastException if any address object in the + * given array is not a NewsAddress objects. Note + * that this is a RuntimeException. + * @return comma separated address strings + */ + public static String toString(Address[] addresses) { + if (addresses == null || addresses.length == 0) + return null; + + StringBuilder s = + new StringBuilder(((NewsAddress)addresses[0]).toString()); + int used = s.length(); + for (int i = 1; i < addresses.length; i++) { + s.append(","); + used++; + String ng = ((NewsAddress)addresses[i]).toString(); + if (used + ng.length() > 76) { + s.append("\r\n\t"); + used = 8; + } + s.append(ng); + used += ng.length(); + } + + return s.toString(); + } + + /** + * Parse the given comma separated sequence of newsgroups into + * NewsAddress objects. + * + * @param newsgroups comma separated newsgroup string + * @return array of NewsAddress objects + * @exception AddressException if the parse failed + */ + public static NewsAddress[] parse(String newsgroups) + throws AddressException { + // XXX - verify format of newsgroup name? + StringTokenizer st = new StringTokenizer(newsgroups, ","); + List<NewsAddress> nglist = new ArrayList<>(); + while (st.hasMoreTokens()) { + String ng = st.nextToken(); + nglist.add(new NewsAddress(ng)); + } + return nglist.toArray(new NewsAddress[nglist.size()]); + } +}
diff --git a/mail/src/main/java/javax/mail/internet/ParameterList.java b/mail/src/main/java/javax/mail/internet/ParameterList.java new file mode 100644 index 0000000..5509b39 --- /dev/null +++ b/mail/src/main/java/javax/mail/internet/ParameterList.java
@@ -0,0 +1,893 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import java.util.*; +import java.io.*; +import com.sun.mail.util.PropUtil; +import com.sun.mail.util.ASCIIUtility; + +/** + * This class holds MIME parameters (attribute-value pairs). + * The <code>mail.mime.encodeparameters</code> and + * <code>mail.mime.decodeparameters</code> System properties + * control whether encoded parameters, as specified by + * <a href="http://www.ietf.org/rfc/rfc2231.txt" target="_top">RFC 2231</a>, + * are supported. By default, such encoded parameters <b>are</b> + * supported. <p> + * + * Also, in the current implementation, setting the System property + * <code>mail.mime.decodeparameters.strict</code> to <code>"true"</code> + * will cause a <code>ParseException</code> to be thrown for errors + * detected while decoding encoded parameters. By default, if any + * decoding errors occur, the original (undecoded) string is used. <p> + * + * The current implementation supports the System property + * <code>mail.mime.parameters.strict</code>, which if set to false + * when parsing a parameter list allows parameter values + * to contain whitespace and other special characters without + * being quoted; the parameter value ends at the next semicolon. + * If set to true (the default), parameter values are required to conform + * to the MIME specification and must be quoted if they contain whitespace + * or special characters. + * + * @author John Mani + * @author Bill Shannon + */ + +public class ParameterList { + + /** + * The map of name, value pairs. + * The value object is either a String, for unencoded + * values, or a Value object, for encoded values, + * or a MultiValue object, for multi-segment parameters, + * or a LiteralValue object for strings that should not be encoded. + * + * We use a LinkedHashMap so that parameters are (as much as + * possible) kept in the original order. Note however that + * multi-segment parameters (see below) will appear in the + * position of the first seen segment and orphan segments + * will all move to the end. + */ + // keep parameters in order + private Map<String, Object> list = new LinkedHashMap<>(); + + /** + * A set of names for multi-segment parameters that we + * haven't processed yet. Normally such names are accumulated + * during the inital parse and processed at the end of the parse, + * but such names can also be set via the set method when the + * IMAP provider accumulates pre-parsed pieces of a parameter list. + * (A special call to the set method tells us when the IMAP provider + * is done setting parameters.) + * + * A multi-segment parameter is defined by RFC 2231. For example, + * "title*0=part1; title*1=part2", which represents a parameter + * named "title" with value "part1part2". + * + * Note also that each segment of the value might or might not be + * encoded, indicated by a trailing "*" on the parameter name. + * If any segment is encoded, the first segment must be encoded. + * Only the first segment contains the charset and language + * information needed to decode any encoded segments. + * + * RFC 2231 introduces many possible failure modes, which we try + * to handle as gracefully as possible. Generally, a failure to + * decode a parameter value causes the non-decoded parameter value + * to be used instead. Missing segments cause all later segments + * to be appear as independent parameters with names that include + * the segment number. For example, "title*0=part1; title*1=part2; + * title*3=part4" appears as two parameters named "title" and "title*3". + */ + private Set<String> multisegmentNames; + + /** + * A map containing the segments for all not-yet-processed + * multi-segment parameters. The map is indexed by "name*seg". + * The value object is either a String or a Value object. + * The Value object is not decoded during the initial parse + * because the segments may appear in any order and until the + * first segment appears we don't know what charset to use to + * decode the encoded segments. The segments are hex decoded + * in order, combined into a single byte array, and converted + * to a String using the specified charset in the + * combineMultisegmentNames method. + */ + private Map<String, Object> slist; + + /** + * MWB 3BView: The name of the last parameter added to the map. + * Used for the AppleMail hack. + */ + private String lastName = null; + + private static final boolean encodeParameters = + PropUtil.getBooleanSystemProperty("mail.mime.encodeparameters", true); + private static final boolean decodeParameters = + PropUtil.getBooleanSystemProperty("mail.mime.decodeparameters", true); + private static final boolean decodeParametersStrict = + PropUtil.getBooleanSystemProperty( + "mail.mime.decodeparameters.strict", false); + private static final boolean applehack = + PropUtil.getBooleanSystemProperty("mail.mime.applefilenames", false); + private static final boolean windowshack = + PropUtil.getBooleanSystemProperty("mail.mime.windowsfilenames", false); + private static final boolean parametersStrict = + PropUtil.getBooleanSystemProperty("mail.mime.parameters.strict", true); + private static final boolean splitLongParameters = + PropUtil.getBooleanSystemProperty( + "mail.mime.splitlongparameters", true); + + + /** + * A struct to hold an encoded value. + * A parsed encoded value is stored as both the + * decoded value and the original encoded value + * (so that toString will produce the same result). + * An encoded value that is set explicitly is stored + * as the original value and the encoded value, to + * ensure that get will return the same value that + * was set. + */ + private static class Value { + String value; + String charset; + String encodedValue; + } + + /** + * A struct to hold a literal value that shouldn't be further encoded. + */ + private static class LiteralValue { + String value; + } + + /** + * A struct for a multi-segment parameter. Each entry in the + * List is either a String or a Value object. When all the + * segments are present and combined in the combineMultisegmentNames + * method, the value field contains the combined and decoded value. + * Until then the value field contains an empty string as a placeholder. + */ + private static class MultiValue extends ArrayList<Object> { + // keep lint happy + private static final long serialVersionUID = 699561094618751023L; + + String value; + } + + /** + * Map the LinkedHashMap's keySet iterator to an Enumeration. + */ + private static class ParamEnum implements Enumeration<String> { + private Iterator<String> it; + + ParamEnum(Iterator<String> it) { + this.it = it; + } + + @Override + public boolean hasMoreElements() { + return it.hasNext(); + } + + @Override + public String nextElement() { + return it.next(); + } + } + + /** + * No-arg Constructor. + */ + public ParameterList() { + // initialize other collections only if they'll be needed + if (decodeParameters) { + multisegmentNames = new HashSet<>(); + slist = new HashMap<>(); + } + } + + /** + * Constructor that takes a parameter-list string. The String + * is parsed and the parameters are collected and stored internally. + * A ParseException is thrown if the parse fails. + * Note that an empty parameter-list string is valid and will be + * parsed into an empty ParameterList. + * + * @param s the parameter-list string. + * @exception ParseException if the parse fails. + */ + public ParameterList(String s) throws ParseException { + this(); + + HeaderTokenizer h = new HeaderTokenizer(s, HeaderTokenizer.MIME); + for (;;) { + HeaderTokenizer.Token tk = h.next(); + int type = tk.getType(); + String name, value; + + if (type == HeaderTokenizer.Token.EOF) // done + break; + + if ((char)type == ';') { + // expect parameter name + tk = h.next(); + // tolerate trailing semicolon, even though it violates the spec + if (tk.getType() == HeaderTokenizer.Token.EOF) + break; + // parameter name must be a MIME Atom + if (tk.getType() != HeaderTokenizer.Token.ATOM) + throw new ParseException("In parameter list <" + s + ">" + + ", expected parameter name, " + + "got \"" + tk.getValue() + "\""); + name = tk.getValue().toLowerCase(Locale.ENGLISH); + + // expect '=' + tk = h.next(); + if ((char)tk.getType() != '=') + throw new ParseException("In parameter list <" + s + ">" + + ", expected '=', " + + "got \"" + tk.getValue() + "\""); + + // expect parameter value + if (windowshack && + (name.equals("name") || name.equals("filename"))) + tk = h.next(';', true); + else if (parametersStrict) + tk = h.next(); + else + tk = h.next(';'); + type = tk.getType(); + // parameter value must be a MIME Atom or Quoted String + if (type != HeaderTokenizer.Token.ATOM && + type != HeaderTokenizer.Token.QUOTEDSTRING) + throw new ParseException("In parameter list <" + s + ">" + + ", expected parameter value, " + + "got \"" + tk.getValue() + "\""); + + value = tk.getValue(); + lastName = name; + if (decodeParameters) + putEncodedName(name, value); + else + list.put(name, value); + } else { + // MWB 3BView new code to add in filenames generated by + // AppleMail. + // Note - one space is assumed between name elements. + // This may not be correct but it shouldn't matter too much. + // Note: AppleMail encodes filenames with non-ascii characters + // correctly, so we don't need to worry about the name* subkeys. + if (type == HeaderTokenizer.Token.ATOM && lastName != null && + ((applehack && + (lastName.equals("name") || + lastName.equals("filename"))) || + !parametersStrict) + ) { + // Add value to previous value + String lastValue = (String)list.get(lastName); + value = lastValue + " " + tk.getValue(); + list.put(lastName, value); + } else { + throw new ParseException("In parameter list <" + s + ">" + + ", expected ';', got \"" + + tk.getValue() + "\""); + } + } + } + + if (decodeParameters) { + /* + * After parsing all the parameters, combine all the + * multi-segment parameter values together. + */ + combineMultisegmentNames(false); + } + } + + /** + * Normal users of this class will use simple parameter names. + * In some cases, for example, when processing IMAP protocol + * messages, individual segments of a multi-segment name + * (specified by RFC 2231) will be encountered and passed to + * the {@link #set} method. After all these segments are added + * to this ParameterList, they need to be combined to represent + * the logical parameter name and value. This method will combine + * all segments of multi-segment names. <p> + * + * Normal users should never need to call this method. + * + * @since JavaMail 1.5 + */ + public void combineSegments() { + /* + * If we've accumulated any multi-segment names from calls to + * the set method from (e.g.) the IMAP provider, combine the pieces. + * Ignore any parse errors (e.g., from decoding the values) + * because it's too late to report them. + */ + if (decodeParameters && multisegmentNames.size() > 0) { + try { + combineMultisegmentNames(true); + } catch (ParseException pex) { + // too late to do anything about it + } + } + } + + /** + * If the name is an encoded or multi-segment name (or both) + * handle it appropriately, storing the appropriate String + * or Value object. Multi-segment names are stored in the + * main parameter list as an emtpy string as a placeholder, + * replaced later in combineMultisegmentNames with a MultiValue + * object. This causes all pieces of the multi-segment parameter + * to appear in the position of the first seen segment of the + * parameter. + */ + private void putEncodedName(String name, String value) + throws ParseException { + int star = name.indexOf('*'); + if (star < 0) { + // single parameter, unencoded value + list.put(name, value); + } else if (star == name.length() - 1) { + // single parameter, encoded value + name = name.substring(0, star); + Value v = extractCharset(value); + try { + v.value = decodeBytes(v.value, v.charset); + } catch (UnsupportedEncodingException ex) { + if (decodeParametersStrict) + throw new ParseException(ex.toString()); + } + list.put(name, v); + } else { + // multiple segments + String rname = name.substring(0, star); + multisegmentNames.add(rname); + list.put(rname, ""); + + Object v; + if (name.endsWith("*")) { + // encoded value + if (name.endsWith("*0*")) { // first segment + v = extractCharset(value); + } else { + v = new Value(); + ((Value)v).encodedValue = value; + ((Value)v).value = value; // default; decoded later + } + name = name.substring(0, name.length() - 1); + } else { + // unencoded value + v = value; + } + slist.put(name, v); + } + } + + /** + * Iterate through the saved set of names of multi-segment parameters, + * for each parameter find all segments stored in the slist map, + * decode each segment as needed, combine the segments together into + * a single decoded value, and save all segments in a MultiValue object + * in the main list indexed by the parameter name. + */ + private void combineMultisegmentNames(boolean keepConsistentOnFailure) + throws ParseException { + boolean success = false; + try { + Iterator<String> it = multisegmentNames.iterator(); + while (it.hasNext()) { + String name = it.next(); + MultiValue mv = new MultiValue(); + /* + * Now find all the segments for this name and + * decode each segment as needed. + */ + String charset = null; + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + int segment; + for (segment = 0; ; segment++) { + String sname = name + "*" + segment; + Object v = slist.get(sname); + if (v == null) // out of segments + break; + mv.add(v); + try { + if (v instanceof Value) { + Value vv = (Value)v; + if (segment == 0) { + // the first segment specifies the charset + // for all other encoded segments + charset = vv.charset; + } else { + if (charset == null) { + // should never happen + multisegmentNames.remove(name); + break; + } + } + decodeBytes(vv.value, bos); + } else { + bos.write(ASCIIUtility.getBytes((String)v)); + } + } catch (IOException ex) { + // XXX - should never happen + } + slist.remove(sname); + } + if (segment == 0) { + // didn't find any segments at all + list.remove(name); + } else { + try { + if (charset != null) + charset = MimeUtility.javaCharset(charset); + if (charset == null || charset.length() == 0) + charset = MimeUtility.getDefaultJavaCharset(); + if (charset != null) + mv.value = bos.toString(charset); + else + mv.value = bos.toString(); + } catch (UnsupportedEncodingException uex) { + if (decodeParametersStrict) + throw new ParseException(uex.toString()); + // convert as if iso-8859-1 + try { + mv.value = bos.toString("iso-8859-1"); + } catch (UnsupportedEncodingException ex) { + // should never happen + } + } + list.put(name, mv); + } + } + success = true; + } finally { + /* + * If we get here because of an exception that's going to + * be thrown (success == false) from the constructor + * (keepConsistentOnFailure == false), this is all wasted effort. + */ + if (keepConsistentOnFailure || success) { + // we should never end up with anything in slist, + // but if we do, add it all to list + if (slist.size() > 0) { + // first, decode any values that we'll add to the list + Iterator<Object> sit = slist.values().iterator(); + while (sit.hasNext()) { + Object v = sit.next(); + if (v instanceof Value) { + Value vv = (Value)v; + try { + vv.value = + decodeBytes(vv.value, vv.charset); + } catch (UnsupportedEncodingException ex) { + if (decodeParametersStrict) + throw new ParseException(ex.toString()); + } + } + } + list.putAll(slist); + } + + // clear out the set of names and segments + multisegmentNames.clear(); + slist.clear(); + } + } + } + + /** + * Return the number of parameters in this list. + * + * @return number of parameters. + */ + public int size() { + return list.size(); + } + + /** + * Returns the value of the specified parameter. Note that + * parameter names are case-insensitive. + * + * @param name parameter name. + * @return Value of the parameter. Returns + * <code>null</code> if the parameter is not + * present. + */ + public String get(String name) { + String value; + Object v = list.get(name.trim().toLowerCase(Locale.ENGLISH)); + if (v instanceof MultiValue) + value = ((MultiValue)v).value; + else if (v instanceof LiteralValue) + value = ((LiteralValue)v).value; + else if (v instanceof Value) + value = ((Value)v).value; + else + value = (String)v; + return value; + } + + /** + * Set a parameter. If this parameter already exists, it is + * replaced by this new value. + * + * @param name name of the parameter. + * @param value value of the parameter. + */ + public void set(String name, String value) { + name = name.trim().toLowerCase(Locale.ENGLISH); + if (decodeParameters) { + try { + putEncodedName(name, value); + } catch (ParseException pex) { + // ignore it + list.put(name, value); + } + } else + list.put(name, value); + } + + /** + * Set a parameter. If this parameter already exists, it is + * replaced by this new value. If the + * <code>mail.mime.encodeparameters</code> System property + * is true, and the parameter value is non-ASCII, it will be + * encoded with the specified charset, as specified by RFC 2231. + * + * @param name name of the parameter. + * @param value value of the parameter. + * @param charset charset of the parameter value. + * @since JavaMail 1.4 + */ + public void set(String name, String value, String charset) { + if (encodeParameters) { + Value ev = encodeValue(value, charset); + // was it actually encoded? + if (ev != null) + list.put(name.trim().toLowerCase(Locale.ENGLISH), ev); + else + set(name, value); + } else + set(name, value); + } + + /** + * Package-private method to set a literal value that won't be + * further encoded. Used to set the filename parameter when + * "mail.mime.encodefilename" is true. + * + * @param name name of the parameter. + * @param value value of the parameter. + */ + void setLiteral(String name, String value) { + LiteralValue lv = new LiteralValue(); + lv.value = value; + list.put(name, lv); + } + + /** + * Removes the specified parameter from this ParameterList. + * This method does nothing if the parameter is not present. + * + * @param name name of the parameter. + */ + public void remove(String name) { + list.remove(name.trim().toLowerCase(Locale.ENGLISH)); + } + + /** + * Return an enumeration of the names of all parameters in this + * list. + * + * @return Enumeration of all parameter names in this list. + */ + public Enumeration<String> getNames() { + return new ParamEnum(list.keySet().iterator()); + } + + /** + * Convert this ParameterList into a MIME String. If this is + * an empty list, an empty string is returned. + * + * @return String + */ + @Override + public String toString() { + return toString(0); + } + + /** + * Convert this ParameterList into a MIME String. If this is + * an empty list, an empty string is returned. + * + * The 'used' parameter specifies the number of character positions + * already taken up in the field into which the resulting parameter + * list is to be inserted. It's used to determine where to fold the + * resulting parameter list. + * + * @param used number of character positions already used, in + * the field into which the parameter list is to + * be inserted. + * @return String + */ + public String toString(int used) { + ToStringBuffer sb = new ToStringBuffer(used); + Iterator<Map.Entry<String, Object>> e = list.entrySet().iterator(); + + while (e.hasNext()) { + Map.Entry<String, Object> ent = e.next(); + String name = ent.getKey(); + String value; + Object v = ent.getValue(); + if (v instanceof MultiValue) { + MultiValue vv = (MultiValue)v; + name += "*"; + for (int i = 0; i < vv.size(); i++) { + Object va = vv.get(i); + String ns; + if (va instanceof Value) { + ns = name + i + "*"; + value = ((Value)va).encodedValue; + } else { + ns = name + i; + value = (String)va; + } + sb.addNV(ns, quote(value)); + } + } else if (v instanceof LiteralValue) { + value = ((LiteralValue)v).value; + sb.addNV(name, quote(value)); + } else if (v instanceof Value) { + /* + * XXX - We could split the encoded value into multiple + * segments if it's too long, but that's more difficult. + */ + name += "*"; + value = ((Value)v).encodedValue; + sb.addNV(name, quote(value)); + } else { + value = (String)v; + /* + * If this value is "long", split it into a multi-segment + * parameter. Only do this if we've enabled RFC2231 style + * encoded parameters. + * + * Note that we check the length before quoting the value. + * Quoting might make the string longer, although typically + * not much, so we allow a little slop in the calculation. + * In the worst case, a 60 character string will turn into + * 122 characters when quoted, which is long but not + * outrageous. + */ + if (value.length() > 60 && + splitLongParameters && encodeParameters) { + int seg = 0; + name += "*"; + while (value.length() > 60) { + sb.addNV(name + seg, quote(value.substring(0, 60))); + value = value.substring(60); + seg++; + } + if (value.length() > 0) + sb.addNV(name + seg, quote(value)); + } else { + sb.addNV(name, quote(value)); + } + } + } + return sb.toString(); + } + + /** + * A special wrapper for a StringBuffer that keeps track of the + * number of characters used in a line, wrapping to a new line + * as necessary; for use by the toString method. + */ + private static class ToStringBuffer { + private int used; // keep track of how much used on current line + private StringBuilder sb = new StringBuilder(); + + public ToStringBuffer(int used) { + this.used = used; + } + + public void addNV(String name, String value) { + sb.append("; "); + used += 2; + int len = name.length() + value.length() + 1; + if (used + len > 76) { // overflows ... + sb.append("\r\n\t"); // .. start new continuation line + used = 8; // account for the starting <tab> char + } + sb.append(name).append('='); + used += name.length() + 1; + if (used + value.length() > 76) { // still overflows ... + // have to fold value + String s = MimeUtility.fold(used, value); + sb.append(s); + int lastlf = s.lastIndexOf('\n'); + if (lastlf >= 0) // always true + used += s.length() - lastlf - 1; + else + used += s.length(); + } else { + sb.append(value); + used += value.length(); + } + } + + @Override + public String toString() { + return sb.toString(); + } + } + + // Quote a parameter value token if required. + private static String quote(String value) { + return MimeUtility.quote(value, HeaderTokenizer.MIME); + } + + private static final char hex[] = { + '0','1', '2', '3', '4', '5', '6', '7', + '8','9', 'A', 'B', 'C', 'D', 'E', 'F' + }; + + /** + * Encode a parameter value, if necessary. + * If the value is encoded, a Value object is returned. + * Otherwise, null is returned. + * XXX - Could return a MultiValue object if parameter value is too long. + */ + private static Value encodeValue(String value, String charset) { + if (MimeUtility.checkAscii(value) == MimeUtility.ALL_ASCII) + return null; // no need to encode it + + byte[] b; // charset encoded bytes from the string + try { + b = value.getBytes(MimeUtility.javaCharset(charset)); + } catch (UnsupportedEncodingException ex) { + return null; + } + StringBuffer sb = new StringBuffer(b.length + charset.length() + 2); + sb.append(charset).append("''"); + for (int i = 0; i < b.length; i++) { + char c = (char)(b[i] & 0xff); + // do we need to encode this character? + if (c <= ' ' || c >= 0x7f || c == '*' || c == '\'' || c == '%' || + HeaderTokenizer.MIME.indexOf(c) >= 0) { + sb.append('%').append(hex[c>>4]).append(hex[c&0xf]); + } else + sb.append(c); + } + Value v = new Value(); + v.charset = charset; + v.value = value; + v.encodedValue = sb.toString(); + return v; + } + + /** + * Extract charset and encoded value. + * Value will be decoded later. + */ + private static Value extractCharset(String value) throws ParseException { + Value v = new Value(); + v.value = v.encodedValue = value; + try { + int i = value.indexOf('\''); + if (i < 0) { + if (decodeParametersStrict) + throw new ParseException( + "Missing charset in encoded value: " + value); + return v; // not encoded correctly? return as is. + } + String charset = value.substring(0, i); + int li = value.indexOf('\'', i + 1); + if (li < 0) { + if (decodeParametersStrict) + throw new ParseException( + "Missing language in encoded value: " + value); + return v; // not encoded correctly? return as is. + } + // String lang = value.substring(i + 1, li); + v.value = value.substring(li + 1); + v.charset = charset; + } catch (NumberFormatException nex) { + if (decodeParametersStrict) + throw new ParseException(nex.toString()); + } catch (StringIndexOutOfBoundsException ex) { + if (decodeParametersStrict) + throw new ParseException(ex.toString()); + } + return v; + } + + /** + * Decode the encoded bytes in value using the specified charset. + */ + private static String decodeBytes(String value, String charset) + throws ParseException, UnsupportedEncodingException { + /* + * Decode the ASCII characters in value + * into an array of bytes, and then convert + * the bytes to a String using the specified + * charset. We'll never need more bytes than + * encoded characters, so use that to size the + * array. + */ + byte[] b = new byte[value.length()]; + int i, bi; + for (i = 0, bi = 0; i < value.length(); i++) { + char c = value.charAt(i); + if (c == '%') { + try { + String hex = value.substring(i + 1, i + 3); + c = (char)Integer.parseInt(hex, 16); + i += 2; + } catch (NumberFormatException ex) { + if (decodeParametersStrict) + throw new ParseException(ex.toString()); + } catch (StringIndexOutOfBoundsException ex) { + if (decodeParametersStrict) + throw new ParseException(ex.toString()); + } + } + b[bi++] = (byte)c; + } + if (charset != null) + charset = MimeUtility.javaCharset(charset); + if (charset == null || charset.length() == 0) + charset = MimeUtility.getDefaultJavaCharset(); + return new String(b, 0, bi, charset); + } + + /** + * Decode the encoded bytes in value and write them to the OutputStream. + */ + private static void decodeBytes(String value, OutputStream os) + throws ParseException, IOException { + /* + * Decode the ASCII characters in value + * and write them to the stream. + */ + int i; + for (i = 0; i < value.length(); i++) { + char c = value.charAt(i); + if (c == '%') { + try { + String hex = value.substring(i + 1, i + 3); + c = (char)Integer.parseInt(hex, 16); + i += 2; + } catch (NumberFormatException ex) { + if (decodeParametersStrict) + throw new ParseException(ex.toString()); + } catch (StringIndexOutOfBoundsException ex) { + if (decodeParametersStrict) + throw new ParseException(ex.toString()); + } + } + os.write((byte)c); + } + } +}
diff --git a/mail/src/main/java/javax/mail/internet/ParseException.java b/mail/src/main/java/javax/mail/internet/ParseException.java new file mode 100644 index 0000000..8497be8 --- /dev/null +++ b/mail/src/main/java/javax/mail/internet/ParseException.java
@@ -0,0 +1,46 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import javax.mail.MessagingException; + +/** + * The exception thrown due to an error in parsing RFC822 + * or MIME headers, including multipart bodies. + * + * @author John Mani + */ + +public class ParseException extends MessagingException { + + private static final long serialVersionUID = 7649991205183658089L; + + /** + * Constructs a ParseException with no detail message. + */ + public ParseException() { + super(); + } + + /** + * Constructs a ParseException with the specified detail message. + * @param s the detail message + */ + public ParseException(String s) { + super(s); + } +}
diff --git a/mail/src/main/java/javax/mail/internet/PreencodedMimeBodyPart.java b/mail/src/main/java/javax/mail/internet/PreencodedMimeBodyPart.java new file mode 100644 index 0000000..3188eca --- /dev/null +++ b/mail/src/main/java/javax/mail/internet/PreencodedMimeBodyPart.java
@@ -0,0 +1,105 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import java.io.*; +import java.util.Enumeration; +import javax.mail.*; + +import com.sun.mail.util.LineOutputStream; + +/** + * A MimeBodyPart that handles data that has already been encoded. + * This class is useful when constructing a message and attaching + * data that has already been encoded (for example, using base64 + * encoding). The data may have been encoded by the application, + * or may have been stored in a file or database in encoded form. + * The encoding is supplied when this object is created. The data + * is attached to this object in the usual fashion, by using the + * <code>setText</code>, <code>setContent</code>, or + * <code>setDataHandler</code> methods. + * + * @since JavaMail 1.4 + */ + +public class PreencodedMimeBodyPart extends MimeBodyPart { + private String encoding; + + /** + * Create a PreencodedMimeBodyPart that assumes the data is + * encoded using the specified encoding. The encoding must + * be a MIME supported Content-Transfer-Encoding. + * + * @param encoding the Content-Transfer-Encoding + */ + public PreencodedMimeBodyPart(String encoding) { + this.encoding = encoding; + } + + /** + * Returns the content transfer encoding specified when + * this object was created. + */ + @Override + public String getEncoding() throws MessagingException { + return encoding; + } + + /** + * Output the body part as an RFC 822 format stream. + * + * @exception IOException if an error occurs writing to the + * stream or if an error is generated + * by the javax.activation layer. + * @exception MessagingException for other failures + * @see javax.activation.DataHandler#writeTo + */ + @Override + public void writeTo(OutputStream os) + throws IOException, MessagingException { + + // see if we already have a LOS + LineOutputStream los = null; + if (os instanceof LineOutputStream) { + los = (LineOutputStream) os; + } else { + los = new LineOutputStream(os); + } + + // First, write out the header + Enumeration<String> hdrLines = getAllHeaderLines(); + while (hdrLines.hasMoreElements()) + los.writeln(hdrLines.nextElement()); + + // The CRLF separator between header and content + los.writeln(); + + // Finally, the content, already encoded. + getDataHandler().writeTo(os); + os.flush(); + } + + /** + * Force the <code>Content-Transfer-Encoding</code> header to use + * the encoding that was specified when this object was created. + */ + @Override + protected void updateHeaders() throws MessagingException { + super.updateHeaders(); + MimeBodyPart.setEncoding(this, encoding); + } +}
diff --git a/mail/src/main/java/javax/mail/internet/SharedInputStream.java b/mail/src/main/java/javax/mail/internet/SharedInputStream.java new file mode 100644 index 0000000..b5cd419 --- /dev/null +++ b/mail/src/main/java/javax/mail/internet/SharedInputStream.java
@@ -0,0 +1,60 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import java.io.*; + +/** + * An InputStream that is backed by data that can be shared by multiple + * readers may implement this interface. This allows users of such an + * InputStream to determine the current position in the InputStream, and + * to create new InputStreams representing a subset of the data in the + * original InputStream. The new InputStream will access the same + * underlying data as the original, without copying the data. <p> + * + * Note that implementations of this interface must ensure that the + * <code>close</code> method does not close any underlying stream + * that might be shared by multiple instances of <code>SharedInputStream</code> + * until all shared instances have been closed. + * + * @author Bill Shannon + * @since JavaMail 1.2 + */ + +public interface SharedInputStream { + /** + * Return the current position in the InputStream, as an + * offset from the beginning of the InputStream. + * + * @return the current position + */ + public long getPosition(); + + /** + * Return a new InputStream representing a subset of the data + * from this InputStream, starting at <code>start</code> (inclusive) + * up to <code>end</code> (exclusive). <code>start</code> must be + * non-negative. If <code>end</code> is -1, the new stream ends + * at the same place as this stream. The returned InputStream + * will also implement the SharedInputStream interface. + * + * @param start the starting position + * @param end the ending position + 1 + * @return the new stream + */ + public InputStream newStream(long start, long end); +}
diff --git a/mail/src/main/java/javax/mail/internet/UniqueValue.java b/mail/src/main/java/javax/mail/internet/UniqueValue.java new file mode 100644 index 0000000..a3e9f8c --- /dev/null +++ b/mail/src/main/java/javax/mail/internet/UniqueValue.java
@@ -0,0 +1,95 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import java.net.*; +import java.util.concurrent.atomic.AtomicInteger; +import javax.mail.Session; + +/** + * This is a utility class that generates unique values. The generated + * String contains only US-ASCII characters and hence is safe for use + * in RFC822 headers. <p> + * + * This is a package private class. + * + * @author John Mani + * @author Max Spivak + * @author Bill Shannon + */ + +class UniqueValue { + /** + * A global unique number, to ensure uniqueness of generated strings. + */ + private static AtomicInteger id = new AtomicInteger(); + + /** + * Get a unique value for use in a multipart boundary string. + * + * This implementation generates it by concatenating a global + * part number, a newly created object's <code>hashCode()</code>, + * and the current time (in milliseconds). + */ + public static String getUniqueBoundaryValue() { + StringBuilder s = new StringBuilder(); + long hash = s.hashCode(); + + // Unique string is ----=_Part_<part>_<hashcode>.<currentTime> + s.append("----=_Part_").append(id.getAndIncrement()).append("_"). + append(hash).append('.'). + append(System.currentTimeMillis()); + return s.toString(); + } + + /** + * Get a unique value for use in a Message-ID. + * + * This implementation generates it by concatenating a newly + * created object's <code>hashCode()</code>, a global ID + * (incremented on every use), the current time (in milliseconds), + * and the host name from this user's local address generated by + * <code>InternetAddress.getLocalAddress()</code>. + * (The host name defaults to "localhost" if + * <code>getLocalAddress()</code> returns null.) + * + * @param ssn Session object used to get the local address + * @see javax.mail.internet.InternetAddress + */ + public static String getUniqueMessageIDValue(Session ssn) { + String suffix = null; + + InternetAddress addr = InternetAddress.getLocalAddress(ssn); + if (addr != null) + suffix = addr.getAddress(); + else { + suffix = "javamailuser@localhost"; // worst-case default + } + int at = suffix.lastIndexOf('@'); + if (at >= 0) + suffix = suffix.substring(at); + + StringBuilder s = new StringBuilder(); + + // Unique string is <hashcode>.<id>.<currentTime><suffix> + s.append(s.hashCode()).append('.'). + append(id.getAndIncrement()).append('.'). + append(System.currentTimeMillis()). + append(suffix); + return s.toString(); + } +}
diff --git a/mail/src/main/java/javax/mail/internet/package.html b/mail/src/main/java/javax/mail/internet/package.html new file mode 100644 index 0000000..4ecf4af --- /dev/null +++ b/mail/src/main/java/javax/mail/internet/package.html
@@ -0,0 +1,548 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> +<HTML> +<HEAD> +<meta http-equiv="Content-Type" content="text/html; charset=utf-8"> +<!-- + + Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<TITLE>javax.mail.internet package</TITLE> +</HEAD> +<BODY BGCOLOR="white"> + +<P> +Classes specific to Internet mail systems. +This package supports features that are specific to Internet mail systems +based on the MIME standard +(<A HREF="http://www.ietf.org/rfc/rfc2045.txt" TARGET="_top">RFC 2045</A>, +<A HREF="http://www.ietf.org/rfc/rfc2045.txt" TARGET="_top">RFC 2046</A>, and +<A HREF="http://www.ietf.org/rfc/rfc2045.txt" TARGET="_top">RFC 2047</A>). +The IMAP, SMTP, and POP3 protocols use +{@link javax.mail.internet.MimeMessage MimeMessages}. +</P> +<A NAME="properties"><STRONG>Properties</STRONG></A> +<P> +The JavaMail API supports the following standard properties, +which may be set in the <code>Session</code> object, or in the +<code>Properties</code> object used to create the <code>Session</code> object. +The properties are always set as strings; the Type column describes +how the string is interpreted. For example, use +</P> +<PRE> + session.setProperty("mail.mime.address.strict", "false"); +</PRE> +<P> +to set the <CODE>mail.mime.address.strict</CODE> property, +which is of type boolean. +</P> +<TABLE BORDER SUMMARY="JavaMail properties"> +<TR> +<TH>Name</TH> +<TH>Type</TH> +<TH>Description</TH> +</TR> + +<TR> +<TD><A NAME="mail.mime.address.strict">mail.mime.address.strict</A></TD> +<TD>boolean</TD> +<TD> +The <code>mail.mime.address.strict</code> session property controls +the parsing of address headers. By default, strict parsing of address +headers is done. If this property is set to <code>"false"</code>, +strict parsing is not done and many illegal addresses that sometimes +occur in real messages are allowed. See the <code>InternetAddress</code> +class for details. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.mime.allowutf8">mail.mime.allowutf8</A></TD> +<TD>boolean</TD> +<TD> +If set to <code>"true"</code>, UTF-8 strings are allowed in message headers, +e.g., in addresses. This should <b>only</b> be set if the mail server also +supports UTF-8. +</TD> +</TR> +</TABLE> +<P> +The JavaMail API specification requires support for the following properties, +which must be set in the <code>System</code> properties. +The properties are always set as strings; the Type column describes +how the string is interpreted. For example, use +</P> +<PRE> + System.setProperty("mail.mime.decodetext.strict", "false"); +</PRE> +<P> +to set the <CODE>mail.mime.decodetext.strict</CODE> property, +which is of type boolean. +</P> +<TABLE BORDER SUMMARY="JavaMail System properties"> +<TR> +<TH>Name</TH> +<TH>Type</TH> +<TH>Description</TH> +</TR> + +<TR> +<TD><A NAME="mail.mime.charset">mail.mime.charset</A></TD> +<TD>String</TD> +<TD> +The <code>mail.mime.charset</code> System property can +be used to specify the default MIME charset to use for encoded words +and text parts that don't otherwise specify a charset. Normally, the +default MIME charset is derived from the default Java charset, as +specified in the <code>file.encoding</code> System property. Most +applications will have no need to explicitly set the default MIME +charset. In cases where the default MIME charset to be used for +mail messages is different than the charset used for files stored on +the system, this property should be set. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.mime.decodetext.strict">mail.mime.decodetext.strict</A></TD> +<TD>boolean</TD> +<TD> +The <code>mail.mime.decodetext.strict</code> property controls +decoding of MIME encoded words. The MIME spec requires that encoded +words start at the beginning of a whitespace separated word. Some +mailers incorrectly include encoded words in the middle of a word. +If the <code>mail.mime.decodetext.strict</code> System property is +set to <code>"false"</code>, an attempt will be made to decode these +illegal encoded words. The default is true. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.mime.encodeeol.strict">mail.mime.encodeeol.strict</A></TD> +<TD>boolean</TD> +<TD> +The <code>mail.mime.encodeeol.strict</code> property controls the +choice of Content-Transfer-Encoding for MIME parts that are not of +type "text". Often such parts will contain textual data for which +an encoding that allows normal end of line conventions is appropriate. +In rare cases, such a part will appear to contain entirely textual +data, but will require an encoding that preserves CR and LF characters +without change. If the <code>mail.mime.encodeeol.strict</code> +System property is set to <code>"true"</code>, such an encoding will +be used when necessary. The default is false. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.mime.decodefilename">mail.mime.decodefilename</A></TD> +<TD>boolean</TD> +<TD> +If set to <code>"true"</code>, the <code>getFileName</code> method +uses the <code>MimeUtility</code> +method <code>decodeText</code> to decode any +non-ASCII characters in the filename. Note that this decoding +violates the MIME specification, but is useful for interoperating +with some mail clients that use this convention. +The default is false. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.mime.encodefilename">mail.mime.encodefilename</A></TD> +<TD>boolean</TD> +<TD> +If set to <code>"true"</code>, the <code>setFileName</code> method +uses the <code>MimeUtility</code> +method <code>encodeText</code> to encode any +non-ASCII characters in the filename. Note that this encoding +violates the MIME specification, but is useful for interoperating +with some mail clients that use this convention. +The default is false. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.mime.decodeparameters">mail.mime.decodeparameters</A></TD> +<TD>boolean</TD> +<TD> +If set to <code>"false"</code>, non-ASCII parameters in a +<code>ParameterList</code>, e.g., in a Content-Type header, +will <b>not</b> be decoded as specified by +<A HREF="http://www.ietf.org/rfc/rfc2231.txt" TARGET="_top">RFC 2231</A>. +The default is true. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.mime.encodeparameters">mail.mime.encodeparameters</A></TD> +<TD>boolean</TD> +<TD> +If set to <code>"false"</code>, non-ASCII parameters in a +<code>ParameterList</code>, e.g., in a Content-Type header, +will <b>not</b> be encoded as specified by +<A HREF="http://www.ietf.org/rfc/rfc2231.txt" TARGET="_top">RFC 2231</A>. +The default is true. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.mime.multipart.ignoremissingendboundary">mail.mime.multipart. ignoremissingendboundary</A></TD> +<TD>boolean</TD> +<TD> +Normally, when parsing a multipart MIME message, a message that is +missing the final end boundary line is not considered an error. +The data simply ends at the end of the input. Note that messages +of this form violate the MIME specification. If the property +<code>mail.mime.multipart.ignoremissingendboundary</code> is set +to <code>false</code>, such messages are considered an error and a +<code>MesagingException</code> will be thrown when parsing such a +message. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.mime.multipart.ignoremissingboundaryparameter">mail.mime.multipart. ignoremissingboundaryparameter</A></TD> +<TD>boolean</TD> +<TD> +If the Content-Type header for a multipart content does not have +a <code>boundary</code> parameter, the multipart parsing code +will look for the first line in the content that looks like a +boundary line and extract the boundary parameter from the line. +If this property is set to <code>"false"</code>, a +<code>MessagingException</code> will be thrown if the Content-Type +header doesn't specify a boundary parameter. +The default is true. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.mime.multipart.ignoreexistingboundaryparameter">mail.mime.multipart. ignoreexistingboundaryparameter</A></TD> +<TD>boolean</TD> +<TD> +Normally the boundary parameter in the Content-Type header of a multipart +body part is used to specify the separator between parts of the multipart +body. This System property may be set to <code>"true"</code> to cause +the parser to look for a line in the multipart body that looks like a +boundary line and use that value as the separator between subsequent parts. +This may be useful in cases where a broken anti-virus product has rewritten +the message incorrectly such that the boundary parameter and the actual +boundary value no longer match. +The default value of this property is false. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.mime.multipart.allowempty">mail.mime.multipart. allowempty</A></TD> +<TD>boolean</TD> +<TD> +Normally, when writing out a MimeMultipart that contains no body +parts, or when trying to parse a multipart message with no body parts, +a <code>MessagingException</code> is thrown. The MIME spec does not allow +multipart content with no body parts. This +System property may be set to <code>"true"</code> to override this behavior. +When writing out such a MimeMultipart, a single empty part will be +included. When reading such a multipart, a MimeMultipart will be created +with no body parts. +The default value of this property is false. +</TD> +</TR> + +</TABLE> + + +<P> +The following properties are supported by the reference implementation (RI) of +JavaMail, but are not currently a required part of the specification. +These must be set as <CODE>Session</CODE> properties. +The names, types, defaults, and semantics of these properties may +change in future releases. +</P> +<TABLE BORDER SUMMARY="JavaMail RI properties"> +<TR> +<TH>Name</TH> +<TH>Type</TH> +<TH>Description</TH> +</TR> + +<TR> +<TD><A NAME="mail.alternates">mail.alternates</A></TD> +<TD>String</TD> +<TD> +A string containing other email addresses that the current user is known by. +The <code>MimeMessage</code> <code>reply</code> method will eliminate any +of these addresses from the recipient list in the message it constructs, +to avoid sending the reply back to the sender. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.replyallcc">mail.replyallcc</A></TD> +<TD>boolean</TD> +<TD> +If set to <code>"true"</code>, the <code>MimeMessage</code> +<code>reply</code> method will put all recipients except the original +sender in the <code>Cc</code> list of the newly constructed message. +Normally, recipients in the <code>To</code> header of the original +message will also appear in the <code>To</code> list of the newly +constructed message. +</TD> +</TR> +</TABLE> + +<P> +The following properties are supported by the reference implementation (RI) of +JavaMail, but are not currently a required part of the specification. +These must be set as <CODE>System</CODE> properties. +The names, types, defaults, and semantics of these properties may +change in future releases. +</P> +<TABLE BORDER SUMMARY="JavaMail RI System properties"> +<TR> +<TH>Name</TH> +<TH>Type</TH> +<TH>Description</TH> +</TR> + +<TR> +<TD><A NAME="mail.mime.base64.ignoreerrors">mail.mime.base64.ignoreerrors</A></TD> +<TD>boolean</TD> +<TD> +If set to <code>"true"</code>, the BASE64 decoder will ignore errors +in the encoded data, returning EOF. This may be useful when dealing +with improperly encoded messages that contain extraneous data at the +end of the encoded stream. Note however that errors anywhere in the +stream will cause the decoder to stop decoding so this should be used +with extreme caution. The default is false. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.mime.foldtext">mail.mime.foldtext</A></TD> +<TD>boolean</TD> +<TD> +If set to <code>"true"</code>, header fields containing just text +such as the <code>Subject</code> and <code>Content-Description</code> +header fields, and long parameter values in structured headers such +as <code>Content-Type</code> will be folded (broken into 76 character lines) +when set and unfolded when read. The default is true. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.mime.setcontenttypefilename">mail.mime.setcontenttypefilename</A></TD> +<TD>boolean</TD> +<TD> +If set to <code>"true"</code>, the <code>setFileName</code> method +will also set the <code>name</code> parameter on the <code>Content-Type</code> +header to the specified filename. This supports interoperability with +some old mail clients. The default is true. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.mime.setdefaulttextcharset">mail.mime.setdefaulttextcharset</A></TD> +<TD>boolean</TD> +<TD> +When updating the headers of a message, a body +part with a <code>text</code> content type but no <code>charset</code> +parameter will have a <code>charset</code> parameter added to it +if this property is set to <code>"true"</code>. +The default is true. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.mime.parameters.strict">mail.mime.parameters.strict</A></TD> +<TD>boolean</TD> +<TD> +If set to false, when reading a message, parameter values in header fields +such as <code>Content-Type</code> and <code>Content-Disposition</code> +are allowed to contain whitespace and other special characters without +being quoted; the parameter value ends at the next semicolon. +If set to true (the default), parameter values are required to conform +to the MIME specification and must be quoted if they contain whitespace +or special characters. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.mime.applefilenames">mail.mime.applefilenames</A></TD> +<TD>boolean</TD> +<TD> +Apple Mail incorrectly encodes filenames that contain spaces, +forgetting to quote the parameter value. If this property is +set to <code>"true"</code>, JavaMail will try to detect this +situation when parsing parameters and work around it. +The default is false. +Note that this property handles a subset of the cases handled +by setting the mail.mime.parameters.strict property to false. +This property will likely be removed in a future release. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.mime.windowsfilenames">mail.mime.windowsfilenames</A></TD> +<TD>boolean</TD> +<TD> +Internet Explorer 6 incorrectly includes a complete pathname +in the filename parameter of the Content-Disposition header +for uploaded files, and fails to properly escape the backslashes +in the pathname. If this property is +set to <code>"true"</code>, JavaMail will preserve all backslashes +in the "filename" and "name" parameters of any MIME header. +The default is false. +Note that this is a violation of the MIME specification but may +be useful when using JavaMail to parse HTTP messages for uploaded +files sent by IE6. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.mime.ignoreunknownencoding">mail.mime. ignoreunknownencoding</A></TD> +<TD>boolean</TD> +<TD> +If set to <code>"true"</code>, an unknown value in the +<code>Content-Transfer-Encoding</code> header will be ignored +when reading a message and an encoding of "8bit" will be assumed. +If set to <code>"false"</code>, an exception is thrown for an +unknown encoding value. The default is false. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.mime.uudecode.ignoreerrors">mail.mime.uudecode. ignoreerrors</A></TD> +<TD>boolean</TD> +<TD> +If set to <code>"true"</code>, errors in the encoded format of a +uuencoded document will be ignored when reading a message part. +If set to <code>"false"</code>, an exception is thrown for an +incorrectly encoded message part. The default is false. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.mime.uudecode.ignoremissingbeginend">mail.mime.uudecode. ignoremissingbeginend</A></TD> +<TD>boolean</TD> +<TD> +If set to <code>"true"</code>, a missing "being" or "end" line in a +uuencoded document will be ignored when reading a message part. +If set to <code>"false"</code>, an exception is thrown for a +uuencoded message part without the required "begin" and "end" lines. +The default is false. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.mime.ignorewhitespacelines">mail.mime. ignorewhitespacelines</A></TD> +<TD>boolean</TD> +<TD> +Normally the header of a MIME part is separated from the body by an empty +line. This System property may be set to <code>"true"</code> to cause +the parser to consider a line containing only whitespace to be an empty +line. The default value of this property is false. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.mime.ignoremultipartencoding">mail.mime. ignoremultipartencoding</A></TD> +<TD>boolean</TD> +<TD> +The MIME spec does not allow body parts of type multipart/* to be encoded. +The Content-Transfer-Encoding header is ignored in this case. +Setting this System property to <code>"false"</code> will +cause the Content-Transfer-Encoding header to be honored for multipart +content. +The default value of this property is true. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.mime.allowencodedmessages">mail.mime.allowencodedmessages</A></TD> +<TD>boolean</TD> +<TD> +The MIME spec does not allow body parts of type message/* to be encoded. +The Content-Transfer-Encoding header is ignored in this case. +Some versions of Microsoft Outlook will incorrectly encode message +attachments. Setting this System property to <code>"true"</code> will +cause the Content-Transfer-Encoding header to be honored for message +attachments. +The default value of this property is false. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.mime.contenttypehandler">mail.mime.contenttypehandler</A></TD> +<TD>String</TD> +<TD> +In some cases JavaMail is unable to process messages with an invalid +Content-Type header. The header may have incorrect syntax or other +problems. This property specifies the name of a class that will be +used to clean up the Content-Type header value before JavaMail uses it. +The class must have a method with this signature: +<CODE>public static String cleanContentType(MimePart mp, String contentType)</CODE> +Whenever JavaMail accesses the Content-Type header of a message, it +will pass the value to this method and use the returned value instead. +The value may be null if the Content-Type header isn't present. +Returning null will cause the default Content-Type to be used. +The MimePart may be used to access other headers of the message part +to determine how to correct the Content-Type. +Note that the Content-Type handler doesn't affect the +<CODE>getHeader</CODE> method, which still returns the raw header value. +Note also that the handler doesn't affect the IMAP provider; the IMAP +server is responsible for returning pre-parsed, syntactically correct +Content-Type information. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.mime.address.usecanonicalhostname">mail.mime.address.usecanonicalhostname</A></TD> +<TD>boolean</TD> +<TD> +Use the +{@link java.net.InetAddress#getCanonicalHostName InetAddress.getCanonicalHostName} +method to determine the host name in the +{@link javax.mail.internet.InternetAddress#getLocalAddress InternetAddress.getLocalAddress} +method. +With some network configurations, InetAddress.getCanonicalHostName may be +slow or may return an address instead of a host name. +In that case, setting this System property to false will cause the +{@link java.net.InetAddress#getHostName InetAddress.getHostName} +method to be used instead. +The default is true. +</TD> +</TR> + +</TABLE> +<P> +The current +implementation of classes in this package log debugging information using +{@link java.util.logging.Logger} as described in the following table: +</P> +<TABLE BORDER SUMMARY="JavaMail Loggers"> +<TR> +<TH>Logger Name</TH> +<TH>Logging Level</TH> +<TH>Purpose</TH> +</TR> + +<TR> +<TD>javax.mail.internet</TD> +<TD>FINE</TD> +<TD>General debugging output</TD> +</TR> +</TABLE> + +</BODY> +</HTML>
diff --git a/mail/src/main/java/javax/mail/package.html b/mail/src/main/java/javax/mail/package.html new file mode 100644 index 0000000..411d023 --- /dev/null +++ b/mail/src/main/java/javax/mail/package.html
@@ -0,0 +1,347 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> +<HTML> +<HEAD> +<meta http-equiv="Content-Type" content="text/html; charset=utf-8"> +<!-- + + Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<TITLE>javax.mail package</TITLE> +</HEAD> +<BODY BGCOLOR="white"> + +<P> +The JavaMail™ API +provides classes that model a mail system. +The <code>javax.mail</code> package defines classes that are common to +all mail systems. +The <code>javax.mail.internet</code> package defines classes that are specific +to mail systems based on internet standards such as MIME, SMTP, POP3, and IMAP. +The JavaMail API includes the <code>javax.mail</code> package and subpackages. +</P> +<P> +For an overview of the JavaMail API, read the +<A HREF="https://javaee.github.io/javamail/docs/JavaMail-1.6.pdf" TARGET="_top"> +JavaMail specification</A>. +</P> +<P> +The code to send a plain text message can be as simple as the following: +</P> +<PRE> + Properties props = new Properties(); + props.put("mail.smtp.host", "my-mail-server"); + Session session = Session.getInstance(props, null); + + try { + MimeMessage msg = new MimeMessage(session); + msg.setFrom("me@example.com"); + msg.setRecipients(Message.RecipientType.TO, + "you@example.com"); + msg.setSubject("JavaMail hello world example"); + msg.setSentDate(new Date()); + msg.setText("Hello, world!\n"); + Transport.send(msg, "me@example.com", "my-password"); + } catch (MessagingException mex) { + System.out.println("send failed, exception: " + mex); + } +</PRE> +<P> +The JavaMail download bundle contains many more complete examples +in the "demo" directory. +</P> +<P> +Don't forget to see the +<A HREF="https://javaee.github.io/javamail/FAQ.html" TARGET="_top"> +JavaMail API FAQ</A> +for answers to the most common questions. +The <A HREF="https://javaee.github.io/javamail/" TARGET="_top"> +JavaMail web site</A> +contains many additional resources. +</P> +<A NAME="properties"><STRONG>Properties</STRONG></A> +<P> +The JavaMail API supports the following standard properties, +which may be set in the <code>Session</code> object, or in the +<code>Properties</code> object used to create the <code>Session</code> object. +The properties are always set as strings; the Type column describes +how the string is interpreted. For example, use +</P> +<PRE> + props.put("mail.debug", "true"); +</PRE> +<P> +to set the <code>mail.debug</code> property, which is of type boolean. +</P> +<TABLE BORDER SUMMARY="JavaMail properties"> +<TR> +<TH>Name</TH> +<TH>Type</TH> +<TH>Description</TH> +</TR> + +<TR> +<TD><A NAME="mail.debug">mail.debug</A></TD> +<TD>boolean</TD> +<TD> +The initial debug mode. +Default is false. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.from">mail.from</A></TD> +<TD>String</TD> +<TD> +The return email address of the current user, used by the +<code>InternetAddress</code> method <code>getLocalAddress</code>. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.mime.address.strict">mail.mime.address.strict</A></TD> +<TD>boolean</TD> +<TD> +The MimeMessage class uses the <code>InternetAddress</code> method +<code>parseHeader</code> to parse headers in messages. This property +controls the strict flag passed to the <code>parseHeader</code> +method. The default is true. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.host">mail.host</A></TD> +<TD>String</TD> +<TD> +The default host name of the mail server for both Stores and Transports. +Used if the <code>mail.<i>protocol</i>.host</code> property isn't set. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.store.protocol">mail.store.protocol</A></TD> +<TD>String</TD> +<TD> +Specifies the default message access protocol. The +<code>Session</code> method <code>getStore()</code> returns a Store +object that implements this protocol. By default the first Store +provider in the configuration files is returned. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.transport.protocol">mail.transport.protocol</A></TD> +<TD>String</TD> +<TD> +Specifies the default message transport protocol. The +<code>Session</code> method <code>getTransport()</code> returns a Transport +object that implements this protocol. By default the first Transport +provider in the configuration files is returned. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.user">mail.user</A></TD> +<TD>String</TD> +<TD> +The default user name to use when connecting to the mail server. +Used if the <code>mail.<i>protocol</i>.user</code> property isn't set. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.protocol.class">mail.<i>protocol</i>.class</A></TD> +<TD>String</TD> +<TD> +Specifies the fully qualified class name of the provider for the +specified protocol. Used in cases where more than one provider +for a given protocol exists; this property can be used to specify +which provider to use by default. The provider must still be listed +in a configuration file. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.protocol.host">mail.<i>protocol</i>.host</A></TD> +<TD>String</TD> +<TD> +The host name of the mail server for the specified protocol. +Overrides the <code>mail.host</code> property. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.protocol.port">mail.<i>protocol</i>.port</A></TD> +<TD>int</TD> +<TD> +The port number of the mail server for the specified protocol. +If not specified the protocol's default port number is used. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.protocol.user">mail.<i>protocol</i>.user</A></TD> +<TD>String</TD> +<TD> +The user name to use when connecting to mail servers +using the specified protocol. +Overrides the <code>mail.user</code> property. +</TD> +</TR> + +</TABLE> + +<P> +The following properties are supported by the reference implementation (RI) of +JavaMail, but are not currently a required part of the specification. +The names, types, defaults, and semantics of these properties may +change in future releases. +</P> +<TABLE BORDER SUMMARY="JavaMail RI properties"> +<TR> +<TH>Name</TH> +<TH>Type</TH> +<TH>Description</TH> +</TR> + +<TR> +<TD><A NAME="mail.debug.auth">mail.debug.auth</A></TD> +<TD>boolean</TD> +<TD> +Include protocol authentication commands (including usernames and passwords) +in the debug output. +Default is false. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.debug.auth.username">mail.debug.auth.username</A></TD> +<TD>boolean</TD> +<TD> +Include the user name in non-protocol debug output. +Default is true. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.debug.auth.password">mail.debug.auth.password</A></TD> +<TD>boolean</TD> +<TD> +Include the password in non-protocol debug output. +Default is false. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.transport.protocol.address-type">mail.transport.protocol.<i>address-type</i></A></TD> +<TD>String</TD> +<TD> +Specifies the default message transport protocol for the specified address type. +The <code>Session</code> method <code>getTransport(Address)</code> returns a +Transport object that implements this protocol when the address is of the +specified type (e.g., "rfc822" for standard internet addresses). +By default the first Transport configured for that address type is used. +This property can be used to override the behavior of the +{@link javax.mail.Transport#send send} method of the +{@link javax.mail.Transport Transport} class so that (for example) the "smtps" +protocol is used instead of the "smtp" protocol by setting the property +<code>mail.transport.protocol.rfc822</code> to <code>"smtps"</code>. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.event.scope">mail.event.scope</A></TD> +<TD>String</TD> +<TD> +Controls the scope of events. (See the javax.mail.event package.) +By default, a separate event queue and thread is used for events for each +Store, Transport, or Folder. +If this property is set to "session", all such events are put in a single +event queue processed by a single thread for the current session. +If this property is set to "application", all such events are put in a single +event queue processed by a single thread for the current application. +(Applications are distinguished by their context class loader.) +</TD> +</TR> + +<TR> +<TD><A NAME="mail.event.executor">mail.event.executor</A></TD> +<TD>java.util.concurrent.Executor</TD> +<TD> +By default, a new Thread is created for each event queue. +This thread is used to call the listeners for these events. +If this property is set to an instance of an Executor, the +Executor.execute method is used to run the event dispatcher +for an event queue. The event dispatcher runs until the +event queue is no longer in use. +</TD> +</TR> + +</TABLE> + +<P> +The JavaMail API also supports several System properties; +see the {@link javax.mail.internet} package documentation +for details. +</P> +<P> +The JavaMail reference +implementation includes protocol providers in subpackages of +<code>com.sun.mail</code>. Note that the APIs to these protocol +providers are not part of the standard JavaMail API. Portable +programs will not use these APIs. +</P> +<P> +Nonportable programs may use the APIs of the protocol providers +by (for example) casting a returned <code>Folder</code> object to a +<code>com.sun.mail.imap.IMAPFolder</code> object. Similarly for +<code>Store</code> and <code>Message</code> objects returned from the +standard JavaMail APIs. +</P> +<P> +The protocol providers also support properties that are specific to +those providers. The package documentation for the +{@link com.sun.mail.imap IMAP}, {@link com.sun.mail.pop3 POP3}, +and {@link com.sun.mail.smtp SMTP} packages provide details. +</P> +<P> +In addition to printing debugging output as controlled by the +{@link javax.mail.Session Session} configuration, the current +implementation of classes in this package log the same information using +{@link java.util.logging.Logger} as described in the following table: +</P> +<TABLE BORDER SUMMARY="JavaMail Loggers"> +<TR> +<TH>Logger Name</TH> +<TH>Logging Level</TH> +<TH>Purpose</TH> +</TR> + +<TR> +<TD>javax.mail</TD> +<TD>CONFIG</TD> +<TD>Configuration of the Session</TD> +</TR> + +<TR> +<TD>javax.mail</TD> +<TD>FINE</TD> +<TD>General debugging output</TD> +</TR> +</TABLE> + +</BODY> +</HTML>
diff --git a/mail/src/main/java/javax/mail/search/AddressStringTerm.java b/mail/src/main/java/javax/mail/search/AddressStringTerm.java new file mode 100644 index 0000000..1e48e72 --- /dev/null +++ b/mail/src/main/java/javax/mail/search/AddressStringTerm.java
@@ -0,0 +1,80 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.search; + +import javax.mail.Message; +import javax.mail.Address; +import javax.mail.internet.InternetAddress; + +/** + * This abstract class implements string comparisons for Message + * addresses. <p> + * + * Note that this class differs from the <code>AddressTerm</code> class + * in that this class does comparisons on address strings rather than + * Address objects. + * + * @since JavaMail 1.1 + */ + +public abstract class AddressStringTerm extends StringTerm { + + private static final long serialVersionUID = 3086821234204980368L; + + /** + * Constructor. + * + * @param pattern the address pattern to be compared. + */ + protected AddressStringTerm(String pattern) { + super(pattern, true); // we need case-insensitive comparison. + } + + /** + * Check whether the address pattern specified in the constructor is + * a substring of the string representation of the given Address + * object. <p> + * + * Note that if the string representation of the given Address object + * contains charset or transfer encodings, the encodings must be + * accounted for, during the match process. <p> + * + * @param a The comparison is applied to this Address object. + * @return true if the match succeeds, otherwise false. + */ + protected boolean match(Address a) { + if (a instanceof InternetAddress) { + InternetAddress ia = (InternetAddress)a; + // We dont use toString() to get "a"'s String representation, + // because InternetAddress.toString() returns a RFC 2047 + // encoded string, which isn't what we need here. + + return super.match(ia.toUnicodeString()); + } else + return super.match(a.toString()); + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof AddressStringTerm)) + return false; + return super.equals(obj); + } +}
diff --git a/mail/src/main/java/javax/mail/search/AddressTerm.java b/mail/src/main/java/javax/mail/search/AddressTerm.java new file mode 100644 index 0000000..748ad33 --- /dev/null +++ b/mail/src/main/java/javax/mail/search/AddressTerm.java
@@ -0,0 +1,79 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.search; + +import javax.mail.Address; + +/** + * This class implements Message Address comparisons. + * + * @author Bill Shannon + * @author John Mani + */ + +public abstract class AddressTerm extends SearchTerm { + /** + * The address. + * + * @serial + */ + protected Address address; + + private static final long serialVersionUID = 2005405551929769980L; + + protected AddressTerm(Address address) { + this.address = address; + } + + /** + * Return the address to match with. + * + * @return the adddress + */ + public Address getAddress() { + return address; + } + + /** + * Match against the argument Address. + * + * @param a the address to match + * @return true if it matches + */ + protected boolean match(Address a) { + return (a.equals(address)); + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof AddressTerm)) + return false; + AddressTerm at = (AddressTerm)obj; + return at.address.equals(this.address); + } + + /** + * Compute a hashCode for this object. + */ + @Override + public int hashCode() { + return address.hashCode(); + } +}
diff --git a/mail/src/main/java/javax/mail/search/AndTerm.java b/mail/src/main/java/javax/mail/search/AndTerm.java new file mode 100644 index 0000000..d0b935d --- /dev/null +++ b/mail/src/main/java/javax/mail/search/AndTerm.java
@@ -0,0 +1,116 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.search; + +import javax.mail.Message; + +/** + * This class implements the logical AND operator on individual + * SearchTerms. + * + * @author Bill Shannon + * @author John Mani + */ +public final class AndTerm extends SearchTerm { + + /** + * The array of terms on which the AND operator should be + * applied. + * + * @serial + */ + private SearchTerm[] terms; + + private static final long serialVersionUID = -3583274505380989582L; + + /** + * Constructor that takes two terms. + * + * @param t1 first term + * @param t2 second term + */ + public AndTerm(SearchTerm t1, SearchTerm t2) { + terms = new SearchTerm[2]; + terms[0] = t1; + terms[1] = t2; + } + + /** + * Constructor that takes an array of SearchTerms. + * + * @param t array of terms + */ + public AndTerm(SearchTerm[] t) { + terms = new SearchTerm[t.length]; // clone the array + for (int i = 0; i < t.length; i++) + terms[i] = t[i]; + } + + /** + * Return the search terms. + * + * @return the search terms + */ + public SearchTerm[] getTerms() { + return terms.clone(); + } + + /** + * The AND operation. <p> + * + * The terms specified in the constructor are applied to + * the given object and the AND operator is applied to their results. + * + * @param msg The specified SearchTerms are applied to this Message + * and the AND operator is applied to their results. + * @return true if the AND succeds, otherwise false + */ + @Override + public boolean match(Message msg) { + for (int i=0; i < terms.length; i++) + if (!terms[i].match(msg)) + return false; + return true; + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof AndTerm)) + return false; + AndTerm at = (AndTerm)obj; + if (at.terms.length != terms.length) + return false; + for (int i=0; i < terms.length; i++) + if (!terms[i].equals(at.terms[i])) + return false; + return true; + } + + /** + * Compute a hashCode for this object. + */ + @Override + public int hashCode() { + int hash = 0; + for (int i=0; i < terms.length; i++) + hash += terms[i].hashCode(); + return hash; + } +}
diff --git a/mail/src/main/java/javax/mail/search/BodyTerm.java b/mail/src/main/java/javax/mail/search/BodyTerm.java new file mode 100644 index 0000000..fa67893 --- /dev/null +++ b/mail/src/main/java/javax/mail/search/BodyTerm.java
@@ -0,0 +1,103 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.search; + +import java.io.IOException; +import javax.mail.*; + +/** + * This class implements searches on a message body. + * All parts of the message that are of MIME type "text/*" are searched. + * The pattern is a simple string that must appear as a substring in + * the message body. + * + * @author Bill Shannon + * @author John Mani + */ +public final class BodyTerm extends StringTerm { + + private static final long serialVersionUID = -4888862527916911385L; + + /** + * Constructor + * @param pattern The String to search for + */ + public BodyTerm(String pattern) { + // Note: comparison is case-insensitive + super(pattern); + } + + /** + * The match method. + * + * @param msg The pattern search is applied on this Message's body + * @return true if the pattern is found; otherwise false + */ + @Override + public boolean match(Message msg) { + return matchPart(msg); + } + + /** + * Search all the parts of the message for any text part + * that matches the pattern. + */ + private boolean matchPart(Part p) { + try { + /* + * Using isMimeType to determine the content type avoids + * fetching the actual content data until we need it. + */ + if (p.isMimeType("text/*")) { + String s = (String)p.getContent(); + if (s == null) + return false; + /* + * We invoke our superclass' (i.e., StringTerm) match method. + * Note however that StringTerm.match() is not optimized + * for substring searches in large string buffers. We really + * need to have a StringTerm subclass, say BigStringTerm, + * with its own match() method that uses a better algorithm .. + * and then subclass BodyTerm from BigStringTerm. + */ + return super.match(s); + } else if (p.isMimeType("multipart/*")) { + Multipart mp = (Multipart)p.getContent(); + int count = mp.getCount(); + for (int i = 0; i < count; i++) + if (matchPart(mp.getBodyPart(i))) + return true; + } else if (p.isMimeType("message/rfc822")) { + return matchPart((Part)p.getContent()); + } + } catch (MessagingException ex) { + } catch (IOException ex) { + } catch (RuntimeException ex) { + } + return false; + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof BodyTerm)) + return false; + return super.equals(obj); + } +}
diff --git a/mail/src/main/java/javax/mail/search/ComparisonTerm.java b/mail/src/main/java/javax/mail/search/ComparisonTerm.java new file mode 100644 index 0000000..57330ec --- /dev/null +++ b/mail/src/main/java/javax/mail/search/ComparisonTerm.java
@@ -0,0 +1,61 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.search; + +/** + * This class models the comparison operator. This is an abstract + * class; subclasses implement comparisons for different datatypes. + * + * @author Bill Shannon + * @author John Mani + */ +public abstract class ComparisonTerm extends SearchTerm { + public static final int LE = 1; + public static final int LT = 2; + public static final int EQ = 3; + public static final int NE = 4; + public static final int GT = 5; + public static final int GE = 6; + + /** + * The comparison. + * + * @serial + */ + protected int comparison; + + private static final long serialVersionUID = 1456646953666474308L; + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof ComparisonTerm)) + return false; + ComparisonTerm ct = (ComparisonTerm)obj; + return ct.comparison == this.comparison; + } + + /** + * Compute a hashCode for this object. + */ + @Override + public int hashCode() { + return comparison; + } +}
diff --git a/mail/src/main/java/javax/mail/search/DateTerm.java b/mail/src/main/java/javax/mail/search/DateTerm.java new file mode 100644 index 0000000..833ad90 --- /dev/null +++ b/mail/src/main/java/javax/mail/search/DateTerm.java
@@ -0,0 +1,108 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.search; + +import java.util.Date; + +/** + * This class implements comparisons for Dates + * + * @author Bill Shannon + * @author John Mani + */ +public abstract class DateTerm extends ComparisonTerm { + /** + * The date. + * + * @serial + */ + protected Date date; + + private static final long serialVersionUID = 4818873430063720043L; + + /** + * Constructor. + * @param comparison the comparison type + * @param date The Date to be compared against + */ + protected DateTerm(int comparison, Date date) { + this.comparison = comparison; + this.date = date; + } + + /** + * Return the Date to compare with. + * + * @return the date + */ + public Date getDate() { + return new Date(date.getTime()); + } + + /** + * Return the type of comparison. + * + * @return the comparison type + */ + public int getComparison() { + return comparison; + } + + /** + * The date comparison method. + * + * @param d the date in the constructor is compared with this date + * @return true if the dates match, otherwise false + */ + protected boolean match(Date d) { + switch (comparison) { + case LE: + return d.before(date) || d.equals(date); + case LT: + return d.before(date); + case EQ: + return d.equals(date); + case NE: + return !d.equals(date); + case GT: + return d.after(date); + case GE: + return d.after(date) || d.equals(date); + default: + return false; + } + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof DateTerm)) + return false; + DateTerm dt = (DateTerm)obj; + return dt.date.equals(this.date) && super.equals(obj); + } + + /** + * Compute a hashCode for this object. + */ + @Override + public int hashCode() { + return date.hashCode() + super.hashCode(); + } +}
diff --git a/mail/src/main/java/javax/mail/search/FlagTerm.java b/mail/src/main/java/javax/mail/search/FlagTerm.java new file mode 100644 index 0000000..20897c6 --- /dev/null +++ b/mail/src/main/java/javax/mail/search/FlagTerm.java
@@ -0,0 +1,144 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.search; + +import javax.mail.*; + +/** + * This class implements comparisons for Message Flags. + * + * @author Bill Shannon + * @author John Mani + */ +public final class FlagTerm extends SearchTerm { + + /** + * Indicates whether to test for the presence or + * absence of the specified Flag. If <code>true</code>, + * then test whether all the specified flags are present, else + * test whether all the specified flags are absent. + * + * @serial + */ + private boolean set; + + /** + * Flags object containing the flags to test. + * + * @serial + */ + private Flags flags; + + private static final long serialVersionUID = -142991500302030647L; + + /** + * Constructor. + * + * @param flags Flags object containing the flags to check for + * @param set the flag setting to check for + */ + public FlagTerm(Flags flags, boolean set) { + this.flags = flags; + this.set = set; + } + + /** + * Return the Flags to test. + * + * @return the flags + */ + public Flags getFlags() { + return (Flags)flags.clone(); + } + + /** + * Return true if testing whether the flags are set. + * + * @return true if testing whether the flags are set + */ + public boolean getTestSet() { + return set; + } + + /** + * The comparison method. + * + * @param msg The flag comparison is applied to this Message + * @return true if the comparson succeeds, otherwise false. + */ + @Override + public boolean match(Message msg) { + + try { + Flags f = msg.getFlags(); + if (set) { // This is easy + if (f.contains(flags)) + return true; + else + return false; + } + + // Return true if ALL flags in the passed in Flags + // object are NOT set in this Message. + + // Got to do this the hard way ... + Flags.Flag[] sf = flags.getSystemFlags(); + + // Check each flag in the passed in Flags object + for (int i = 0; i < sf.length; i++) { + if (f.contains(sf[i])) + // this flag IS set in this Message, get out. + return false; + } + + String[] s = flags.getUserFlags(); + + // Check each flag in the passed in Flags object + for (int i = 0; i < s.length; i++) { + if (f.contains(s[i])) + // this flag IS set in this Message, get out. + return false; + } + + return true; + + } catch (MessagingException e) { + return false; + } catch (RuntimeException e) { + return false; + } + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof FlagTerm)) + return false; + FlagTerm ft = (FlagTerm)obj; + return ft.set == this.set && ft.flags.equals(this.flags); + } + + /** + * Compute a hashCode for this object. + */ + @Override + public int hashCode() { + return set ? flags.hashCode() : ~flags.hashCode(); + } +}
diff --git a/mail/src/main/java/javax/mail/search/FromStringTerm.java b/mail/src/main/java/javax/mail/search/FromStringTerm.java new file mode 100644 index 0000000..69ab926 --- /dev/null +++ b/mail/src/main/java/javax/mail/search/FromStringTerm.java
@@ -0,0 +1,82 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.search; + +import javax.mail.Message; +import javax.mail.Address; + +/** + * This class implements string comparisons for the From Address + * header. <p> + * + * Note that this class differs from the <code>FromTerm</code> class + * in that this class does comparisons on address strings rather than Address + * objects. The string comparisons are case-insensitive. + * + * @since JavaMail 1.1 + */ + +public final class FromStringTerm extends AddressStringTerm { + + private static final long serialVersionUID = 5801127523826772788L; + + /** + * Constructor. + * + * @param pattern the address pattern to be compared. + */ + public FromStringTerm(String pattern) { + super(pattern); + } + + /** + * Check whether the address string specified in the constructor is + * a substring of the From address of this Message. + * + * @param msg The comparison is applied to this Message's From + * address. + * @return true if the match succeeds, otherwise false. + */ + @Override + public boolean match(Message msg) { + Address[] from; + + try { + from = msg.getFrom(); + } catch (Exception e) { + return false; + } + + if (from == null) + return false; + + for (int i=0; i < from.length; i++) + if (super.match(from[i])) + return true; + return false; + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof FromStringTerm)) + return false; + return super.equals(obj); + } +}
diff --git a/mail/src/main/java/javax/mail/search/FromTerm.java b/mail/src/main/java/javax/mail/search/FromTerm.java new file mode 100644 index 0000000..6320a97 --- /dev/null +++ b/mail/src/main/java/javax/mail/search/FromTerm.java
@@ -0,0 +1,74 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.search; + +import javax.mail.Message; +import javax.mail.Address; + +/** + * This class implements comparisons for the From Address header. + * + * @author Bill Shannon + * @author John Mani + */ +public final class FromTerm extends AddressTerm { + + private static final long serialVersionUID = 5214730291502658665L; + + /** + * Constructor + * @param address The Address to be compared + */ + public FromTerm(Address address) { + super(address); + } + + /** + * The address comparator. + * + * @param msg The address comparison is applied to this Message + * @return true if the comparison succeeds, otherwise false + */ + @Override + public boolean match(Message msg) { + Address[] from; + + try { + from = msg.getFrom(); + } catch (Exception e) { + return false; + } + + if (from == null) + return false; + + for (int i=0; i < from.length; i++) + if (super.match(from[i])) + return true; + return false; + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof FromTerm)) + return false; + return super.equals(obj); + } +}
diff --git a/mail/src/main/java/javax/mail/search/HeaderTerm.java b/mail/src/main/java/javax/mail/search/HeaderTerm.java new file mode 100644 index 0000000..72b82de --- /dev/null +++ b/mail/src/main/java/javax/mail/search/HeaderTerm.java
@@ -0,0 +1,105 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.search; + +import java.util.Locale; +import javax.mail.Message; + +/** + * This class implements comparisons for Message headers. + * The comparison is case-insensitive. + * + * @author Bill Shannon + * @author John Mani + */ +public final class HeaderTerm extends StringTerm { + /** + * The name of the header. + * + * @serial + */ + private String headerName; + + private static final long serialVersionUID = 8342514650333389122L; + + /** + * Constructor. + * + * @param headerName The name of the header + * @param pattern The pattern to search for + */ + public HeaderTerm(String headerName, String pattern) { + super(pattern); + this.headerName = headerName; + } + + /** + * Return the name of the header to compare with. + * + * @return the name of the header + */ + public String getHeaderName() { + return headerName; + } + + /** + * The header match method. + * + * @param msg The match is applied to this Message's header + * @return true if the match succeeds, otherwise false + */ + @Override + public boolean match(Message msg) { + String[] headers; + + try { + headers = msg.getHeader(headerName); + } catch (Exception e) { + return false; + } + + if (headers == null) + return false; + + for (int i=0; i < headers.length; i++) + if (super.match(headers[i])) + return true; + return false; + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof HeaderTerm)) + return false; + HeaderTerm ht = (HeaderTerm)obj; + // XXX - depends on header comparisons being case independent + return ht.headerName.equalsIgnoreCase(headerName) && super.equals(ht); + } + + /** + * Compute a hashCode for this object. + */ + @Override + public int hashCode() { + // XXX - depends on header comparisons being case independent + return headerName.toLowerCase(Locale.ENGLISH).hashCode() + + super.hashCode(); + } +}
diff --git a/mail/src/main/java/javax/mail/search/IntegerComparisonTerm.java b/mail/src/main/java/javax/mail/search/IntegerComparisonTerm.java new file mode 100644 index 0000000..f890051 --- /dev/null +++ b/mail/src/main/java/javax/mail/search/IntegerComparisonTerm.java
@@ -0,0 +1,95 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.search; + +/** + * This class implements comparisons for integers. + * + * @author Bill Shannon + * @author John Mani + */ +public abstract class IntegerComparisonTerm extends ComparisonTerm { + /** + * The number. + * + * @serial + */ + protected int number; + + private static final long serialVersionUID = -6963571240154302484L; + + protected IntegerComparisonTerm(int comparison, int number) { + this.comparison = comparison; + this.number = number; + } + + /** + * Return the number to compare with. + * + * @return the number + */ + public int getNumber() { + return number; + } + + /** + * Return the type of comparison. + * + * @return the comparison type + */ + public int getComparison() { + return comparison; + } + + protected boolean match(int i) { + switch (comparison) { + case LE: + return i <= number; + case LT: + return i < number; + case EQ: + return i == number; + case NE: + return i != number; + case GT: + return i > number; + case GE: + return i >= number; + default: + return false; + } + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof IntegerComparisonTerm)) + return false; + IntegerComparisonTerm ict = (IntegerComparisonTerm)obj; + return ict.number == this.number && super.equals(obj); + } + + /** + * Compute a hashCode for this object. + */ + @Override + public int hashCode() { + return number + super.hashCode(); + } +}
diff --git a/mail/src/main/java/javax/mail/search/MessageIDTerm.java b/mail/src/main/java/javax/mail/search/MessageIDTerm.java new file mode 100644 index 0000000..6762c04 --- /dev/null +++ b/mail/src/main/java/javax/mail/search/MessageIDTerm.java
@@ -0,0 +1,81 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.search; + +import javax.mail.Message; + +/** + * This term models the RFC822 "MessageId" - a message-id for + * Internet messages that is supposed to be unique per message. + * Clients can use this term to search a folder for a message given + * its MessageId. <p> + * + * The MessageId is represented as a String. + * + * @author Bill Shannon + * @author John Mani + */ +public final class MessageIDTerm extends StringTerm { + + private static final long serialVersionUID = -2121096296454691963L; + + /** + * Constructor. + * + * @param msgid the msgid to search for + */ + public MessageIDTerm(String msgid) { + // Note: comparison is case-insensitive + super(msgid); + } + + /** + * The match method. + * + * @param msg the match is applied to this Message's + * Message-ID header + * @return true if the match succeeds, otherwise false + */ + @Override + public boolean match(Message msg) { + String[] s; + + try { + s = msg.getHeader("Message-ID"); + } catch (Exception e) { + return false; + } + + if (s == null) + return false; + + for (int i=0; i < s.length; i++) + if (super.match(s[i])) + return true; + return false; + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof MessageIDTerm)) + return false; + return super.equals(obj); + } +}
diff --git a/mail/src/main/java/javax/mail/search/MessageNumberTerm.java b/mail/src/main/java/javax/mail/search/MessageNumberTerm.java new file mode 100644 index 0000000..3ec0f28 --- /dev/null +++ b/mail/src/main/java/javax/mail/search/MessageNumberTerm.java
@@ -0,0 +1,68 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.search; + +import javax.mail.Message; + +/** + * This class implements comparisons for Message numbers. + * + * @author Bill Shannon + * @author John Mani + */ +public final class MessageNumberTerm extends IntegerComparisonTerm { + + private static final long serialVersionUID = -5379625829658623812L; + + /** + * Constructor. + * + * @param number the Message number + */ + public MessageNumberTerm(int number) { + super(EQ, number); + } + + /** + * The match method. + * + * @param msg the Message number is matched with this Message + * @return true if the match succeeds, otherwise false + */ + @Override + public boolean match(Message msg) { + int msgno; + + try { + msgno = msg.getMessageNumber(); + } catch (Exception e) { + return false; + } + + return super.match(msgno); + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof MessageNumberTerm)) + return false; + return super.equals(obj); + } +}
diff --git a/mail/src/main/java/javax/mail/search/NotTerm.java b/mail/src/main/java/javax/mail/search/NotTerm.java new file mode 100644 index 0000000..b87c151 --- /dev/null +++ b/mail/src/main/java/javax/mail/search/NotTerm.java
@@ -0,0 +1,74 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.search; + +import javax.mail.Message; + +/** + * This class implements the logical NEGATION operator. + * + * @author Bill Shannon + * @author John Mani + */ +public final class NotTerm extends SearchTerm { + /** + * The search term to negate. + * + * @serial + */ + private SearchTerm term; + + private static final long serialVersionUID = 7152293214217310216L; + + public NotTerm(SearchTerm t) { + term = t; + } + + /** + * Return the term to negate. + * + * @return the Term + */ + public SearchTerm getTerm() { + return term; + } + + /* The NOT operation */ + @Override + public boolean match(Message msg) { + return !term.match(msg); + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof NotTerm)) + return false; + NotTerm nt = (NotTerm)obj; + return nt.term.equals(this.term); + } + + /** + * Compute a hashCode for this object. + */ + @Override + public int hashCode() { + return term.hashCode() << 1; + } +}
diff --git a/mail/src/main/java/javax/mail/search/OrTerm.java b/mail/src/main/java/javax/mail/search/OrTerm.java new file mode 100644 index 0000000..3df58f4 --- /dev/null +++ b/mail/src/main/java/javax/mail/search/OrTerm.java
@@ -0,0 +1,116 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.search; + +import javax.mail.Message; + +/** + * This class implements the logical OR operator on individual SearchTerms. + * + * @author Bill Shannon + * @author John Mani + */ +public final class OrTerm extends SearchTerm { + + /** + * The array of terms on which the OR operator should + * be applied. + * + * @serial + */ + private SearchTerm[] terms; + + private static final long serialVersionUID = 5380534067523646936L; + + /** + * Constructor that takes two operands. + * + * @param t1 first term + * @param t2 second term + */ + public OrTerm(SearchTerm t1, SearchTerm t2) { + terms = new SearchTerm[2]; + terms[0] = t1; + terms[1] = t2; + } + + /** + * Constructor that takes an array of SearchTerms. + * + * @param t array of search terms + */ + public OrTerm(SearchTerm[] t) { + terms = new SearchTerm[t.length]; + for (int i = 0; i < t.length; i++) + terms[i] = t[i]; + } + + /** + * Return the search terms. + * + * @return the search terms + */ + public SearchTerm[] getTerms() { + return terms.clone(); + } + + /** + * The OR operation. <p> + * + * The terms specified in the constructor are applied to + * the given object and the OR operator is applied to their results. + * + * @param msg The specified SearchTerms are applied to this Message + * and the OR operator is applied to their results. + * @return true if the OR succeds, otherwise false + */ + + @Override + public boolean match(Message msg) { + for (int i=0; i < terms.length; i++) + if (terms[i].match(msg)) + return true; + return false; + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof OrTerm)) + return false; + OrTerm ot = (OrTerm)obj; + if (ot.terms.length != terms.length) + return false; + for (int i=0; i < terms.length; i++) + if (!terms[i].equals(ot.terms[i])) + return false; + return true; + } + + /** + * Compute a hashCode for this object. + */ + @Override + public int hashCode() { + int hash = 0; + for (int i=0; i < terms.length; i++) + hash += terms[i].hashCode(); + return hash; + } +}
diff --git a/mail/src/main/java/javax/mail/search/ReceivedDateTerm.java b/mail/src/main/java/javax/mail/search/ReceivedDateTerm.java new file mode 100644 index 0000000..b189d01 --- /dev/null +++ b/mail/src/main/java/javax/mail/search/ReceivedDateTerm.java
@@ -0,0 +1,74 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.search; + +import java.util.Date; +import javax.mail.Message; + +/** + * This class implements comparisons for the Message Received date + * + * @author Bill Shannon + * @author John Mani + */ +public final class ReceivedDateTerm extends DateTerm { + + private static final long serialVersionUID = -2756695246195503170L; + + /** + * Constructor. + * + * @param comparison the Comparison type + * @param date the date to be compared + */ + public ReceivedDateTerm(int comparison, Date date) { + super(comparison, date); + } + + /** + * The match method. + * + * @param msg the date comparator is applied to this Message's + * received date + * @return true if the comparison succeeds, otherwise false + */ + @Override + public boolean match(Message msg) { + Date d; + + try { + d = msg.getReceivedDate(); + } catch (Exception e) { + return false; + } + + if (d == null) + return false; + + return super.match(d); + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof ReceivedDateTerm)) + return false; + return super.equals(obj); + } +}
diff --git a/mail/src/main/java/javax/mail/search/RecipientStringTerm.java b/mail/src/main/java/javax/mail/search/RecipientStringTerm.java new file mode 100644 index 0000000..60466cd --- /dev/null +++ b/mail/src/main/java/javax/mail/search/RecipientStringTerm.java
@@ -0,0 +1,109 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.search; + +import javax.mail.Message; +import javax.mail.Address; + +/** + * This class implements string comparisons for the Recipient Address + * headers. <p> + * + * Note that this class differs from the <code>RecipientTerm</code> class + * in that this class does comparisons on address strings rather than Address + * objects. The string comparisons are case-insensitive. + * + * @since JavaMail 1.1 + */ + +public final class RecipientStringTerm extends AddressStringTerm { + + /** + * The recipient type. + * + * @serial + */ + private Message.RecipientType type; + + private static final long serialVersionUID = -8293562089611618849L; + + /** + * Constructor. + * + * @param type the recipient type + * @param pattern the address pattern to be compared. + */ + public RecipientStringTerm(Message.RecipientType type, String pattern) { + super(pattern); + this.type = type; + } + + /** + * Return the type of recipient to match with. + * + * @return the recipient type + */ + public Message.RecipientType getRecipientType() { + return type; + } + + /** + * Check whether the address specified in the constructor is + * a substring of the recipient address of this Message. + * + * @param msg The comparison is applied to this Message's recipient + * address. + * @return true if the match succeeds, otherwise false. + */ + @Override + public boolean match(Message msg) { + Address[] recipients; + + try { + recipients = msg.getRecipients(type); + } catch (Exception e) { + return false; + } + + if (recipients == null) + return false; + + for (int i=0; i < recipients.length; i++) + if (super.match(recipients[i])) + return true; + return false; + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof RecipientStringTerm)) + return false; + RecipientStringTerm rst = (RecipientStringTerm)obj; + return rst.type.equals(this.type) && super.equals(obj); + } + + /** + * Compute a hashCode for this object. + */ + @Override + public int hashCode() { + return type.hashCode() + super.hashCode(); + } +}
diff --git a/mail/src/main/java/javax/mail/search/RecipientTerm.java b/mail/src/main/java/javax/mail/search/RecipientTerm.java new file mode 100644 index 0000000..7f2d149 --- /dev/null +++ b/mail/src/main/java/javax/mail/search/RecipientTerm.java
@@ -0,0 +1,103 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.search; + +import javax.mail.Message; +import javax.mail.Address; + +/** + * This class implements comparisons for the Recipient Address headers. + * + * @author Bill Shannon + * @author John Mani + */ +public final class RecipientTerm extends AddressTerm { + + /** + * The recipient type. + * + * @serial + */ + private Message.RecipientType type; + + private static final long serialVersionUID = 6548700653122680468L; + + /** + * Constructor. + * + * @param type the recipient type + * @param address the address to match for + */ + public RecipientTerm(Message.RecipientType type, Address address) { + super(address); + this.type = type; + } + + /** + * Return the type of recipient to match with. + * + * @return the recipient type + */ + public Message.RecipientType getRecipientType() { + return type; + } + + /** + * The match method. + * + * @param msg The address match is applied to this Message's recepient + * address + * @return true if the match succeeds, otherwise false + */ + @Override + public boolean match(Message msg) { + Address[] recipients; + + try { + recipients = msg.getRecipients(type); + } catch (Exception e) { + return false; + } + + if (recipients == null) + return false; + + for (int i=0; i < recipients.length; i++) + if (super.match(recipients[i])) + return true; + return false; + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof RecipientTerm)) + return false; + RecipientTerm rt = (RecipientTerm)obj; + return rt.type.equals(this.type) && super.equals(obj); + } + + /** + * Compute a hashCode for this object. + */ + @Override + public int hashCode() { + return type.hashCode() + super.hashCode(); + } +}
diff --git a/mail/src/main/java/javax/mail/search/SearchException.java b/mail/src/main/java/javax/mail/search/SearchException.java new file mode 100644 index 0000000..5c4d460 --- /dev/null +++ b/mail/src/main/java/javax/mail/search/SearchException.java
@@ -0,0 +1,46 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.search; + +import javax.mail.MessagingException; + + +/** + * The exception thrown when a Search expression could not be handled. + * + * @author John Mani + */ + +public class SearchException extends MessagingException { + + private static final long serialVersionUID = -7092886778226268686L; + + /** + * Constructs a SearchException with no detail message. + */ + public SearchException() { + super(); + } + + /** + * Constructs a SearchException with the specified detail message. + * @param s the detail message + */ + public SearchException(String s) { + super(s); + } +}
diff --git a/mail/src/main/java/javax/mail/search/SearchTerm.java b/mail/src/main/java/javax/mail/search/SearchTerm.java new file mode 100644 index 0000000..3194b63 --- /dev/null +++ b/mail/src/main/java/javax/mail/search/SearchTerm.java
@@ -0,0 +1,54 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.search; + +import java.io.Serializable; + +import javax.mail.Message; + +/** + * Search criteria are expressed as a tree of search-terms, forming + * a parse-tree for the search expression. <p> + * + * Search-terms are represented by this class. This is an abstract + * class; subclasses implement specific match methods. <p> + * + * Search terms are serializable, which allows storing a search term + * between sessions. + * + * <strong>Warning:</strong> + * Serialized objects of this class may not be compatible with future + * JavaMail API releases. The current serialization support is + * appropriate for short term storage. <p> + * + * @author Bill Shannon + * @author John Mani + */ +public abstract class SearchTerm implements Serializable { + + private static final long serialVersionUID = -6652358452205992789L; + + /** + * This method applies a specific match criterion to the given + * message and returns the result. + * + * @param msg The match criterion is applied on this message + * @return true, it the match succeeds, false if the match fails + */ + + public abstract boolean match(Message msg); +}
diff --git a/mail/src/main/java/javax/mail/search/SentDateTerm.java b/mail/src/main/java/javax/mail/search/SentDateTerm.java new file mode 100644 index 0000000..2bd2fcb --- /dev/null +++ b/mail/src/main/java/javax/mail/search/SentDateTerm.java
@@ -0,0 +1,74 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.search; + +import java.util.Date; +import javax.mail.Message; + +/** + * This class implements comparisons for the Message SentDate. + * + * @author Bill Shannon + * @author John Mani + */ +public final class SentDateTerm extends DateTerm { + + private static final long serialVersionUID = 5647755030530907263L; + + /** + * Constructor. + * + * @param comparison the Comparison type + * @param date the date to be compared + */ + public SentDateTerm(int comparison, Date date) { + super(comparison, date); + } + + /** + * The match method. + * + * @param msg the date comparator is applied to this Message's + * sent date + * @return true if the comparison succeeds, otherwise false + */ + @Override + public boolean match(Message msg) { + Date d; + + try { + d = msg.getSentDate(); + } catch (Exception e) { + return false; + } + + if (d == null) + return false; + + return super.match(d); + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof SentDateTerm)) + return false; + return super.equals(obj); + } +}
diff --git a/mail/src/main/java/javax/mail/search/SizeTerm.java b/mail/src/main/java/javax/mail/search/SizeTerm.java new file mode 100644 index 0000000..a940176 --- /dev/null +++ b/mail/src/main/java/javax/mail/search/SizeTerm.java
@@ -0,0 +1,72 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.search; + +import javax.mail.Message; + +/** + * This class implements comparisons for Message sizes. + * + * @author Bill Shannon + * @author John Mani + */ +public final class SizeTerm extends IntegerComparisonTerm { + + private static final long serialVersionUID = -2556219451005103709L; + + /** + * Constructor. + * + * @param comparison the Comparison type + * @param size the size + */ + public SizeTerm(int comparison, int size) { + super(comparison, size); + } + + /** + * The match method. + * + * @param msg the size comparator is applied to this Message's size + * @return true if the size is equal, otherwise false + */ + @Override + public boolean match(Message msg) { + int size; + + try { + size = msg.getSize(); + } catch (Exception e) { + return false; + } + + if (size == -1) + return false; + + return super.match(size); + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof SizeTerm)) + return false; + return super.equals(obj); + } +}
diff --git a/mail/src/main/java/javax/mail/search/StringTerm.java b/mail/src/main/java/javax/mail/search/StringTerm.java new file mode 100644 index 0000000..51edfae --- /dev/null +++ b/mail/src/main/java/javax/mail/search/StringTerm.java
@@ -0,0 +1,117 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.search; + +/** + * This class implements the match method for Strings. The current + * implementation provides only for substring matching. We + * could add comparisons (like strcmp ...). + * + * @author Bill Shannon + * @author John Mani + */ +public abstract class StringTerm extends SearchTerm { + /** + * The pattern. + * + * @serial + */ + protected String pattern; + + /** + * Ignore case when comparing? + * + * @serial + */ + protected boolean ignoreCase; + + private static final long serialVersionUID = 1274042129007696269L; + + /** + * Construct a StringTerm with the given pattern. + * Case will be ignored. + * + * @param pattern the pattern + */ + protected StringTerm(String pattern) { + this.pattern = pattern; + ignoreCase = true; + } + + /** + * Construct a StringTerm with the given pattern and ignoreCase flag. + * + * @param pattern the pattern + * @param ignoreCase should we ignore case? + */ + protected StringTerm(String pattern, boolean ignoreCase) { + this.pattern = pattern; + this.ignoreCase = ignoreCase; + } + + /** + * Return the string to match with. + * + * @return the string to match + */ + public String getPattern() { + return pattern; + } + + /** + * Return true if we should ignore case when matching. + * + * @return true if we should ignore case + */ + public boolean getIgnoreCase() { + return ignoreCase; + } + + protected boolean match(String s) { + int len = s.length() - pattern.length(); + for (int i=0; i <= len; i++) { + if (s.regionMatches(ignoreCase, i, + pattern, 0, pattern.length())) + return true; + } + return false; + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof StringTerm)) + return false; + StringTerm st = (StringTerm)obj; + if (ignoreCase) + return st.pattern.equalsIgnoreCase(this.pattern) && + st.ignoreCase == this.ignoreCase; + else + return st.pattern.equals(this.pattern) && + st.ignoreCase == this.ignoreCase; + } + + /** + * Compute a hashCode for this object. + */ + @Override + public int hashCode() { + return ignoreCase ? pattern.hashCode() : ~pattern.hashCode(); + } +}
diff --git a/mail/src/main/java/javax/mail/search/SubjectTerm.java b/mail/src/main/java/javax/mail/search/SubjectTerm.java new file mode 100644 index 0000000..3326180 --- /dev/null +++ b/mail/src/main/java/javax/mail/search/SubjectTerm.java
@@ -0,0 +1,75 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.search; + +import javax.mail.Message; + +/** + * This class implements comparisons for the message Subject header. + * The comparison is case-insensitive. The pattern is a simple string + * that must appear as a substring in the Subject. + * + * @author Bill Shannon + * @author John Mani + */ +public final class SubjectTerm extends StringTerm { + + private static final long serialVersionUID = 7481568618055573432L; + + /** + * Constructor. + * + * @param pattern the pattern to search for + */ + public SubjectTerm(String pattern) { + // Note: comparison is case-insensitive + super(pattern); + } + + /** + * The match method. + * + * @param msg the pattern match is applied to this Message's + * subject header + * @return true if the pattern match succeeds, otherwise false + */ + @Override + public boolean match(Message msg) { + String subj; + + try { + subj = msg.getSubject(); + } catch (Exception e) { + return false; + } + + if (subj == null) + return false; + + return super.match(subj); + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof SubjectTerm)) + return false; + return super.equals(obj); + } +}
diff --git a/mail/src/main/java/javax/mail/search/package.html b/mail/src/main/java/javax/mail/search/package.html new file mode 100644 index 0000000..fbab315 --- /dev/null +++ b/mail/src/main/java/javax/mail/search/package.html
@@ -0,0 +1,49 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> +<HTML> +<HEAD> +<meta http-equiv="Content-Type" content="text/html; charset=utf-8"> +<!-- + + Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<TITLE>javax.mail.search package</TITLE> +</HEAD> +<BODY BGCOLOR="white"> + +<P> +Message search terms for the JavaMail API. +This package defines classes that can be used to construct a search +expression to search a folder for messages matching the expression; +see the {@link javax.mail.Folder#search search} method on +{@link javax.mail.Folder javax.mail.Folder}. +See {@link javax.mail.search.SearchTerm SearchTerm}. +</P> +<P> +Note that the exact search capabilities depend on the protocol, +provider, and server in use. For the POP3 protocol, all searching is +done on the client side using the JavaMail classes. For IMAP, all +searching is done on the server side and is limited by the search +capabilities of the IMAP protocol and the IMAP server being used. +For example, IMAP date based searches have only day granularity. +</P> +<P> +In general, all of the string patterns supported by search terms are +just simple strings; no regular expressions are supported. +</P> + +</BODY> +</HTML>
diff --git a/mail/src/main/java/javax/mail/util/ByteArrayDataSource.java b/mail/src/main/java/javax/mail/util/ByteArrayDataSource.java new file mode 100644 index 0000000..e4b9e5b --- /dev/null +++ b/mail/src/main/java/javax/mail/util/ByteArrayDataSource.java
@@ -0,0 +1,180 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.util; + +import java.io.*; +import javax.activation.*; +import javax.mail.internet.*; + +/** + * A DataSource backed by a byte array. The byte array may be + * passed in directly, or may be initialized from an InputStream + * or a String. + * + * @since JavaMail 1.4 + * @author John Mani + * @author Bill Shannon + * @author Max Spivak + */ +public class ByteArrayDataSource implements DataSource { + private byte[] data; // data + private int len = -1; + private String type; // content-type + private String name = ""; + + static class DSByteArrayOutputStream extends ByteArrayOutputStream { + public byte[] getBuf() { + return buf; + } + + public int getCount() { + return count; + } + } + + /** + * Create a ByteArrayDataSource with data from the + * specified InputStream and with the specified MIME type. + * The InputStream is read completely and the data is + * stored in a byte array. + * + * @param is the InputStream + * @param type the MIME type + * @exception IOException errors reading the stream + */ + public ByteArrayDataSource(InputStream is, String type) throws IOException { + DSByteArrayOutputStream os = new DSByteArrayOutputStream(); + byte[] buf = new byte[8192]; + int len; + while ((len = is.read(buf)) > 0) + os.write(buf, 0, len); + this.data = os.getBuf(); + this.len = os.getCount(); + + /* + * ByteArrayOutputStream doubles the size of the buffer every time + * it needs to expand, which can waste a lot of memory in the worst + * case with large buffers. Check how much is wasted here and if + * it's too much, copy the data into a new buffer and allow the + * old buffer to be garbage collected. + */ + if (this.data.length - this.len > 256*1024) { + this.data = os.toByteArray(); + this.len = this.data.length; // should be the same + } + this.type = type; + } + + /** + * Create a ByteArrayDataSource with data from the + * specified byte array and with the specified MIME type. + * + * @param data the data + * @param type the MIME type + */ + public ByteArrayDataSource(byte[] data, String type) { + this.data = data; + this.type = type; + } + + /** + * Create a ByteArrayDataSource with data from the + * specified String and with the specified MIME type. + * The MIME type should include a <code>charset</code> + * parameter specifying the charset to be used for the + * string. If the parameter is not included, the + * default charset is used. + * + * @param data the String + * @param type the MIME type + * @exception IOException errors reading the String + */ + public ByteArrayDataSource(String data, String type) throws IOException { + String charset = null; + try { + ContentType ct = new ContentType(type); + charset = ct.getParameter("charset"); + } catch (ParseException pex) { + // ignore parse error + } + charset = MimeUtility.javaCharset(charset); + if (charset == null) + charset = MimeUtility.getDefaultJavaCharset(); + // XXX - could convert to bytes on demand rather than copying here + this.data = data.getBytes(charset); + this.type = type; + } + + /** + * Return an InputStream for the data. + * Note that a new stream is returned each time + * this method is called. + * + * @return the InputStream + * @exception IOException if no data has been set + */ + @Override + public InputStream getInputStream() throws IOException { + if (data == null) + throw new IOException("no data"); + if (len < 0) + len = data.length; + return new SharedByteArrayInputStream(data, 0, len); + } + + /** + * Return an OutputStream for the data. + * Writing the data is not supported; an <code>IOException</code> + * is always thrown. + * + * @exception IOException always + */ + @Override + public OutputStream getOutputStream() throws IOException { + throw new IOException("cannot do this"); + } + + /** + * Get the MIME content type of the data. + * + * @return the MIME type + */ + @Override + public String getContentType() { + return type; + } + + /** + * Get the name of the data. + * By default, an empty string ("") is returned. + * + * @return the name of this data + */ + @Override + public String getName() { + return name; + } + + /** + * Set the name of the data. + * + * @param name the name of this data + */ + public void setName(String name) { + this.name = name; + } +}
diff --git a/mail/src/main/java/javax/mail/util/SharedByteArrayInputStream.java b/mail/src/main/java/javax/mail/util/SharedByteArrayInputStream.java new file mode 100644 index 0000000..0e13214 --- /dev/null +++ b/mail/src/main/java/javax/mail/util/SharedByteArrayInputStream.java
@@ -0,0 +1,93 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.util; + +import java.io.*; +import javax.mail.internet.SharedInputStream; + +/** + * A ByteArrayInputStream that implements the SharedInputStream interface, + * allowing the underlying byte array to be shared between multiple readers. + * + * @author Bill Shannon + * @since JavaMail 1.4 + */ + +public class SharedByteArrayInputStream extends ByteArrayInputStream + implements SharedInputStream { + /** + * Position within shared buffer that this stream starts at. + */ + protected int start = 0; + + /** + * Create a SharedByteArrayInputStream representing the entire + * byte array. + * + * @param buf the byte array + */ + public SharedByteArrayInputStream(byte[] buf) { + super(buf); + } + + /** + * Create a SharedByteArrayInputStream representing the part + * of the byte array from <code>offset</code> for <code>length</code> + * bytes. + * + * @param buf the byte array + * @param offset offset in byte array to first byte to include + * @param length number of bytes to include + */ + public SharedByteArrayInputStream(byte[] buf, int offset, int length) { + super(buf, offset, length); + start = offset; + } + + /** + * Return the current position in the InputStream, as an + * offset from the beginning of the InputStream. + * + * @return the current position + */ + @Override + public long getPosition() { + return pos - start; + } + + /** + * Return a new InputStream representing a subset of the data + * from this InputStream, starting at <code>start</code> (inclusive) + * up to <code>end</code> (exclusive). <code>start</code> must be + * non-negative. If <code>end</code> is -1, the new stream ends + * at the same place as this stream. The returned InputStream + * will also implement the SharedInputStream interface. + * + * @param start the starting position + * @param end the ending position + 1 + * @return the new stream + */ + @Override + public InputStream newStream(long start, long end) { + if (start < 0) + throw new IllegalArgumentException("start < 0"); + if (end == -1) + end = count - this.start; + return new SharedByteArrayInputStream(buf, + this.start + (int)start, (int)(end - start)); + } +}
diff --git a/mail/src/main/java/javax/mail/util/SharedFileInputStream.java b/mail/src/main/java/javax/mail/util/SharedFileInputStream.java new file mode 100644 index 0000000..31df0da --- /dev/null +++ b/mail/src/main/java/javax/mail/util/SharedFileInputStream.java
@@ -0,0 +1,539 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.util; + +import java.io.*; +import javax.mail.internet.SharedInputStream; + +/** + * A <code>SharedFileInputStream</code> is a + * <code>BufferedInputStream</code> that buffers + * data from the file and supports the <code>mark</code> + * and <code>reset</code> methods. It also supports the + * <code>newStream</code> method that allows you to create + * other streams that represent subsets of the file. + * A <code>RandomAccessFile</code> object is used to + * access the file data. <p> + * + * Note that when the SharedFileInputStream is closed, + * all streams created with the <code>newStream</code> + * method are also closed. This allows the creator of the + * SharedFileInputStream object to control access to the + * underlying file and ensure that it is closed when + * needed, to avoid leaking file descriptors. Note also + * that this behavior contradicts the requirements of + * SharedInputStream and may change in a future release. + * + * @author Bill Shannon + * @since JavaMail 1.4 + */ +public class SharedFileInputStream extends BufferedInputStream + implements SharedInputStream { + + private static int defaultBufferSize = 2048; + + /** + * The file containing the data. + * Shared by all related SharedFileInputStreams. + */ + protected RandomAccessFile in; + + /** + * The normal size of the read buffer. + */ + protected int bufsize; + + /** + * The file offset that corresponds to the first byte in + * the read buffer. + */ + protected long bufpos; + + /** + * The file offset of the start of data in this subset of the file. + */ + protected long start = 0; + + /** + * The amount of data in this subset of the file. + */ + protected long datalen; + + /** + * True if this is a top level stream created directly by "new". + * False if this is a derived stream created by newStream. + */ + private boolean master = true; + + /** + * A shared class that keeps track of the references + * to a particular file so it can be closed when the + * last reference is gone. + */ + static class SharedFile { + private int cnt; + private RandomAccessFile in; + + SharedFile(String file) throws IOException { + this.in = new RandomAccessFile(file, "r"); + } + + SharedFile(File file) throws IOException { + this.in = new RandomAccessFile(file, "r"); + } + + public synchronized RandomAccessFile open() { + cnt++; + return in; + } + + public synchronized void close() throws IOException { + if (cnt > 0 && --cnt <= 0) + in.close(); + } + + public synchronized void forceClose() throws IOException { + if (cnt > 0) { + // normal case, close exceptions propagated + cnt = 0; + in.close(); + } else { + // should already be closed, ignore exception + try { + in.close(); + } catch (IOException ioex) { } + } + } + + @Override + protected void finalize() throws Throwable { + try { + in.close(); + } finally { + super.finalize(); + } + } + } + + private SharedFile sf; + + /** + * Check to make sure that this stream has not been closed + */ + private void ensureOpen() throws IOException { + if (in == null) + throw new IOException("Stream closed"); + } + + /** + * Creates a <code>SharedFileInputStream</code> + * for the file. + * + * @param file the file + * @exception IOException for errors opening the file + */ + public SharedFileInputStream(File file) throws IOException { + this(file, defaultBufferSize); + } + + /** + * Creates a <code>SharedFileInputStream</code> + * for the named file + * + * @param file the file + * @exception IOException for errors opening the file + */ + public SharedFileInputStream(String file) throws IOException { + this(file, defaultBufferSize); + } + + /** + * Creates a <code>SharedFileInputStream</code> + * with the specified buffer size. + * + * @param file the file + * @param size the buffer size. + * @exception IOException for errors opening the file + * @exception IllegalArgumentException if size ≤ 0. + */ + public SharedFileInputStream(File file, int size) throws IOException { + super(null); // XXX - will it NPE? + if (size <= 0) + throw new IllegalArgumentException("Buffer size <= 0"); + init(new SharedFile(file), size); + } + + /** + * Creates a <code>SharedFileInputStream</code> + * with the specified buffer size. + * + * @param file the file + * @param size the buffer size. + * @exception IOException for errors opening the file + * @exception IllegalArgumentException if size ≤ 0. + */ + public SharedFileInputStream(String file, int size) throws IOException { + super(null); // XXX - will it NPE? + if (size <= 0) + throw new IllegalArgumentException("Buffer size <= 0"); + init(new SharedFile(file), size); + } + + private void init(SharedFile sf, int size) throws IOException { + this.sf = sf; + this.in = sf.open(); + this.start = 0; + this.datalen = in.length(); // XXX - file can't grow + this.bufsize = size; + buf = new byte[size]; + } + + /** + * Used internally by the <code>newStream</code> method. + */ + private SharedFileInputStream(SharedFile sf, long start, long len, + int bufsize) { + super(null); + this.master = false; + this.sf = sf; + this.in = sf.open(); + this.start = start; + this.bufpos = start; + this.datalen = len; + this.bufsize = bufsize; + buf = new byte[bufsize]; + } + + /** + * Fills the buffer with more data, taking into account + * shuffling and other tricks for dealing with marks. + * Assumes that it is being called by a synchronized method. + * This method also assumes that all data has already been read in, + * hence pos > count. + */ + private void fill() throws IOException { + if (markpos < 0) { + pos = 0; /* no mark: throw away the buffer */ + bufpos += count; + } else if (pos >= buf.length) /* no room left in buffer */ + if (markpos > 0) { /* can throw away early part of the buffer */ + int sz = pos - markpos; + System.arraycopy(buf, markpos, buf, 0, sz); + pos = sz; + bufpos += markpos; + markpos = 0; + } else if (buf.length >= marklimit) { + markpos = -1; /* buffer got too big, invalidate mark */ + pos = 0; /* drop buffer contents */ + bufpos += count; + } else { /* grow buffer */ + int nsz = pos * 2; + if (nsz > marklimit) + nsz = marklimit; + byte nbuf[] = new byte[nsz]; + System.arraycopy(buf, 0, nbuf, 0, pos); + buf = nbuf; + } + count = pos; + // limit to datalen + int len = buf.length - pos; + if (bufpos - start + pos + len > datalen) + len = (int)(datalen - (bufpos - start + pos)); + synchronized (in) { + in.seek(bufpos + pos); + int n = in.read(buf, pos, len); + if (n > 0) + count = n + pos; + } + } + + /** + * See the general contract of the <code>read</code> + * method of <code>InputStream</code>. + * + * @return the next byte of data, or <code>-1</code> if the end of the + * stream is reached. + * @exception IOException if an I/O error occurs. + */ + @Override + public synchronized int read() throws IOException { + ensureOpen(); + if (pos >= count) { + fill(); + if (pos >= count) + return -1; + } + return buf[pos++] & 0xff; + } + + /** + * Read characters into a portion of an array, reading from the underlying + * stream at most once if necessary. + */ + private int read1(byte[] b, int off, int len) throws IOException { + int avail = count - pos; + if (avail <= 0) { + if (false) { + /* If the requested length is at least as large as the buffer, and + if there is no mark/reset activity, do not bother to copy the + bytes into the local buffer. In this way buffered streams will + cascade harmlessly. */ + if (len >= buf.length && markpos < 0) { + // XXX - seek, update bufpos - how? + return in.read(b, off, len); + } + } + fill(); + avail = count - pos; + if (avail <= 0) return -1; + } + int cnt = (avail < len) ? avail : len; + System.arraycopy(buf, pos, b, off, cnt); + pos += cnt; + return cnt; + } + + /** + * Reads bytes from this stream into the specified byte array, + * starting at the given offset. + * + * <p> This method implements the general contract of the corresponding + * <code>{@link java.io.InputStream#read(byte[], int, int) read}</code> + * method of the <code>{@link java.io.InputStream}</code> class. + * + * @param b destination buffer. + * @param off offset at which to start storing bytes. + * @param len maximum number of bytes to read. + * @return the number of bytes read, or <code>-1</code> if the end of + * the stream has been reached. + * @exception IOException if an I/O error occurs. + */ + @Override + public synchronized int read(byte b[], int off, int len) + throws IOException + { + ensureOpen(); + if ((off | len | (off + len) | (b.length - (off + len))) < 0) { + throw new IndexOutOfBoundsException(); + } else if (len == 0) { + return 0; + } + + int n = read1(b, off, len); + if (n <= 0) return n; + while ((n < len) /* && (in.available() > 0) */) { + int n1 = read1(b, off + n, len - n); + if (n1 <= 0) break; + n += n1; + } + return n; + } + + /** + * See the general contract of the <code>skip</code> + * method of <code>InputStream</code>. + * + * @param n the number of bytes to be skipped. + * @return the actual number of bytes skipped. + * @exception IOException if an I/O error occurs. + */ + @Override + public synchronized long skip(long n) throws IOException { + ensureOpen(); + if (n <= 0) { + return 0; + } + long avail = count - pos; + + if (avail <= 0) { + // If no mark position set then don't keep in buffer + /* + if (markpos <0) + return in.skip(n); + */ + + // Fill in buffer to save bytes for reset + fill(); + avail = count - pos; + if (avail <= 0) + return 0; + } + + long skipped = (avail < n) ? avail : n; + pos += skipped; + return skipped; + } + + /** + * Returns the number of bytes that can be read from this input + * stream without blocking. + * + * @return the number of bytes that can be read from this input + * stream without blocking. + * @exception IOException if an I/O error occurs. + */ + @Override + public synchronized int available() throws IOException { + ensureOpen(); + return (count - pos) + in_available(); + } + + private int in_available() throws IOException { + // XXX - overflow + return (int)((start + datalen) - (bufpos + count)); + } + + /** + * See the general contract of the <code>mark</code> + * method of <code>InputStream</code>. + * + * @param readlimit the maximum limit of bytes that can be read before + * the mark position becomes invalid. + * @see #reset() + */ + @Override + public synchronized void mark(int readlimit) { + marklimit = readlimit; + markpos = pos; + } + + /** + * See the general contract of the <code>reset</code> + * method of <code>InputStream</code>. + * <p> + * If <code>markpos</code> is <code>-1</code> + * (no mark has been set or the mark has been + * invalidated), an <code>IOException</code> + * is thrown. Otherwise, <code>pos</code> is + * set equal to <code>markpos</code>. + * + * @exception IOException if this stream has not been marked or + * if the mark has been invalidated. + * @see #mark(int) + */ + @Override + public synchronized void reset() throws IOException { + ensureOpen(); + if (markpos < 0) + throw new IOException("Resetting to invalid mark"); + pos = markpos; + } + + /** + * Tests if this input stream supports the <code>mark</code> + * and <code>reset</code> methods. The <code>markSupported</code> + * method of <code>SharedFileInputStream</code> returns + * <code>true</code>. + * + * @return a <code>boolean</code> indicating if this stream type supports + * the <code>mark</code> and <code>reset</code> methods. + * @see java.io.InputStream#mark(int) + * @see java.io.InputStream#reset() + */ + @Override + public boolean markSupported() { + return true; + } + + /** + * Closes this input stream and releases any system resources + * associated with the stream. + * + * @exception IOException if an I/O error occurs. + */ + @Override + public void close() throws IOException { + if (in == null) + return; + try { + if (master) + sf.forceClose(); + else + sf.close(); + } finally { + sf = null; + in = null; + buf = null; + } + } + + /** + * Return the current position in the InputStream, as an + * offset from the beginning of the InputStream. + * + * @return the current position + */ + @Override + public long getPosition() { +//System.out.println("getPosition: start " + start + " pos " + pos +// + " bufpos " + bufpos + " = " + (bufpos + pos - start)); + if (in == null) + throw new RuntimeException("Stream closed"); + return bufpos + pos - start; + } + + /** + * Return a new InputStream representing a subset of the data + * from this InputStream, starting at <code>start</code> (inclusive) + * up to <code>end</code> (exclusive). <code>start</code> must be + * non-negative. If <code>end</code> is -1, the new stream ends + * at the same place as this stream. The returned InputStream + * will also implement the SharedInputStream interface. + * + * @param start the starting position + * @param end the ending position + 1 + * @return the new stream + */ + @Override + public synchronized InputStream newStream(long start, long end) { + if (in == null) + throw new RuntimeException("Stream closed"); + if (start < 0) + throw new IllegalArgumentException("start < 0"); + if (end == -1) + end = datalen; + return new SharedFileInputStream(sf, + this.start + start, end - start, bufsize); + } + + // for testing... + /* + public static void main(String[] argv) throws Exception { + SharedFileInputStream is = new SharedFileInputStream(argv[0]); + java.util.Random r = new java.util.Random(); + int b; + while ((b = is.read()) >= 0) { + System.out.write(b); + if (r.nextDouble() < 0.3) { + InputStream is2 = is.newStream(is.getPosition(), -1); + int b2; + while ((b2 = is2.read()) >= 0) + ; + } + } + } + */ + + /** + * Force this stream to close. + */ + @Override + protected void finalize() throws Throwable { + super.finalize(); + close(); + } +}
diff --git a/mail/src/main/java/javax/mail/util/package.html b/mail/src/main/java/javax/mail/util/package.html new file mode 100644 index 0000000..190a26c --- /dev/null +++ b/mail/src/main/java/javax/mail/util/package.html
@@ -0,0 +1,34 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> +<HTML> +<HEAD> +<meta http-equiv="Content-Type" content="text/html; charset=utf-8"> +<!-- + + Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<TITLE>javax.mail.util package</TITLE> +</HEAD> +<BODY BGCOLOR="white"> + +<P> +JavaMail API utility classes. +This package specifies utility classes that are useful with +other JavaMail APIs. +</P> + +</BODY> +</HTML>
diff --git a/mail/src/main/java/overview.html b/mail/src/main/java/overview.html new file mode 100644 index 0000000..411d023 --- /dev/null +++ b/mail/src/main/java/overview.html
@@ -0,0 +1,347 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> +<HTML> +<HEAD> +<meta http-equiv="Content-Type" content="text/html; charset=utf-8"> +<!-- + + Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<TITLE>javax.mail package</TITLE> +</HEAD> +<BODY BGCOLOR="white"> + +<P> +The JavaMail™ API +provides classes that model a mail system. +The <code>javax.mail</code> package defines classes that are common to +all mail systems. +The <code>javax.mail.internet</code> package defines classes that are specific +to mail systems based on internet standards such as MIME, SMTP, POP3, and IMAP. +The JavaMail API includes the <code>javax.mail</code> package and subpackages. +</P> +<P> +For an overview of the JavaMail API, read the +<A HREF="https://javaee.github.io/javamail/docs/JavaMail-1.6.pdf" TARGET="_top"> +JavaMail specification</A>. +</P> +<P> +The code to send a plain text message can be as simple as the following: +</P> +<PRE> + Properties props = new Properties(); + props.put("mail.smtp.host", "my-mail-server"); + Session session = Session.getInstance(props, null); + + try { + MimeMessage msg = new MimeMessage(session); + msg.setFrom("me@example.com"); + msg.setRecipients(Message.RecipientType.TO, + "you@example.com"); + msg.setSubject("JavaMail hello world example"); + msg.setSentDate(new Date()); + msg.setText("Hello, world!\n"); + Transport.send(msg, "me@example.com", "my-password"); + } catch (MessagingException mex) { + System.out.println("send failed, exception: " + mex); + } +</PRE> +<P> +The JavaMail download bundle contains many more complete examples +in the "demo" directory. +</P> +<P> +Don't forget to see the +<A HREF="https://javaee.github.io/javamail/FAQ.html" TARGET="_top"> +JavaMail API FAQ</A> +for answers to the most common questions. +The <A HREF="https://javaee.github.io/javamail/" TARGET="_top"> +JavaMail web site</A> +contains many additional resources. +</P> +<A NAME="properties"><STRONG>Properties</STRONG></A> +<P> +The JavaMail API supports the following standard properties, +which may be set in the <code>Session</code> object, or in the +<code>Properties</code> object used to create the <code>Session</code> object. +The properties are always set as strings; the Type column describes +how the string is interpreted. For example, use +</P> +<PRE> + props.put("mail.debug", "true"); +</PRE> +<P> +to set the <code>mail.debug</code> property, which is of type boolean. +</P> +<TABLE BORDER SUMMARY="JavaMail properties"> +<TR> +<TH>Name</TH> +<TH>Type</TH> +<TH>Description</TH> +</TR> + +<TR> +<TD><A NAME="mail.debug">mail.debug</A></TD> +<TD>boolean</TD> +<TD> +The initial debug mode. +Default is false. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.from">mail.from</A></TD> +<TD>String</TD> +<TD> +The return email address of the current user, used by the +<code>InternetAddress</code> method <code>getLocalAddress</code>. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.mime.address.strict">mail.mime.address.strict</A></TD> +<TD>boolean</TD> +<TD> +The MimeMessage class uses the <code>InternetAddress</code> method +<code>parseHeader</code> to parse headers in messages. This property +controls the strict flag passed to the <code>parseHeader</code> +method. The default is true. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.host">mail.host</A></TD> +<TD>String</TD> +<TD> +The default host name of the mail server for both Stores and Transports. +Used if the <code>mail.<i>protocol</i>.host</code> property isn't set. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.store.protocol">mail.store.protocol</A></TD> +<TD>String</TD> +<TD> +Specifies the default message access protocol. The +<code>Session</code> method <code>getStore()</code> returns a Store +object that implements this protocol. By default the first Store +provider in the configuration files is returned. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.transport.protocol">mail.transport.protocol</A></TD> +<TD>String</TD> +<TD> +Specifies the default message transport protocol. The +<code>Session</code> method <code>getTransport()</code> returns a Transport +object that implements this protocol. By default the first Transport +provider in the configuration files is returned. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.user">mail.user</A></TD> +<TD>String</TD> +<TD> +The default user name to use when connecting to the mail server. +Used if the <code>mail.<i>protocol</i>.user</code> property isn't set. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.protocol.class">mail.<i>protocol</i>.class</A></TD> +<TD>String</TD> +<TD> +Specifies the fully qualified class name of the provider for the +specified protocol. Used in cases where more than one provider +for a given protocol exists; this property can be used to specify +which provider to use by default. The provider must still be listed +in a configuration file. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.protocol.host">mail.<i>protocol</i>.host</A></TD> +<TD>String</TD> +<TD> +The host name of the mail server for the specified protocol. +Overrides the <code>mail.host</code> property. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.protocol.port">mail.<i>protocol</i>.port</A></TD> +<TD>int</TD> +<TD> +The port number of the mail server for the specified protocol. +If not specified the protocol's default port number is used. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.protocol.user">mail.<i>protocol</i>.user</A></TD> +<TD>String</TD> +<TD> +The user name to use when connecting to mail servers +using the specified protocol. +Overrides the <code>mail.user</code> property. +</TD> +</TR> + +</TABLE> + +<P> +The following properties are supported by the reference implementation (RI) of +JavaMail, but are not currently a required part of the specification. +The names, types, defaults, and semantics of these properties may +change in future releases. +</P> +<TABLE BORDER SUMMARY="JavaMail RI properties"> +<TR> +<TH>Name</TH> +<TH>Type</TH> +<TH>Description</TH> +</TR> + +<TR> +<TD><A NAME="mail.debug.auth">mail.debug.auth</A></TD> +<TD>boolean</TD> +<TD> +Include protocol authentication commands (including usernames and passwords) +in the debug output. +Default is false. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.debug.auth.username">mail.debug.auth.username</A></TD> +<TD>boolean</TD> +<TD> +Include the user name in non-protocol debug output. +Default is true. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.debug.auth.password">mail.debug.auth.password</A></TD> +<TD>boolean</TD> +<TD> +Include the password in non-protocol debug output. +Default is false. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.transport.protocol.address-type">mail.transport.protocol.<i>address-type</i></A></TD> +<TD>String</TD> +<TD> +Specifies the default message transport protocol for the specified address type. +The <code>Session</code> method <code>getTransport(Address)</code> returns a +Transport object that implements this protocol when the address is of the +specified type (e.g., "rfc822" for standard internet addresses). +By default the first Transport configured for that address type is used. +This property can be used to override the behavior of the +{@link javax.mail.Transport#send send} method of the +{@link javax.mail.Transport Transport} class so that (for example) the "smtps" +protocol is used instead of the "smtp" protocol by setting the property +<code>mail.transport.protocol.rfc822</code> to <code>"smtps"</code>. +</TD> +</TR> + +<TR> +<TD><A NAME="mail.event.scope">mail.event.scope</A></TD> +<TD>String</TD> +<TD> +Controls the scope of events. (See the javax.mail.event package.) +By default, a separate event queue and thread is used for events for each +Store, Transport, or Folder. +If this property is set to "session", all such events are put in a single +event queue processed by a single thread for the current session. +If this property is set to "application", all such events are put in a single +event queue processed by a single thread for the current application. +(Applications are distinguished by their context class loader.) +</TD> +</TR> + +<TR> +<TD><A NAME="mail.event.executor">mail.event.executor</A></TD> +<TD>java.util.concurrent.Executor</TD> +<TD> +By default, a new Thread is created for each event queue. +This thread is used to call the listeners for these events. +If this property is set to an instance of an Executor, the +Executor.execute method is used to run the event dispatcher +for an event queue. The event dispatcher runs until the +event queue is no longer in use. +</TD> +</TR> + +</TABLE> + +<P> +The JavaMail API also supports several System properties; +see the {@link javax.mail.internet} package documentation +for details. +</P> +<P> +The JavaMail reference +implementation includes protocol providers in subpackages of +<code>com.sun.mail</code>. Note that the APIs to these protocol +providers are not part of the standard JavaMail API. Portable +programs will not use these APIs. +</P> +<P> +Nonportable programs may use the APIs of the protocol providers +by (for example) casting a returned <code>Folder</code> object to a +<code>com.sun.mail.imap.IMAPFolder</code> object. Similarly for +<code>Store</code> and <code>Message</code> objects returned from the +standard JavaMail APIs. +</P> +<P> +The protocol providers also support properties that are specific to +those providers. The package documentation for the +{@link com.sun.mail.imap IMAP}, {@link com.sun.mail.pop3 POP3}, +and {@link com.sun.mail.smtp SMTP} packages provide details. +</P> +<P> +In addition to printing debugging output as controlled by the +{@link javax.mail.Session Session} configuration, the current +implementation of classes in this package log the same information using +{@link java.util.logging.Logger} as described in the following table: +</P> +<TABLE BORDER SUMMARY="JavaMail Loggers"> +<TR> +<TH>Logger Name</TH> +<TH>Logging Level</TH> +<TH>Purpose</TH> +</TR> + +<TR> +<TD>javax.mail</TD> +<TD>CONFIG</TD> +<TD>Configuration of the Session</TD> +</TR> + +<TR> +<TD>javax.mail</TD> +<TD>FINE</TD> +<TD>General debugging output</TD> +</TR> +</TABLE> + +</BODY> +</HTML>
diff --git a/mail/src/main/resources/META-INF/LICENSE.txt b/mail/src/main/resources/META-INF/LICENSE.txt new file mode 100644 index 0000000..5de3d1b --- /dev/null +++ b/mail/src/main/resources/META-INF/LICENSE.txt
@@ -0,0 +1,637 @@ +# Eclipse Public License - v 2.0 + + THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE + PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION + OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + + 1. DEFINITIONS + + "Contribution" means: + + a) in the case of the initial Contributor, the initial content + Distributed under this Agreement, and + + b) in the case of each subsequent Contributor: + i) changes to the Program, and + ii) additions to the Program; + where such changes and/or additions to the Program originate from + and are Distributed by that particular Contributor. A Contribution + "originates" from a Contributor if it was added to the Program by + such Contributor itself or anyone acting on such Contributor's behalf. + Contributions do not include changes or additions to the Program that + are not Modified Works. + + "Contributor" means any person or entity that Distributes the Program. + + "Licensed Patents" mean patent claims licensable by a Contributor which + are necessarily infringed by the use or sale of its Contribution alone + or when combined with the Program. + + "Program" means the Contributions Distributed in accordance with this + Agreement. + + "Recipient" means anyone who receives the Program under this Agreement + or any Secondary License (as applicable), including Contributors. + + "Derivative Works" shall mean any work, whether in Source Code or other + form, that is based on (or derived from) the Program and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. + + "Modified Works" shall mean any work in Source Code or other form that + results from an addition to, deletion from, or modification of the + contents of the Program, including, for purposes of clarity any new file + in Source Code form that contains any contents of the Program. Modified + Works shall not include works that contain only declarations, + interfaces, types, classes, structures, or files of the Program solely + in each case in order to link to, bind by name, or subclass the Program + or Modified Works thereof. + + "Distribute" means the acts of a) distributing or b) making available + in any manner that enables the transfer of a copy. + + "Source Code" means the form of a Program preferred for making + modifications, including but not limited to software source code, + documentation source, and configuration files. + + "Secondary License" means either the GNU General Public License, + Version 2.0, or any later versions of that license, including any + exceptions or additional permissions as identified by the initial + Contributor. + + 2. GRANT OF RIGHTS + + a) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free copyright + license to reproduce, prepare Derivative Works of, publicly display, + publicly perform, Distribute and sublicense the Contribution of such + Contributor, if any, and such Derivative Works. + + b) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free patent + license under Licensed Patents to make, use, sell, offer to sell, + import and otherwise transfer the Contribution of such Contributor, + if any, in Source Code or other form. This patent license shall + apply to the combination of the Contribution and the Program if, at + the time the Contribution is added by the Contributor, such addition + of the Contribution causes such combination to be covered by the + Licensed Patents. The patent license shall not apply to any other + combinations which include the Contribution. No hardware per se is + licensed hereunder. + + c) Recipient understands that although each Contributor grants the + licenses to its Contributions set forth herein, no assurances are + provided by any Contributor that the Program does not infringe the + patent or other intellectual property rights of any other entity. + Each Contributor disclaims any liability to Recipient for claims + brought by any other entity based on infringement of intellectual + property rights or otherwise. As a condition to exercising the + rights and licenses granted hereunder, each Recipient hereby + assumes sole responsibility to secure any other intellectual + property rights needed, if any. For example, if a third party + patent license is required to allow Recipient to Distribute the + Program, it is Recipient's responsibility to acquire that license + before distributing the Program. + + d) Each Contributor represents that to its knowledge it has + sufficient copyright rights in its Contribution, if any, to grant + the copyright license set forth in this Agreement. + + e) Notwithstanding the terms of any Secondary License, no + Contributor makes additional grants to any Recipient (other than + those set forth in this Agreement) as a result of such Recipient's + receipt of the Program under the terms of a Secondary License + (if permitted under the terms of Section 3). + + 3. REQUIREMENTS + + 3.1 If a Contributor Distributes the Program in any form, then: + + a) the Program must also be made available as Source Code, in + accordance with section 3.2, and the Contributor must accompany + the Program with a statement that the Source Code for the Program + is available under this Agreement, and informs Recipients how to + obtain it in a reasonable manner on or through a medium customarily + used for software exchange; and + + b) the Contributor may Distribute the Program under a license + different than this Agreement, provided that such license: + i) effectively disclaims on behalf of all other Contributors all + warranties and conditions, express and implied, including + warranties or conditions of title and non-infringement, and + implied warranties or conditions of merchantability and fitness + for a particular purpose; + + ii) effectively excludes on behalf of all other Contributors all + liability for damages, including direct, indirect, special, + incidental and consequential damages, such as lost profits; + + iii) does not attempt to limit or alter the recipients' rights + in the Source Code under section 3.2; and + + iv) requires any subsequent distribution of the Program by any + party to be under a license that satisfies the requirements + of this section 3. + + 3.2 When the Program is Distributed as Source Code: + + a) it must be made available under this Agreement, or if the + Program (i) is combined with other material in a separate file or + files made available under a Secondary License, and (ii) the initial + Contributor attached to the Source Code the notice described in + Exhibit A of this Agreement, then the Program may be made available + under the terms of such Secondary Licenses, and + + b) a copy of this Agreement must be included with each copy of + the Program. + + 3.3 Contributors may not remove or alter any copyright, patent, + trademark, attribution notices, disclaimers of warranty, or limitations + of liability ("notices") contained within the Program from any copy of + the Program which they Distribute, provided that Contributors may add + their own appropriate notices. + + 4. COMMERCIAL DISTRIBUTION + + Commercial distributors of software may accept certain responsibilities + with respect to end users, business partners and the like. While this + license is intended to facilitate the commercial use of the Program, + the Contributor who includes the Program in a commercial product + offering should do so in a manner which does not create potential + liability for other Contributors. Therefore, if a Contributor includes + the Program in a commercial product offering, such Contributor + ("Commercial Contributor") hereby agrees to defend and indemnify every + other Contributor ("Indemnified Contributor") against any losses, + damages and costs (collectively "Losses") arising from claims, lawsuits + and other legal actions brought by a third party against the Indemnified + Contributor to the extent caused by the acts or omissions of such + Commercial Contributor in connection with its distribution of the Program + in a commercial product offering. The obligations in this section do not + apply to any claims or Losses relating to any actual or alleged + intellectual property infringement. In order to qualify, an Indemnified + Contributor must: a) promptly notify the Commercial Contributor in + writing of such claim, and b) allow the Commercial Contributor to control, + and cooperate with the Commercial Contributor in, the defense and any + related settlement negotiations. The Indemnified Contributor may + participate in any such claim at its own expense. + + For example, a Contributor might include the Program in a commercial + product offering, Product X. That Contributor is then a Commercial + Contributor. If that Commercial Contributor then makes performance + claims, or offers warranties related to Product X, those performance + claims and warranties are such Commercial Contributor's responsibility + alone. Under this section, the Commercial Contributor would have to + defend claims against the other Contributors related to those performance + claims and warranties, and if a court requires any other Contributor to + pay any damages as a result, the Commercial Contributor must pay + those damages. + + 5. NO WARRANTY + + EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT + PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" + BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR + IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF + TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR + PURPOSE. Each Recipient is solely responsible for determining the + appropriateness of using and distributing the Program and assumes all + risks associated with its exercise of rights under this Agreement, + including but not limited to the risks and costs of program errors, + compliance with applicable laws, damage to or loss of data, programs + or equipment, and unavailability or interruption of operations. + + 6. DISCLAIMER OF LIABILITY + + EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT + PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS + SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST + PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE + EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGES. + + 7. GENERAL + + If any provision of this Agreement is invalid or unenforceable under + applicable law, it shall not affect the validity or enforceability of + the remainder of the terms of this Agreement, and without further + action by the parties hereto, such provision shall be reformed to the + minimum extent necessary to make such provision valid and enforceable. + + If Recipient institutes patent litigation against any entity + (including a cross-claim or counterclaim in a lawsuit) alleging that the + Program itself (excluding combinations of the Program with other software + or hardware) infringes such Recipient's patent(s), then such Recipient's + rights granted under Section 2(b) shall terminate as of the date such + litigation is filed. + + All Recipient's rights under this Agreement shall terminate if it + fails to comply with any of the material terms or conditions of this + Agreement and does not cure such failure in a reasonable period of + time after becoming aware of such noncompliance. If all Recipient's + rights under this Agreement terminate, Recipient agrees to cease use + and distribution of the Program as soon as reasonably practicable. + However, Recipient's obligations under this Agreement and any licenses + granted by Recipient relating to the Program shall continue and survive. + + Everyone is permitted to copy and distribute copies of this Agreement, + but in order to avoid inconsistency the Agreement is copyrighted and + may only be modified in the following manner. The Agreement Steward + reserves the right to publish new versions (including revisions) of + this Agreement from time to time. No one other than the Agreement + Steward has the right to modify this Agreement. The Eclipse Foundation + is the initial Agreement Steward. The Eclipse Foundation may assign the + responsibility to serve as the Agreement Steward to a suitable separate + entity. Each new version of the Agreement will be given a distinguishing + version number. The Program (including Contributions) may always be + Distributed subject to the version of the Agreement under which it was + received. In addition, after a new version of the Agreement is published, + Contributor may elect to Distribute the Program (including its + Contributions) under the new version. + + Except as expressly stated in Sections 2(a) and 2(b) above, Recipient + receives no rights or licenses to the intellectual property of any + Contributor under this Agreement, whether expressly, by implication, + estoppel or otherwise. All rights in the Program not expressly granted + under this Agreement are reserved. Nothing in this Agreement is intended + to be enforceable by any entity that is not a Contributor or Recipient. + No third-party beneficiary rights are created under this Agreement. + + Exhibit A - Form of Secondary Licenses Notice + + "This Source Code may also be made available under the following + Secondary Licenses when the conditions for such availability set forth + in the Eclipse Public License, v. 2.0 are satisfied: {name license(s), + version(s), and exceptions or additional permissions here}." + + Simply including a copy of this Agreement, including this Exhibit A + is not sufficient to license the Source Code under Secondary Licenses. + + If it is not possible or desirable to put the notice in a particular + file, then You may include the notice in a location (such as a LICENSE + file in a relevant directory) where a recipient would be likely to + look for such a notice. + + You may add additional accurate notices of copyright ownership. + +--- + +## The GNU General Public License (GPL) Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor + Boston, MA 02110-1335 + USA + + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your freedom to + share and change it. By contrast, the GNU General Public License is + intended to guarantee your freedom to share and change free software--to + make sure the software is free for all its users. This General Public + License applies to most of the Free Software Foundation's software and + to any other program whose authors commit to using it. (Some other Free + Software Foundation software is covered by the GNU Library General + Public License instead.) You can apply it to your programs, too. + + When we speak of free software, we are referring to freedom, not price. + Our General Public Licenses are designed to make sure that you have the + freedom to distribute copies of free software (and charge for this + service if you wish), that you receive source code or can get it if you + want it, that you can change the software or use pieces of it in new + free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid anyone + to deny you these rights or to ask you to surrender the rights. These + restrictions translate to certain responsibilities for you if you + distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether gratis + or for a fee, you must give the recipients all the rights that you have. + You must make sure that they, too, receive or can get the source code. + And you must show them these terms so they know their rights. + + We protect your rights with two steps: (1) copyright the software, and + (2) offer you this license which gives you legal permission to copy, + distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain + that everyone understands that there is no warranty for this free + software. If the software is modified by someone else and passed on, we + want its recipients to know that what they have is not the original, so + that any problems introduced by others will not reflect on the original + authors' reputations. + + Finally, any free program is threatened constantly by software patents. + We wish to avoid the danger that redistributors of a free program will + individually obtain patent licenses, in effect making the program + proprietary. To prevent this, we have made it clear that any patent must + be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and + modification follow. + + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains a + notice placed by the copyright holder saying it may be distributed under + the terms of this General Public License. The "Program", below, refers + to any such program or work, and a "work based on the Program" means + either the Program or any derivative work under copyright law: that is + to say, a work containing the Program or a portion of it, either + verbatim or with modifications and/or translated into another language. + (Hereinafter, translation is included without limitation in the term + "modification".) Each licensee is addressed as "you". + + Activities other than copying, distribution and modification are not + covered by this License; they are outside its scope. The act of running + the Program is not restricted, and the output from the Program is + covered only if its contents constitute a work based on the Program + (independent of having been made by running the Program). Whether that + is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's source + code as you receive it, in any medium, provided that you conspicuously + and appropriately publish on each copy an appropriate copyright notice + and disclaimer of warranty; keep intact all the notices that refer to + this License and to the absence of any warranty; and give any other + recipients of the Program a copy of this License along with the Program. + + You may charge a fee for the physical act of transferring a copy, and + you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion of + it, thus forming a work based on the Program, and copy and distribute + such modifications or work under the terms of Section 1 above, provided + that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any part + thereof, to be licensed as a whole at no charge to all third parties + under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a notice + that there is no warranty (or else, saying that you provide a + warranty) and that users may redistribute the program under these + conditions, and telling the user how to view a copy of this License. + (Exception: if the Program itself is interactive but does not + normally print such an announcement, your work based on the Program + is not required to print an announcement.) + + These requirements apply to the modified work as a whole. If + identifiable sections of that work are not derived from the Program, and + can be reasonably considered independent and separate works in + themselves, then this License, and its terms, do not apply to those + sections when you distribute them as separate works. But when you + distribute the same sections as part of a whole which is a work based on + the Program, the distribution of the whole must be on the terms of this + License, whose permissions for other licensees extend to the entire + whole, and thus to each and every part regardless of who wrote it. + + Thus, it is not the intent of this section to claim rights or contest + your rights to work written entirely by you; rather, the intent is to + exercise the right to control the distribution of derivative or + collective works based on the Program. + + In addition, mere aggregation of another work not based on the Program + with the Program (or with a work based on the Program) on a volume of a + storage or distribution medium does not bring the other work under the + scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, + under Section 2) in object code or executable form under the terms of + Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections 1 + and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your cost + of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer to + distribute corresponding source code. (This alternative is allowed + only for noncommercial distribution and only if you received the + program in object code or executable form with such an offer, in + accord with Subsection b above.) + + The source code for a work means the preferred form of the work for + making modifications to it. For an executable work, complete source code + means all the source code for all modules it contains, plus any + associated interface definition files, plus the scripts used to control + compilation and installation of the executable. However, as a special + exception, the source code distributed need not include anything that is + normally distributed (in either source or binary form) with the major + components (compiler, kernel, and so on) of the operating system on + which the executable runs, unless that component itself accompanies the + executable. + + If distribution of executable or object code is made by offering access + to copy from a designated place, then offering equivalent access to copy + the source code from the same place counts as distribution of the source + code, even though third parties are not compelled to copy the source + along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program + except as expressly provided under this License. Any attempt otherwise + to copy, modify, sublicense or distribute the Program is void, and will + automatically terminate your rights under this License. However, parties + who have received copies, or rights, from you under this License will + not have their licenses terminated so long as such parties remain in + full compliance. + + 5. You are not required to accept this License, since you have not + signed it. However, nothing else grants you permission to modify or + distribute the Program or its derivative works. These actions are + prohibited by law if you do not accept this License. Therefore, by + modifying or distributing the Program (or any work based on the + Program), you indicate your acceptance of this License to do so, and all + its terms and conditions for copying, distributing or modifying the + Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the + Program), the recipient automatically receives a license from the + original licensor to copy, distribute or modify the Program subject to + these terms and conditions. You may not impose any further restrictions + on the recipients' exercise of the rights granted herein. You are not + responsible for enforcing compliance by third parties to this License. + + 7. If, as a consequence of a court judgment or allegation of patent + infringement or for any other reason (not limited to patent issues), + conditions are imposed on you (whether by court order, agreement or + otherwise) that contradict the conditions of this License, they do not + excuse you from the conditions of this License. If you cannot distribute + so as to satisfy simultaneously your obligations under this License and + any other pertinent obligations, then as a consequence you may not + distribute the Program at all. For example, if a patent license would + not permit royalty-free redistribution of the Program by all those who + receive copies directly or indirectly through you, then the only way you + could satisfy both it and this License would be to refrain entirely from + distribution of the Program. + + If any portion of this section is held invalid or unenforceable under + any particular circumstance, the balance of the section is intended to + apply and the section as a whole is intended to apply in other + circumstances. + + It is not the purpose of this section to induce you to infringe any + patents or other property right claims or to contest validity of any + such claims; this section has the sole purpose of protecting the + integrity of the free software distribution system, which is implemented + by public license practices. Many people have made generous + contributions to the wide range of software distributed through that + system in reliance on consistent application of that system; it is up to + the author/donor to decide if he or she is willing to distribute + software through any other system and a licensee cannot impose that choice. + + This section is intended to make thoroughly clear what is believed to be + a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in + certain countries either by patents or by copyrighted interfaces, the + original copyright holder who places the Program under this License may + add an explicit geographical distribution limitation excluding those + countries, so that distribution is permitted only in or among countries + not thus excluded. In such case, this License incorporates the + limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new + versions of the General Public License from time to time. Such new + versions will be similar in spirit to the present version, but may + differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the Program + specifies a version number of this License which applies to it and "any + later version", you have the option of following the terms and + conditions either of that version or of any later version published by + the Free Software Foundation. If the Program does not specify a version + number of this License, you may choose any version ever published by the + Free Software Foundation. + + 10. If you wish to incorporate parts of the Program into other free + programs whose distribution conditions are different, write to the + author to ask for permission. For software which is copyrighted by the + Free Software Foundation, write to the Free Software Foundation; we + sometimes make exceptions for this. Our decision will be guided by the + two goals of preserving the free status of all derivatives of our free + software and of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO + WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. + EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR + OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, + EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE + ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH + YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL + NECESSARY SERVICING, REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN + WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY + AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR + DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL + DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM + (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED + INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF + THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR + OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest + possible use to the public, the best way to achieve this is to make it + free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest to + attach them to the start of each source file to most effectively convey + the exclusion of warranty; and each file should have at least the + "copyright" line and a pointer to where the full notice is found. + + One line to give the program's name and a brief idea of what it does. + Copyright (C) <year> <name of author> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335 USA + + Also add information on how to contact you by electronic and paper mail. + + If the program is interactive, make it output a short notice like this + when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type + `show w'. This is free software, and you are welcome to redistribute + it under certain conditions; type `show c' for details. + + The hypothetical commands `show w' and `show c' should show the + appropriate parts of the General Public License. Of course, the commands + you use may be called something other than `show w' and `show c'; they + could even be mouse-clicks or menu items--whatever suits your program. + + You should also get your employer (if you work as a programmer) or your + school, if any, to sign a "copyright disclaimer" for the program, if + necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + program `Gnomovision' (which makes passes at compilers) written by + James Hacker. + + signature of Ty Coon, 1 April 1989 + Ty Coon, President of Vice + + This General Public License does not permit incorporating your program + into proprietary programs. If your program is a subroutine library, you + may consider it more useful to permit linking proprietary applications + with the library. If this is what you want to do, use the GNU Library + General Public License instead of this License. + +--- + +## CLASSPATH EXCEPTION + + Linking this library statically or dynamically with other modules is + making a combined work based on this library. Thus, the terms and + conditions of the GNU General Public License version 2 cover the whole + combination. + + As a special exception, the copyright holders of this library give you + permission to link this library with independent modules to produce an + executable, regardless of the license terms of these independent + modules, and to copy and distribute the resulting executable under + terms of your choice, provided that you also meet, for each linked + independent module, the terms and conditions of the license of that + module. An independent module is a module which is not derived from or + based on this library. If you modify this library, you may extend this + exception to your version of the library, but you are not obligated to + do so. If you do not wish to do so, delete this exception statement + from your version.
diff --git a/mail/src/main/resources/META-INF/MANIFEST.MF b/mail/src/main/resources/META-INF/MANIFEST.MF new file mode 100644 index 0000000..db5acf6 --- /dev/null +++ b/mail/src/main/resources/META-INF/MANIFEST.MF
@@ -0,0 +1,9 @@ +Manifest-Version: 1.0 +Extension-Name: javax.mail +Specification-Title: JavaMail(TM) API Design Specification +Specification-Version: ${mail.spec.version} +Specification-Vendor: ${project.organization.name} +Implementation-Title: javax.mail +Implementation-Version: ${mail.version} +Implementation-Vendor: ${project.organization.name} +Implementation-Vendor-Id: com.sun
diff --git a/mail/src/main/resources/META-INF/gfprobe-provider.xml b/mail/src/main/resources/META-INF/gfprobe-provider.xml new file mode 100644 index 0000000..b511959 --- /dev/null +++ b/mail/src/main/resources/META-INF/gfprobe-provider.xml
@@ -0,0 +1,69 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<probe-providers> + <probe-provider moduleProviderName="glassfish" moduleName="javamail" + probeProviderName="smtp-transport" + class="com.sun.mail.smtp.SMTPTransport"> + <probe name="sendMessageStart"> + <method>sendMessageStart</method> + <probe-param type="java.lang.String" name="subject" /> + <return-param type="void" /> + </probe> + <probe name="sendMessageEnd"> + <method>sendMessageEnd</method> + <return-param type="void" /> + </probe> + </probe-provider> + <probe-provider moduleProviderName="glassfish" moduleName="javamail" + probeProviderName="iap-protocol" + class="com.sun.mail.iap.Protocol"> + <probe name="commandStart"> + <method>commandStart</method> + <probe-param type="java.lang.String" name="command" /> + <return-param type="void" /> + </probe> + <probe name="commandEnd"> + <method>commandEnd</method> + <return-param type="void" /> + </probe> + </probe-provider> + <probe-provider moduleProviderName="glassfish" moduleName="javamail" + probeProviderName="pop3-protocol" + class="com.sun.mail.pop3.Protocol"> + <probe name="simpleCommandStart"> + <method>simpleCommandStart</method> + <probe-param type="java.lang.String" name="command" /> + <return-param type="void" /> + </probe> + <probe name="simpleCommandEnd"> + <method>simpleCommandEnd</method> + <return-param type="void" /> + </probe> + <probe name="multilineCommandStart"> + <method>multilineCommandStart</method> + <probe-param type="java.lang.String" name="command" /> + <return-param type="void" /> + </probe> + <probe name="multilineCommandEnd"> + <method>multilineCommandEnd</method> + <return-param type="void" /> + </probe> + </probe-provider> +</probe-providers>
diff --git a/mail/src/main/resources/META-INF/hk2-locator/default b/mail/src/main/resources/META-INF/hk2-locator/default new file mode 100644 index 0000000..226ff1d --- /dev/null +++ b/mail/src/main/resources/META-INF/hk2-locator/default
@@ -0,0 +1,9 @@ +# +# This metadata allows com.sun.mail.util.logging.MailHandler to be +# configured in the logging.properties file and used in GlassFish. +# This file was created by hand to avoid a compile time dependency +# on the HK2 annotations. +# +[com.sun.mail.util.logging.MailHandler] +contract={java.util.logging.Handler} +scope=javax.inject.Singleton
diff --git a/mail/src/main/resources/META-INF/javamail.charset.map b/mail/src/main/resources/META-INF/javamail.charset.map new file mode 100644 index 0000000..cc58d4f --- /dev/null +++ b/mail/src/main/resources/META-INF/javamail.charset.map
@@ -0,0 +1,77 @@ +### JDK-to-MIME charset mapping table #### +### This should be the first mapping table ### + +8859_1 ISO-8859-1 +iso8859_1 ISO-8859-1 +ISO8859-1 ISO-8859-1 + +8859_2 ISO-8859-2 +iso8859_2 ISO-8859-2 +ISO8859-2 ISO-8859-2 + +8859_3 ISO-8859-3 +iso8859_3 ISO-8859-3 +ISO8859-3 ISO-8859-3 + +8859_4 ISO-8859-4 +iso8859_4 ISO-8859-4 +ISO8859-4 ISO-8859-4 + +8859_5 ISO-8859-5 +iso8859_5 ISO-8859-5 +ISO8859-5 ISO-8859-5 + +8859_6 ISO-8859-6 +iso8859_6 ISO-8859-6 +ISO8859-6 ISO-8859-6 + +8859_7 ISO-8859-7 +iso8859_7 ISO-8859-7 +ISO8859-7 ISO-8859-7 + +8859_8 ISO-8859-8 +iso8859_8 ISO-8859-8 +ISO8859-8 ISO-8859-8 + +8859_9 ISO-8859-9 +iso8859_9 ISO-8859-9 +ISO8859-9 ISO-8859-9 + +SJIS Shift_JIS +JIS ISO-2022-JP +ISO2022JP ISO-2022-JP +EUC_JP euc-jp +KOI8_R koi8-r +EUC_CN euc-cn +EUC_TW euc-tw +EUC_KR euc-kr + +--DIVIDER: this line *must* start with "--" and end with "--" -- + +#### XXX-to-JDK charset mapping table #### + +iso-2022-cn ISO2022CN +iso-2022-kr ISO2022KR +utf-8 UTF8 +utf8 UTF8 +ja_jp.iso2022-7 ISO2022JP +ja_jp.eucjp EUCJIS + +# these two are not needed in 1.1.6. (since EUC_KR exists +# and KSC5601 will map to the correct converter) +euc-kr KSC5601 +euckr KSC5601 + +# in JDK 1.1.6 we will no longer need the "us-ascii" convert +us-ascii ISO-8859-1 +x-us-ascii ISO-8859-1 + +# Chinese charsets are a mess and widely misrepresented. +# gb18030 is a superset of gbk, which is a supserset of cp936/ms936, +# which is a superset of gb2312. +# https://bugzilla.gnome.org/show_bug.cgi?id=446783 +# map all of these to gb18030. +gb2312 GB18030 +cp936 GB18030 +ms936 GB18030 +gbk GB18030
diff --git a/mail/src/main/resources/META-INF/javamail.default.address.map b/mail/src/main/resources/META-INF/javamail.default.address.map new file mode 100644 index 0000000..4ab5572 --- /dev/null +++ b/mail/src/main/resources/META-INF/javamail.default.address.map
@@ -0,0 +1 @@ +rfc822=smtp
diff --git a/mail/src/main/resources/META-INF/javamail.default.providers b/mail/src/main/resources/META-INF/javamail.default.providers new file mode 100644 index 0000000..bedc076 --- /dev/null +++ b/mail/src/main/resources/META-INF/javamail.default.providers
@@ -0,0 +1,9 @@ +# JavaMail IMAP provider Oracle +protocol=imap; type=store; class=com.sun.mail.imap.IMAPStore; vendor=Oracle; +protocol=imaps; type=store; class=com.sun.mail.imap.IMAPSSLStore; vendor=Oracle; +# JavaMail SMTP provider Oracle +protocol=smtp; type=transport; class=com.sun.mail.smtp.SMTPTransport; vendor=Oracle; +protocol=smtps; type=transport; class=com.sun.mail.smtp.SMTPSSLTransport; vendor=Oracle; +# JavaMail POP3 provider Oracle +protocol=pop3; type=store; class=com.sun.mail.pop3.POP3Store; vendor=Oracle; +protocol=pop3s; type=store; class=com.sun.mail.pop3.POP3SSLStore; vendor=Oracle;
diff --git a/mail/src/main/resources/META-INF/mailcap b/mail/src/main/resources/META-INF/mailcap new file mode 100644 index 0000000..5259ae7 --- /dev/null +++ b/mail/src/main/resources/META-INF/mailcap
@@ -0,0 +1,16 @@ +# +# +# Default mailcap file for the JavaMail System. +# +# JavaMail content-handlers: +# +text/plain;; x-java-content-handler=com.sun.mail.handlers.text_plain +text/html;; x-java-content-handler=com.sun.mail.handlers.text_html +text/xml;; x-java-content-handler=com.sun.mail.handlers.text_xml +multipart/*;; x-java-content-handler=com.sun.mail.handlers.multipart_mixed; x-java-fallback-entry=true +message/rfc822;; x-java-content-handler=com.sun.mail.handlers.message_rfc822 +# +# can't support image types because java.awt.Toolkit doesn't work on servers +# +#image/gif;; x-java-content-handler=com.sun.mail.handlers.image_gif +#image/jpeg;; x-java-content-handler=com.sun.mail.handlers.image_jpeg
diff --git a/mail/src/main/resources/META-INF/services/javax.mail.Provider b/mail/src/main/resources/META-INF/services/javax.mail.Provider new file mode 100644 index 0000000..4688e5a --- /dev/null +++ b/mail/src/main/resources/META-INF/services/javax.mail.Provider
@@ -0,0 +1,6 @@ +com.sun.mail.imap.IMAPProvider +com.sun.mail.imap.IMAPSSLProvider +com.sun.mail.smtp.SMTPProvider +com.sun.mail.smtp.SMTPSSLProvider +com.sun.mail.pop3.POP3Provider +com.sun.mail.pop3.POP3SSLProvider
diff --git a/mail/src/main/resources/javax/mail/Version.java b/mail/src/main/resources/javax/mail/Version.java new file mode 100644 index 0000000..e1d9899 --- /dev/null +++ b/mail/src/main/resources/javax/mail/Version.java
@@ -0,0 +1,26 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +/** + * Package-private class that defines the version of JavaMail. + * This file is a template for the class that is generated + * at build time. + */ +class Version { + public static final String version = "${mail.version}"; +}
diff --git a/mail/src/oldtest/java/javax/mail/internet/decodetest b/mail/src/oldtest/java/javax/mail/internet/decodetest new file mode 100644 index 0000000..5d7f496 --- /dev/null +++ b/mail/src/oldtest/java/javax/mail/internet/decodetest
@@ -0,0 +1,4 @@ +#!/bin/sh +java decodetext < encodedheaders.data > x1 +java -Dmail.mime.decodetext.strict=false decodetext < encodedheaders.data > x2 +diff -c x1 x2
diff --git a/mail/src/oldtest/java/javax/mail/internet/encodedheaders.data b/mail/src/oldtest/java/javax/mail/internet/encodedheaders.data new file mode 100644 index 0000000..e853736 --- /dev/null +++ b/mail/src/oldtest/java/javax/mail/internet/encodedheaders.data
@@ -0,0 +1,28 @@ +=?ISO-8859-1?B?SWYgeW91IGNhbiByZWFkIHRoaXMgeW8=?= + =?ISO-8859-2?B?dSB1bmRlcnN0YW5kIHRoZSBleGFtcGxlLg==?= Congrats! +Hello World + Hello + World +=?ISO-8859-1?Q?Andr=E9?= Pirard <PIRARD@vm1.ulg.ac.be> +=?ISO-8859-1?Q?Keld_J=F8rn_Simonsen?= <keld@dkuug.dk> +=?ISO-8859-1?Q?Patrik_F=E4ltstr=F6m?= <paf@nada.kth.se> +=?iso-8859-8?b?7eXs+SDv4SDp7Oj08A==?= +=?ISO-2022-JP?B?GyRCQmdCPCEhWEYwbE86GyhK?= +[mizuki 1007] Re: Hajimemasite!! +Kazuyuki Murata <hiroko@hamakko.or.jp> +[mizuki 1009] =?ISO-2022-JP?B?GyRCJUYlbCVTPVAxaT5wSnMbKEI=?= +=?ISO-8859-1?Q?a?= +=?ISO-8859-1?Q?a?= b +=?ISO-8859-1?Q?a?= =?ISO-8859-1?Q?b?= +=?ISO-8859-1?Q?a?= =?ISO-8859-1?Q?b?= +=?ISO-8859-1?Q?a_b?= +=?ISO-8859-1?Q?a?= =?ISO-8859-2?Q?_b?= +=?US-ASCII?Q?Keith_Moore?= +=?ISO-8859-1?B?SWYgeW91IGNhbiByZWFkIHRoaXMgeW8=?= +=?ISO-8859-2?B?dSB1bmRlcnN0YW5kIHRoZSBleGFtcGxlLg==?= +=?ISO-8859-1?Q?Olle_J=E4rnefors?= + +babababa=?ISO-2022-JP?B?GyRCMj0kMSRrGyhK?= +babababa=?ISO-2022-JP?B?GyRCMj0kMSRrGyhK?=babababa +bababa =?ISO-2022-JP?B?IBskQjI9JDEkaxsoSg==?= +=?ISO-8859-1?Q?a?==?ISO-8859-1?Q?b?=
diff --git a/mail/src/oldtest/java/javax/mail/internet/messagecachetest.java b/mail/src/oldtest/java/javax/mail/internet/messagecachetest.java new file mode 100644 index 0000000..34e56a2 --- /dev/null +++ b/mail/src/oldtest/java/javax/mail/internet/messagecachetest.java
@@ -0,0 +1,302 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +import java.util.*; +import java.io.*; +import javax.mail.*; +import javax.mail.event.*; +import javax.mail.internet.*; +import javax.activation.*; + +/* + * Test IMAP message cache. + * + * @author Bill Shannon + */ + +public class messagecachetest { + + static String protocol; + static String host = null; + static String user = null; + static String password = null; + static String mbox = null; + static String url = null; + static int port = -1; + static boolean verbose = false; + static boolean debug = false; + static Session session; + + public static void main(String argv[]) { + int nummsg = 256; + int optind; + + for (optind = 0; optind < argv.length; optind++) { + if (argv[optind].equals("-T")) { + protocol = argv[++optind]; + } else if (argv[optind].equals("-H")) { + host = argv[++optind]; + } else if (argv[optind].equals("-U")) { + user = argv[++optind]; + } else if (argv[optind].equals("-P")) { + password = argv[++optind]; + } else if (argv[optind].equals("-v")) { + verbose = true; + } else if (argv[optind].equals("-D")) { + debug = true; + } else if (argv[optind].equals("-f")) { + mbox = argv[++optind]; + } else if (argv[optind].equals("-L")) { + url = argv[++optind]; + } else if (argv[optind].equals("-p")) { + port = Integer.parseInt(argv[++optind]); + } else if (argv[optind].equals("--")) { + optind++; + break; + } else if (argv[optind].startsWith("-")) { + System.out.println( +"Usage: messagecachetest [-L url] [-T protocol] [-H host] [-p port] [-U user]"); + System.out.println( +"\t[-P password] [-f mailbox] [msgnum] [-v] [-D]"); + System.exit(1); + } else { + break; + } + } + + try { + if (optind < argv.length) + nummsg = Integer.parseInt(argv[optind]); + + // Get a Properties object + Properties props = System.getProperties(); + + // Get a Session object + session = Session.getInstance(props, null); + session.setDebug(debug); + + // Get a Store object + Store store = null; + if (url != null) { + URLName urln = new URLName(url); + store = session.getStore(urln); + store.connect(); + } else { + if (protocol != null) + store = session.getStore(protocol); + else + store = session.getStore(); + + // Connect + if (host != null || user != null || password != null) + store.connect(host, port, user, password); + else + store.connect(); + } + + + // Open the Folder + + Folder folder = store.getDefaultFolder(); + if (folder == null) { + System.out.println("No default folder"); + System.exit(1); + } + + if (mbox == null) + mbox = "messagecachetest"; + folder = folder.getFolder(mbox); + if (folder == null) { + System.out.println("Invalid folder"); + System.exit(1); + } + + Message[] msgs = createMessages(nummsg); + if (folder.exists()) + folder.delete(false); + folder.create(Folder.HOLDS_MESSAGES); + + folder.open(Folder.READ_WRITE); + if (verbose) + System.out.println("fill folder"); + folder.appendMessages(msgs); + folder.close(false); + + folder.open(Folder.READ_WRITE); + if (verbose) + System.out.println("test message number"); + testMessageNumber(folder); + folder.close(false); + folder.open(Folder.READ_WRITE); + if (verbose) + System.out.println("test expunge forward"); + testExpungeForward(folder); + folder.close(false); + + folder.open(Folder.READ_WRITE); + folder.appendMessages(msgs); + folder.close(false); + folder.open(Folder.READ_WRITE); + if (verbose) + System.out.println("test expunge reverse"); + testExpungeReverse(folder); + folder.close(false); + + folder.open(Folder.READ_WRITE); + folder.appendMessages(msgs); + folder.close(false); + folder.open(Folder.READ_WRITE); + if (verbose) + System.out.println("test expunge random"); + testExpungeRandom(folder); + folder.close(false); + + folder.open(Folder.READ_WRITE); + folder.appendMessages(msgs); + folder.close(false); + folder.open(Folder.READ_WRITE); + if (verbose) + System.out.println("test expunge other"); + testExpungeOther(folder); + folder.close(false); + store.close(); + } catch (Exception ex) { + System.out.println("Oops, got exception! " + ex.getMessage()); + ex.printStackTrace(); + System.exit(1); + } + System.exit(0); + } + + private static Message[] createMessages(int num) throws MessagingException { + Message[] msgs = new Message[num]; + for (int i = 1; i <= num; i++) { + MimeMessage msg = new MimeMessage(session); + msg.setSentDate(new Date()); + msg.setFrom(); + msg.setRecipients(Message.RecipientType.TO, "nobody@nowhere.com"); + msg.setSubject(Integer.toString(i)); + msg.setText(i + "\n"); + msg.saveChanges(); + msgs[i-1] = msg; + } + return msgs; + } + + private static void testMessageNumber(Folder folder) + throws MessagingException { + int nummsg = folder.getMessageCount(); + Message msgs[] = new Message[nummsg]; + for (int i = 1; i <= nummsg; i++) { + Message msg = folder.getMessage(i); + msgs[i-1] = msg; + if (Integer.valueOf(msg.getSubject()) != i) { + System.out.println("FAIL: Wrong message! Got " + + msg.getSubject() + ", Expected " + i); + } + } + for (int i = 1; i <= nummsg; i++) { + Message msg = folder.getMessage(i); + if (msgs[i-1] != msg || msg.getMessageNumber() != i) { + System.out.println("FAIL: Wrong message! Got " + + msg.getMessageNumber() + ", Expected " + i); + } + } + } + + private static void testExpungeForward(Folder folder) + throws MessagingException { + int nummsg = folder.getMessageCount(); + for (int i = 1; i <= nummsg; i++) { + Message msg = folder.getMessage(1); + msg.setFlag(Flags.Flag.DELETED, true); + folder.expunge(); + for (int j = 1; j < nummsg - i; j++) { + msg = folder.getMessage(j); + if (msg.getMessageNumber() != j) { + System.out.println("FAIL: Wrong message! Got " + + msg.getMessageNumber() + ", Expected " + j); + } + } + } + } + + private static void testExpungeReverse(Folder folder) + throws MessagingException { + int nummsg = folder.getMessageCount(); + for (int i = nummsg; i >= 1; i--) { + Message msg = folder.getMessage(i); + msg.setFlag(Flags.Flag.DELETED, true); + folder.expunge(); + for (int j = 1; j < i; j++) { + msg = folder.getMessage(j); + if (msg.getMessageNumber() != j) { + System.out.println("FAIL: Wrong message! Got " + + msg.getMessageNumber() + ", Expected " + j); + } + } + } + } + + private static void testExpungeRandom(Folder folder) + throws MessagingException { + int nummsg = folder.getMessageCount(); + Random rnd = new Random(System.currentTimeMillis()); + while (nummsg > 0) { + Message msg = folder.getMessage(rnd.nextInt(nummsg) + 1); + msg.setFlag(Flags.Flag.DELETED, true); + folder.expunge(); + nummsg--; + for (int j = 1; j <= nummsg; j++) { + msg = folder.getMessage(j); + if (msg.getMessageNumber() != j) { + System.out.println("FAIL: Wrong message! Got " + + msg.getMessageNumber() + ", Expected " + j); + } + } + } + } + + private static void testExpungeOther(Folder folder) + throws MessagingException { + Folder other = folder.getStore().getFolder(folder.getFullName()); + other.open(Folder.READ_WRITE); + Message msg = other.getMessage(2); + msg.setFlag(Flags.Flag.DELETED, true); + other.expunge(); + System.out.println("waiting for expunge notification"); + try { Thread.sleep(2000); } catch (InterruptedException ex) { } + folder.isOpen(); + try { + msg = folder.getMessage(2); + msg.getFlags(); + if (!msg.isExpunged()) { + System.out.println("FAIL: Message 2 is not expunged!"); + } + } catch (MessageRemovedException mex) { + System.out.println("SUCCESS: message 2 is removed!"); + } + msg = folder.getMessage(1); + if (Integer.valueOf(msg.getSubject()) != 1) { + System.out.println("FAIL: Message 1 is wrong!"); + } + msg = folder.getMessage(3); + if (Integer.valueOf(msg.getSubject()) != 3) { + System.out.println("FAIL: Message 3 is wrong!"); + } + other.close(false); + } +}
diff --git a/mail/src/oldtest/java/javax/mail/internet/socketfactory/DummySSLSocketFactory.java b/mail/src/oldtest/java/javax/mail/internet/socketfactory/DummySSLSocketFactory.java new file mode 100644 index 0000000..12e3834 --- /dev/null +++ b/mail/src/oldtest/java/javax/mail/internet/socketfactory/DummySSLSocketFactory.java
@@ -0,0 +1,82 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; + +import javax.net.SocketFactory; +import javax.net.ssl.*; + + +/** + * DummySSLSocketFactory + */ +public class DummySSLSocketFactory extends SSLSocketFactory { + private SSLSocketFactory factory; + + public DummySSLSocketFactory() { + try { + SSLContext sslcontext = SSLContext.getInstance("TLS"); + sslcontext.init(null, + new TrustManager[] { new DummyTrustManager()}, + null); + factory = (SSLSocketFactory)sslcontext.getSocketFactory(); + } catch(Exception ex) { + // ignore + } + } + + public static SocketFactory getDefault() { + return new DummySSLSocketFactory(); + } + + public Socket createSocket() throws IOException { + TestResult.success(); + return factory.createSocket(); + } + + public Socket createSocket(Socket socket, String s, int i, boolean flag) + throws IOException { + TestResult.success(); + return factory.createSocket(socket, s, i, flag); + } + + public Socket createSocket(InetAddress inaddr, int i, + InetAddress inaddr1, int j) throws IOException { + TestResult.success(); + return factory.createSocket(inaddr, i, inaddr1, j); + } + + public Socket createSocket(InetAddress inaddr, int i) + throws IOException { + TestResult.success(); + return factory.createSocket(inaddr, i); + } + + public Socket createSocket(String s, int i, InetAddress inaddr, int j) + throws IOException { + TestResult.success(); + return factory.createSocket(s, i, inaddr, j); + } + + public Socket createSocket(String s, int i) throws IOException { + TestResult.success(); + return factory.createSocket(s, i); + } + + public String[] getDefaultCipherSuites() { + return factory.getDefaultCipherSuites(); + } + + public String[] getSupportedCipherSuites() { + return factory.getSupportedCipherSuites(); + } +}
diff --git a/mail/src/oldtest/java/javax/mail/internet/socketfactory/DummySocketFactory.java b/mail/src/oldtest/java/javax/mail/internet/socketfactory/DummySocketFactory.java new file mode 100644 index 0000000..aa24722 --- /dev/null +++ b/mail/src/oldtest/java/javax/mail/internet/socketfactory/DummySocketFactory.java
@@ -0,0 +1,59 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; + +import javax.net.SocketFactory; + + +/** + * DummySocketFactory + */ +public class DummySocketFactory extends SocketFactory { + private SocketFactory factory; + + public DummySocketFactory() { + factory = SocketFactory.getDefault(); + } + + public static SocketFactory getDefault() { + return new DummySocketFactory(); + } + + public Socket createSocket() throws IOException { + TestResult.success(); + return factory.createSocket(); + } + + public Socket createSocket(InetAddress inaddr, int i, + InetAddress inaddr1, int j) throws IOException { + TestResult.success(); + return factory.createSocket(inaddr, i, inaddr1, j); + } + + public Socket createSocket(InetAddress inaddr, int i) + throws IOException { + TestResult.success(); + return factory.createSocket(inaddr, i); + } + + public Socket createSocket(String s, int i, InetAddress inaddr, int j) + throws IOException { + TestResult.success(); + return factory.createSocket(s, i, inaddr, j); + } + + public Socket createSocket(String s, int i) throws IOException { + TestResult.success(); + return factory.createSocket(s, i); + } +}
diff --git a/mail/src/oldtest/java/javax/mail/internet/socketfactory/DummyTrustManager.java b/mail/src/oldtest/java/javax/mail/internet/socketfactory/DummyTrustManager.java new file mode 100644 index 0000000..4307623 --- /dev/null +++ b/mail/src/oldtest/java/javax/mail/internet/socketfactory/DummyTrustManager.java
@@ -0,0 +1,31 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +import javax.net.ssl.X509TrustManager; +import java.security.cert.X509Certificate; + + +/** + * DummyTrustManager + */ +public class DummyTrustManager implements X509TrustManager { + + public void checkClientTrusted(X509Certificate[] cert, String authType) { + // everything is trusted + } + + public void checkServerTrusted(X509Certificate[] cert, String authType) { + // everything is trusted + } + + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } +}
diff --git a/mail/src/oldtest/java/javax/mail/internet/socketfactory/TestResult.java b/mail/src/oldtest/java/javax/mail/internet/socketfactory/TestResult.java new file mode 100644 index 0000000..1946d0c --- /dev/null +++ b/mail/src/oldtest/java/javax/mail/internet/socketfactory/TestResult.java
@@ -0,0 +1,27 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +public class TestResult { + private static boolean pass = false; + + private TestResult() { } // no public constructor + + public static void reset() { + pass = false; + } + + public static void success() { + pass = true; + } + + public static void print(String s) { + System.out.println((pass ? "SUCCESS: " : "FAIL: ") + s); + } +}
diff --git a/mail/src/oldtest/java/javax/mail/internet/socketfactory/socketfactorytest.java b/mail/src/oldtest/java/javax/mail/internet/socketfactory/socketfactorytest.java new file mode 100644 index 0000000..caf94aa --- /dev/null +++ b/mail/src/oldtest/java/javax/mail/internet/socketfactory/socketfactorytest.java
@@ -0,0 +1,276 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +import java.io.*; +import java.util.Properties; + +import javax.mail.*; +import javax.mail.internet.*; + +import com.sun.mail.util.MailSSLSocketFactory; + +/** + * Test socket factory properties. + * Needs to be run with an SMTP server that supports both SSL + * connections and the STARTTLS command, e.g., mail-sfbay.sun.com. + * + * @author Bill Shannon + */ + +public class socketfactorytest { + + public static void main(String[] argv) { + String mailhost = "mail-sfbay.sun.com"; + String user = null, password = null; + boolean debug = false; + boolean auth = false; + int optind; + + for (optind = 0; optind < argv.length; optind++) { + if (argv[optind].equals("-U")) { + user = argv[++optind]; + } else if (argv[optind].equals("-P")) { + password = argv[++optind]; + } else if (argv[optind].equals("-M")) { + mailhost = argv[++optind]; + } else if (argv[optind].equals("-d")) { + debug = true; + } else if (argv[optind].equals("-A")) { + auth = true; + } else if (argv[optind].equals("--")) { + optind++; + break; + } else if (argv[optind].startsWith("-")) { + System.out.println( + "Usage: socketfactorytest [-U user] [-P passwd]]"); + System.out.println("\t[-M transport-host] [-d] [-A]"); + System.exit(1); + } else { + break; + } + } + + try { + + // first, no factories + + Properties props = new Properties(); + props.put("mail.smtp.host", mailhost); + Session session = Session.getInstance(props, null); + if (debug) + session.setDebug(true); + + Transport t = session.getTransport("smtp"); + try { + if (auth) + t.connect(mailhost, user, password); + else + t.connect(); + TestResult.success(); + } finally { + t.close(); + } + TestResult.print("no factories"); + + // socket factory property + + TestResult.reset(); + props = new Properties(); + props.put("mail.smtp.host", mailhost); + props.put("mail.smtp.socketFactory.class", "DummySocketFactory"); + session = Session.getInstance(props, null); + if (debug) + session.setDebug(true); + + t = session.getTransport("smtp"); + try { + if (auth) + t.connect(mailhost, user, password); + else + t.connect(); + } finally { + t.close(); + } + TestResult.print("socket factory property"); + + // socket factory object + + TestResult.reset(); + props = new Properties(); + props.put("mail.smtp.host", mailhost); + props.put("mail.smtp.socketFactory", new DummySocketFactory()); + session = Session.getInstance(props, null); + if (debug) + session.setDebug(true); + + t = session.getTransport("smtp"); + try { + if (auth) + t.connect(mailhost, user, password); + else + t.connect(); + } finally { + t.close(); + } + TestResult.print("socket factory object"); + + // SSL socket factory property + + TestResult.reset(); + props = new Properties(); + props.put("mail.smtp.host", mailhost); + props.put("mail.smtp.ssl.enable", "true"); + props.put("mail.smtp.ssl.checkserveridentity", "true"); + props.put("mail.smtp.ssl.socketFactory.class", + "DummySSLSocketFactory"); + session = Session.getInstance(props, null); + if (debug) + session.setDebug(true); + + t = session.getTransport("smtp"); + try { + if (auth) + t.connect(mailhost, user, password); + else + t.connect(); + } finally { + t.close(); + } + TestResult.print("SSL socket factory property"); + + // SSL socket factory object + + TestResult.reset(); + props = new Properties(); + props.put("mail.smtp.host", mailhost); + props.put("mail.smtp.ssl.enable", "true"); + props.put("mail.smtp.ssl.checkserveridentity", "true"); + props.put("mail.smtp.ssl.socketFactory", + new DummySSLSocketFactory()); + session = Session.getInstance(props, null); + if (debug) + session.setDebug(true); + + t = session.getTransport("smtp"); + try { + if (auth) + t.connect(mailhost, user, password); + else + t.connect(); + } finally { + t.close(); + } + TestResult.print("SSL socket factory object"); + + // STARTTLS no socket factory + + TestResult.reset(); + props = new Properties(); + props.put("mail.smtp.host", mailhost); + props.put("mail.smtp.starttls.enable", "true"); + session = Session.getInstance(props, null); + if (debug) + session.setDebug(true); + + t = session.getTransport("smtp"); + try { + if (auth) + t.connect(mailhost, user, password); + else + t.connect(); + TestResult.success(); + } finally { + t.close(); + } + TestResult.print("STARTTLS no socket factory"); + + // STARTTLS SSL socket factory property + + TestResult.reset(); + props = new Properties(); + props.put("mail.smtp.host", mailhost); + props.put("mail.smtp.starttls.enable", "true"); + props.put("mail.smtp.ssl.checkserveridentity", "true"); + props.put("mail.smtp.ssl.socketFactory.class", + "DummySSLSocketFactory"); + session = Session.getInstance(props, null); + if (debug) + session.setDebug(true); + + t = session.getTransport("smtp"); + try { + if (auth) + t.connect(mailhost, user, password); + else + t.connect(); + } finally { + t.close(); + } + TestResult.print("STARTTLS SSL socket factory property"); + + // STARTTLS SSL socket factory object + + TestResult.reset(); + props = new Properties(); + props.put("mail.smtp.host", mailhost); + props.put("mail.smtp.starttls.enable", "true"); + props.put("mail.smtp.ssl.checkserveridentity", "true"); + props.put("mail.smtp.ssl.socketFactory", + new DummySSLSocketFactory()); + session = Session.getInstance(props, null); + if (debug) + session.setDebug(true); + + t = session.getTransport("smtp"); + try { + if (auth) + t.connect(mailhost, user, password); + else + t.connect(); + } finally { + t.close(); + } + TestResult.print("STARTTLS SSL socket factory object"); + + // Mail SSL socket factory object + + /* + TestResult.reset(); + props = new Properties(); + props.put("mail.imap.host", mailhost); + props.put("mail.imap.ssl.enable", "true"); + props.put("mail.imap.ssl.checkserveridentity", "true"); + MailSSLSocketFactory sf = new MailSSLSocketFactory("TLS"); + //sf.setTrustAllHosts(true); + sf.setTrustedHosts(new String[] { "loghost" }); + props.put("mail.imap.ssl.socketFactory", sf); + props.put("mail.imap.socketFactory.fallback", "false"); + session = Session.getInstance(props, null); + if (debug) + session.setDebug(true); + + Store st = session.getStore("imap"); + try { + if (auth) + st.connect(mailhost, user, password); + else + st.connect(); + TestResult.success(); + } finally { + st.close(); + } + TestResult.print("Mail SSL socket factory object"); + */ + + } catch (Exception e) { + System.out.println("Exception: " + e); + } + } +}
diff --git a/mail/src/test/java/com/sun/mail/handlers/TextXmlTest.java b/mail/src/test/java/com/sun/mail/handlers/TextXmlTest.java new file mode 100644 index 0000000..26c8123 --- /dev/null +++ b/mail/src/test/java/com/sun/mail/handlers/TextXmlTest.java
@@ -0,0 +1,121 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.handlers; + +import java.io.*; +import java.util.*; +import java.awt.datatransfer.DataFlavor; +import javax.activation.*; +import javax.mail.*; +import javax.mail.util.ByteArrayDataSource; +import javax.xml.transform.stream.*; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * Test the text/xml DataContentHandler. + * + * XXX - should test other Source objects in addition to StreamSource. + * + * @author Bill Shannon + */ + +public class TextXmlTest { + + private static String xml = "<test><foo>bar</foo></test>\n"; + private static byte[] xmlBytes = xml.getBytes(); + + // test InputStream to String + @Test + public void testStreamToStringTextXml() throws Exception { + testStreamToString("text/xml"); + } + + // test InputStream to String + @Test + public void testStreamToStringApplicationXml() throws Exception { + testStreamToString("application/xml"); + } + + private static void testStreamToString(String mimeType) throws Exception { + DataContentHandler dch = new text_xml(); + DataFlavor df = new ActivationDataFlavor(String.class, mimeType, "XML"); + DataSource ds = new ByteArrayDataSource(xmlBytes, mimeType); + Object content = dch.getContent(ds); + assertEquals(String.class, content.getClass()); + assertEquals(xml, (String)content); + content = dch.getTransferData(df, ds); + assertEquals(String.class, content.getClass()); + assertEquals(xml, (String)content); + } + + // test InputStream to StreamSource + @Test + public void testStreamToSource() throws Exception { + DataContentHandler dch = new text_xml(); + DataFlavor df = new ActivationDataFlavor(StreamSource.class, + "text/xml", "XML stream"); + DataSource ds = new ByteArrayDataSource(xmlBytes, "text/xml"); + Object content = dch.getTransferData(df, ds); + assertEquals(StreamSource.class, content.getClass()); + String sc = streamToString(((StreamSource)content).getInputStream()); + assertEquals(xml, sc); + } + + // test String to OutputStream + @Test + public void testStringToStream() throws Exception { + DataContentHandler dch = new text_xml(); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + dch.writeTo(xml, "text/xml", bos); + String sc = new String(bos.toByteArray(), "us-ascii"); + assertEquals(xml, sc); + } + + // test StreamSource to OutputStream + @Test + public void testSourceToStream() throws Exception { + DataContentHandler dch = new text_xml(); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + StreamSource ss = new StreamSource(new ByteArrayInputStream(xmlBytes)); + dch.writeTo(ss, "text/xml", bos); + String sc = new String(bos.toByteArray(), "us-ascii"); + // transformer adds an <?xml> header, so can't check for exact match + assertTrue(sc.indexOf(xml.trim()) >= 0); + } + + /** + * Read a stream into a String. + */ + private static String streamToString(InputStream is) { + try { + StringBuilder sb = new StringBuilder(); + int c; + while ((c = is.read()) > 0) + sb.append((char)c); + return sb.toString(); + } catch (IOException ex) { + return ""; + } finally { + try { + is.close(); + } catch (IOException cex) { } + } + } +}
diff --git a/mail/src/test/java/com/sun/mail/iap/ProtocolTest.java b/mail/src/test/java/com/sun/mail/iap/ProtocolTest.java new file mode 100644 index 0000000..53f57b2 --- /dev/null +++ b/mail/src/test/java/com/sun/mail/iap/ProtocolTest.java
@@ -0,0 +1,85 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.iap; + +import java.io.*; +import java.util.Properties; + +import com.sun.mail.test.NullOutputStream; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; + +/** + * Test the Protocol class. + */ +public final class ProtocolTest { + + private static final byte[] noBytes = new byte[0]; + private static final PrintStream nullps = + new PrintStream(new NullOutputStream()); + private static final ByteArrayInputStream nullis = + new ByteArrayInputStream(noBytes); + + /** + * Test that the tag prefix is computed properly. + */ + @Test + public void testTagPrefix() throws IOException, ProtocolException { + Protocol.tagNum.set(0); // reset for testing + String tag = newProtocolTag(); + assertEquals("A0", tag); + for (int i = 1; i < 26; i++) + tag = newProtocolTag(); + assertEquals("Z0", tag); + tag = newProtocolTag(); + assertEquals("AA0", tag); + for (int i = 26 + 1; i < (26*26 + 26); i++) + tag = newProtocolTag(); + assertEquals("ZZ0", tag); + tag = newProtocolTag(); + assertEquals("AAA0", tag); + for (int i = 26*26 + 26 + 1; i < (26*26*26 + 26*26 + 26); i++) + tag = newProtocolTag(); + assertEquals("ZZZ0", tag); + tag = newProtocolTag(); + // did it wrap around? + assertEquals("A0", tag); + } + + private String newProtocolTag() throws IOException, ProtocolException { + Properties props = new Properties(); + Protocol p = new Protocol(nullis, nullps, props, false); + String tag = p.writeCommand("CMD", null); + return tag; + } + + /** + * Test that the tag prefix is reused. + */ + @Test + public void testTagPrefixReuse() throws IOException, ProtocolException { + Properties props = new Properties(); + props.setProperty("mail.imap.reusetagprefix", "true"); + Protocol p = new Protocol(nullis, nullps, props, false); + String tag = p.writeCommand("CMD", null); + assertEquals("A0", tag); + p = new Protocol(nullis, nullps, props, false); + tag = p.writeCommand("CMD", null); + assertEquals("A0", tag); + } +}
diff --git a/mail/src/test/java/com/sun/mail/iap/ResponseInputStreamTest.java b/mail/src/test/java/com/sun/mail/iap/ResponseInputStreamTest.java new file mode 100644 index 0000000..dfdb0c3 --- /dev/null +++ b/mail/src/test/java/com/sun/mail/iap/ResponseInputStreamTest.java
@@ -0,0 +1,46 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.iap; + +import java.io.ByteArrayInputStream; +import java.io.IOException; + +import static org.junit.Assert.fail; +import org.junit.Test; + +/** + * Test ResponseInputStream. + */ +public class ResponseInputStreamTest { + + /** + * Test that an EOF while reading a literal throws an IOException. + */ + @Test + public void testEofWhileReadingLiteral() throws Exception { + ByteArrayInputStream bis = new ByteArrayInputStream( + "test{1}\r\n".getBytes("ISO-8859-1")); + ResponseInputStream ris = new ResponseInputStream(bis); + try { + ris.readResponse(); + } catch (IOException ex) { + // success! + return; + } + fail("no exception"); + } +}
diff --git a/mail/src/test/java/com/sun/mail/iap/ResponseTest.java b/mail/src/test/java/com/sun/mail/iap/ResponseTest.java new file mode 100644 index 0000000..d75ead4 --- /dev/null +++ b/mail/src/test/java/com/sun/mail/iap/ResponseTest.java
@@ -0,0 +1,168 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.iap; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertArrayEquals; +import org.junit.Test; +import org.junit.Rule; +import org.junit.rules.Timeout; + +/** + * Test response parsing. + */ +public class ResponseTest { + // timeout the test in case of infinite loop + @Rule + public Timeout timeout = Timeout.seconds(5); + + private static String[] atomTests = { + "atom", "atom ", "atom(", "atom)", "atom{", "atom*", "atom%", + "atom\"", "atom\\ ", "atom]", "atom\001", "atom\177" + }; + + private static String[] astringTests = { + "atom", "atom ", "atom(", "atom)", "atom{", "atom*", "atom%", + "atom\"", "atom\\ ", "atom\001", "atom\177", "\"atom\"", + "{4}\r\natom" + }; + + /** + * Test parsing atoms. + */ + @Test + public void testAtom() throws Exception { + for (String s : atomTests) { + Response r = new Response("* " + s); + assertEquals("atom", r.readAtom()); + } + for (String s : atomTests) { + Response r = new Response("* " + s + " "); + assertEquals("atom", r.readAtom()); + } + } + + /** + * Test parsing astrings. + */ + @Test + public void testAString() throws Exception { + for (String s : astringTests) { + Response r = new Response("* " + s); + assertEquals("atom", r.readAtomString()); + } + for (String s : astringTests) { + Response r = new Response("* " + s + " "); + assertEquals("atom", r.readAtomString()); + } + } + + /** + * Test the special case where an astring can include ']'. + */ + @Test + public void testAStringSpecial() throws Exception { + Response r = new Response("* " + "atom] "); + assertEquals("atom]", r.readAtomString()); + } + + /** + * Test astring lists. + */ + @Test + public void testAStringList() throws Exception { + Response r = new Response("* " + "(A B C)"); + assertArrayEquals(new String[] { "A", "B", "C" }, + r.readAtomStringList()); + } + + @Test + public void testAStringListInitialSpace() throws Exception { + Response r = new Response("* " + "( A B C)"); + assertArrayEquals(new String[] { "A", "B", "C" }, + r.readAtomStringList()); + } + + @Test + public void testAStringListTrailingSpace() throws Exception { + Response r = new Response("* " + "(A B C )"); + assertArrayEquals(new String[] { "A", "B", "C" }, + r.readAtomStringList()); + } + + @Test + public void testAStringListInitialAndTrailingSpace() throws Exception { + Response r = new Response("* " + "( A B C )"); + assertArrayEquals(new String[] { "A", "B", "C" }, + r.readAtomStringList()); + } + + @Test + public void testAStringListMultipleSpaces() throws Exception { + Response r = new Response("* " + "(A B C)"); + assertArrayEquals(new String[] { "A", "B", "C" }, + r.readAtomStringList()); + } + + @Test + public void testAStringListQuoted() throws Exception { + Response r = new Response("* " + "(A B \"C\")"); + assertArrayEquals(new String[] { "A", "B", "C" }, + r.readAtomStringList()); + } + + /** + * Test astring lists with more data following. + */ + @Test + public void testAStringListMore() throws Exception { + Response r = new Response("* " + "(A B \"C\") atom"); + assertArrayEquals(new String[] { "A", "B", "C" }, + r.readAtomStringList()); + assertEquals("atom", r.readAtomString()); + } + + /** + * Test empty astring lists. + */ + @Test + public void testAStringListEmpty() throws Exception { + Response r = new Response("* " + "()"); + assertArrayEquals(new String[0], r.readAtomStringList()); + } + + /** + * Test empty astring lists with more data following. + */ + @Test + public void testAStringListEmptyMore() throws Exception { + Response r = new Response("* " + "() atom"); + assertArrayEquals(new String[0], r.readAtomStringList()); + assertEquals("atom", r.readAtomString()); + } + + /** + * Test readStringList + */ + @Test + public void testBadStringList() throws Exception { + Response response = new Response( + "* (\"name\", \"test\", \"version\", \"1.0\")"); + String[] list = response.readStringList(); + // anything other than an infinite loop timeout is considered success + } +}
diff --git a/mail/src/test/java/com/sun/mail/imap/IMAPAlertTest.java b/mail/src/test/java/com/sun/mail/imap/IMAPAlertTest.java new file mode 100644 index 0000000..cb351d6 --- /dev/null +++ b/mail/src/test/java/com/sun/mail/imap/IMAPAlertTest.java
@@ -0,0 +1,104 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.io.IOException; +import java.util.Properties; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import javax.mail.Session; +import javax.mail.Store; +import javax.mail.event.StoreListener; +import javax.mail.event.StoreEvent; + +import com.sun.mail.test.TestServer; + +import org.junit.Test; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * Test alerts. + */ +public final class IMAPAlertTest { + + private volatile boolean gotAlert = false; + + @Test + public void test() { + TestServer server = null; + try { + final IMAPHandler handler = new IMAPHandlerAlert(); + server = new TestServer(handler); + server.start(); + + final Properties properties = new Properties(); + properties.setProperty("mail.imap.host", "localhost"); + properties.setProperty("mail.imap.port", "" + server.getPort()); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + final CountDownLatch latch = new CountDownLatch(1); + + final Store store = session.getStore("imap"); + store.addStoreListener(new StoreListener() { + @Override + public void notification(StoreEvent e) { + String s; + if (e.getMessageType() == StoreEvent.ALERT) { + s = "ALERT: "; + gotAlert = true; + latch.countDown(); + } else + s = "NOTICE: "; + //System.out.println(s + e.getMessage()); + } + }); + try { + store.connect("test", "test"); + // time for event to be delivered + latch.await(5, TimeUnit.SECONDS); + assertTrue(gotAlert); + + } catch (Exception ex) { + System.out.println(ex); + //ex.printStackTrace(); + fail(ex.toString()); + } finally { + store.close(); + } + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + } + } + } + + /** + * Custom handler. Returns an alert message at login. + */ + private static final class IMAPHandlerAlert extends IMAPHandler { + @Override + public void login() throws IOException { + untagged("OK [ALERT] account is over quota"); + super.login(); + } + } +}
diff --git a/mail/src/test/java/com/sun/mail/imap/IMAPAuthDebugTest.java b/mail/src/test/java/com/sun/mail/imap/IMAPAuthDebugTest.java new file mode 100644 index 0000000..a1ff4fb --- /dev/null +++ b/mail/src/test/java/com/sun/mail/imap/IMAPAuthDebugTest.java
@@ -0,0 +1,132 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.io.*; +import java.util.Properties; + +import javax.mail.Session; +import javax.mail.Store; + +import com.sun.mail.test.TestServer; + +import org.junit.Test; +import org.junit.Rule; +import org.junit.rules.Timeout; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.fail; + +/** + * Test that authentication information is only included in + * the debug output when explicitly requested by setting the + * property "mail.debug.auth" to "true". + * + * XXX - should test all authentication types, but that requires + * more work in the dummy test server. + */ +public final class IMAPAuthDebugTest { + + // timeout the test in case of deadlock + @Rule + public Timeout deadlockTimeout = Timeout.seconds(20); + + /** + * Test that authentication information isn't included in the debug output. + */ + @Test + public void testNoAuthDefault() { + final Properties properties = new Properties(); + assertFalse("LOGIN in debug output", test(properties, "LOGIN")); + } + + @Test + public void testNoAuth() { + final Properties properties = new Properties(); + properties.setProperty("mail.debug.auth", "false"); + assertFalse("LOGIN in debug output", test(properties, "LOGIN")); + } + + /** + * Test that authentication information *is* included in the debug output. + */ + @Test + public void testAuth() { + final Properties properties = new Properties(); + properties.setProperty("mail.debug.auth", "true"); + assertTrue("LOGIN in debug output", test(properties, "LOGIN")); + } + + /** + * Create a test server, connect to it, and collect the debug output. + * Scan the debug output looking for "expect", return true if found. + */ + public boolean test(Properties properties, String expect) { + TestServer server = null; + try { + final IMAPHandler handler = new IMAPHandler(); + server = new TestServer(handler); + server.start(); + + properties.setProperty("mail.imap.host", "localhost"); + properties.setProperty("mail.imap.port", "" + server.getPort()); + final Session session = Session.getInstance(properties); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + PrintStream ps = new PrintStream(bos); + session.setDebugOut(ps); + session.setDebug(true); + + final Store store = session.getStore("imap"); + try { + store.connect("test", "test"); + } catch (Exception ex) { + System.out.println(ex); + //ex.printStackTrace(); + fail(ex.toString()); + } finally { + store.close(); + } + + ps.close(); + bos.close(); + ByteArrayInputStream bis = + new ByteArrayInputStream(bos.toByteArray()); + BufferedReader r = new BufferedReader( + new InputStreamReader(bis, "us-ascii")); + String line; + boolean found = false; + while ((line = r.readLine()) != null) { + if (line.startsWith("DEBUG")) + continue; + if (line.startsWith("*")) + continue; + if (line.indexOf(expect) >= 0) + found = true; + } + r.close(); + return found; + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + return false; // XXX - doesn't matter + } finally { + if (server != null) { + server.quit(); + } + } + } +}
diff --git a/mail/src/test/java/com/sun/mail/imap/IMAPCloseFailureTest.java b/mail/src/test/java/com/sun/mail/imap/IMAPCloseFailureTest.java new file mode 100644 index 0000000..eacb68f --- /dev/null +++ b/mail/src/test/java/com/sun/mail/imap/IMAPCloseFailureTest.java
@@ -0,0 +1,114 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.io.*; +import java.util.Properties; + +import javax.mail.Session; +import javax.mail.Store; +import javax.mail.Folder; + +import com.sun.mail.test.TestServer; + +import org.junit.Test; +import static org.junit.Assert.fail; + +/** + * Test that failures while closing a folder are handled properly. + */ +public final class IMAPCloseFailureTest { + + private static final String HOST = "localhost"; + + static class NoIMAPHandler extends IMAPHandler { + static boolean first = true; + + @Override + public void examine(String line) throws IOException { + if (first) + no("mailbox gone"); + else + super.examine(line); + first = false; + } + } + + static class BadIMAPHandler extends IMAPHandler { + static boolean first = true; + + @Override + public void examine(String line) throws IOException { + if (first) + bad("mailbox gone"); + else + super.examine(line); + first = false; + } + } + + @Test + public void testCloseNo() { + testClose(new NoIMAPHandler()); + } + + @Test + public void testCloseBad() { + testClose(new BadIMAPHandler()); + } + + public void testClose(IMAPHandler handler) { + TestServer server = null; + try { + server = new TestServer(handler); + server.start(); + + Properties properties = new Properties(); + properties.setProperty("mail.imap.host", HOST); + properties.setProperty("mail.imap.port", "" + server.getPort()); + Session session = Session.getInstance(properties); + //session.setDebug(true); + + Store store = session.getStore("imap"); + try { + store.connect("test", "test"); + Folder f = store.getFolder("INBOX"); + f.open(Folder.READ_WRITE); + f.close(false); + // Make sure that failure while closing doesn't leave us + // with a connection that can't be used to open a folder. + f.open(Folder.READ_WRITE); + f.close(false); + } catch (Exception ex) { + System.out.println(ex); + //ex.printStackTrace(); + fail(ex.toString()); + } finally { + if (store.isConnected()) + store.close(); + } + + } catch (Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + } + } + } +}
diff --git a/mail/src/test/java/com/sun/mail/imap/IMAPConnectFailureTest.java b/mail/src/test/java/com/sun/mail/imap/IMAPConnectFailureTest.java new file mode 100644 index 0000000..d6827df --- /dev/null +++ b/mail/src/test/java/com/sun/mail/imap/IMAPConnectFailureTest.java
@@ -0,0 +1,138 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.io.*; +import java.util.Properties; +import java.net.ServerSocket; + +import javax.mail.Session; +import javax.mail.Store; +import javax.mail.MessagingException; + +import com.sun.mail.iap.ConnectionException; + +import com.sun.mail.test.TestServer; +import com.sun.mail.util.MailConnectException; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * Test connect failure. + */ +public final class IMAPConnectFailureTest { + + private static final String HOST = "localhost"; + private static final int CTO = 20; + + @Test + public void testNoServer() { + try { + // verify that port is not being used + ServerSocket ss = new ServerSocket(0); + int port = ss.getLocalPort(); + ss.close(); + + Properties properties = new Properties(); + properties.setProperty("mail.imap.host", HOST); + properties.setProperty("mail.imap.port", "" + port); + properties.setProperty("mail.imap.connectiontimeout", "" + CTO); + Session session = Session.getInstance(properties); + //session.setDebug(true); + + Store store = session.getStore("imap"); + try { + store.connect("test", "test"); + fail("Connected!"); + // failure! + } catch (MailConnectException mcex) { + // success! + assertEquals(HOST, mcex.getHost()); + assertEquals(port, mcex.getPort()); + assertEquals(CTO, mcex.getConnectionTimeout()); + } catch (Exception ex) { + System.out.println(ex); + //ex.printStackTrace(); + fail(ex.toString()); + } finally { + if (store.isConnected()) + store.close(); + } + + } catch (Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } + } + + /** + * Test that a disconnect after issuing the CAPABILITY command + * results in a ConnectionException. + */ + @Test + public void testCapabilityDisconnect() { + TestServer server = null; + try { + final IMAPHandler handler = new IMAPHandler() { + @Override + public void sendGreetings() throws IOException { + untagged("OK IMAPHandler"); + } + + @Override + public void capability() throws IOException { + exit(); + } + }; + server = new TestServer(handler); + server.start(); + + Properties properties = new Properties(); + properties.setProperty("mail.imap.host", "localhost"); + properties.setProperty("mail.imap.port", "" + server.getPort()); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + final Store store = session.getStore("imap"); + try { + store.connect("test", "test"); + fail("connect did not fail"); + } catch (MessagingException mex) { + // this is what we expect, now check that it was caused by + // the right exception + assertTrue(mex.getCause() instanceof ConnectionException); + } catch (Exception ex) { + System.out.println(ex); + //ex.printStackTrace(); + fail(ex.toString()); + } finally { + store.close(); + } + + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + } + } + } +}
diff --git a/mail/src/test/java/com/sun/mail/imap/IMAPFetchProfileTest.java b/mail/src/test/java/com/sun/mail/imap/IMAPFetchProfileTest.java new file mode 100644 index 0000000..2b933d9 --- /dev/null +++ b/mail/src/test/java/com/sun/mail/imap/IMAPFetchProfileTest.java
@@ -0,0 +1,204 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.io.IOException; +import java.util.Properties; +import java.util.StringTokenizer; +import java.util.Set; +import java.util.HashSet; + +import javax.mail.Folder; +import javax.mail.Session; +import javax.mail.Store; +import javax.mail.Message; +import javax.mail.MessagingException; +import javax.mail.FetchProfile; + +import com.sun.mail.test.TestServer; + +import org.junit.Test; +import org.junit.Rule; +import org.junit.rules.Timeout; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.fail; + +/** + * Test IMAP FetchProfile items. + */ +public final class IMAPFetchProfileTest { + + // timeout the test in case of deadlock + @Rule + public Timeout deadlockTimeout = Timeout.seconds(20); + + private static final String RDATE = "23-Jun-2004 06:26:26 -0700"; + private static final String ENVELOPE = + "(\"Wed, 23 Jun 2004 18:56:42 +0530\" \"test\" " + + "((\"JavaMail\" NIL \"testuser\" \"example.com\")) " + + "((\"JavaMail\" NIL \"testuser\" \"example.com\")) " + + "((\"JavaMail\" NIL \"testuser\" \"example.com\")) " + + "((NIL NIL \"testuser\" \"example.com\")) NIL NIL NIL " + + "\"<40D98512.9040803@example.com>\")"; + + public static interface IMAPTest { + public void test(Folder folder, IMAPHandlerFetch handler) + throws MessagingException; + } + + @Test + public void testINTERNALDATEFetch() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Folder folder, IMAPHandlerFetch handler) + throws MessagingException { + FetchProfile fp = new FetchProfile(); + fp.add(IMAPFolder.FetchProfileItem.INTERNALDATE); + Message m = folder.getMessage(1); + folder.fetch(new Message[] { m }, fp); + assertTrue(handler.saw("INTERNALDATE")); + handler.reset(); + assertTrue(m.getReceivedDate() != null); + assertFalse(handler.saw("INTERNALDATE")); + } + }, + new IMAPHandlerFetch() { + @Override + public void fetch(String line) throws IOException { + if (line.indexOf("INTERNALDATE") >= 0) + saw.add("INTERNALDATE"); + untagged("1 FETCH (INTERNALDATE \"" + RDATE + "\")"); + ok(); + } + }); + } + + @Test + public void testINTERNALDATEFetchEnvelope() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Folder folder, IMAPHandlerFetch handler) + throws MessagingException { + FetchProfile fp = new FetchProfile(); + fp.add(FetchProfile.Item.ENVELOPE); + Message m = folder.getMessage(1); + folder.fetch(new Message[] { m }, fp); + assertTrue(handler.saw("INTERNALDATE")); + handler.reset(); + assertTrue(m.getReceivedDate() != null); + assertFalse(handler.saw("INTERNALDATE")); + } + }, + new IMAPHandlerFetch() { + @Override + public void fetch(String line) throws IOException { + if (line.indexOf("INTERNALDATE") >= 0) + saw.add("INTERNALDATE"); + untagged("1 FETCH (INTERNALDATE \"" + RDATE + "\")"); + ok(); + } + }); + } + + @Test + public void testINTERNALDATENoFetch() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Folder folder, IMAPHandlerFetch handler) + throws MessagingException { + Message m = folder.getMessage(1); + assertTrue(m.getReceivedDate() != null); + assertTrue(handler.saw("INTERNALDATE")); + } + }, + new IMAPHandlerFetch() { + @Override + public void fetch(String line) throws IOException { + if (line.indexOf("INTERNALDATE") >= 0) + saw.add("INTERNALDATE"); + untagged("1 FETCH (ENVELOPE " + ENVELOPE + + " INTERNALDATE \"" + RDATE + "\" RFC822.SIZE 0)"); + ok(); + } + }); + } + + public void testWithHandler(IMAPTest test, IMAPHandlerFetch handler) { + TestServer server = null; + try { + server = new TestServer(handler); + server.start(); + + final Properties properties = new Properties(); + properties.setProperty("mail.imap.host", "localhost"); + properties.setProperty("mail.imap.port", "" + server.getPort()); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + final Store store = session.getStore("imap"); + Folder folder = null; + try { + store.connect("test", "test"); + folder = store.getFolder("INBOX"); + folder.open(Folder.READ_WRITE); + test.test(folder, handler); + } catch (Exception ex) { + System.out.println(ex); + //ex.printStackTrace(); + fail(ex.toString()); + } finally { + if (folder != null) + folder.close(false); + store.close(); + } + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + } + } + } + + /** + * Custom handler. + */ + private static class IMAPHandlerFetch extends IMAPHandler { + // must be static because handler is cloned for each connection + protected static Set<String> saw = new HashSet<>(); + + @Override + public void select(String line) throws IOException { + numberOfMessages = 1; + super.select(line); + } + + public boolean saw(String item) { + return saw.contains(item); + } + + public void reset() { + saw.clear(); + } + } +}
diff --git a/mail/src/test/java/com/sun/mail/imap/IMAPFolderTest.java b/mail/src/test/java/com/sun/mail/imap/IMAPFolderTest.java new file mode 100644 index 0000000..8f2c9c5 --- /dev/null +++ b/mail/src/test/java/com/sun/mail/imap/IMAPFolderTest.java
@@ -0,0 +1,527 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.io.IOException; +import java.util.Properties; +import java.util.StringTokenizer; + +import javax.mail.Folder; +import javax.mail.Session; +import javax.mail.Store; +import javax.mail.MessagingException; +import javax.mail.UIDFolder; + +import com.sun.mail.test.TestServer; + +import org.junit.Test; +import org.junit.Rule; +import org.junit.rules.Timeout; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.fail; + +/** + * Test IMAPFolder methods. + */ +public final class IMAPFolderTest { + + // timeout the test in case of deadlock + @Rule + public Timeout deadlockTimeout = Timeout.seconds(20); + + private static final String utf8Folder = "test\u03b1"; + private static final String utf7Folder = "test&A7E-"; + + public static abstract class IMAPTest { + public void init(Properties props) { }; + public abstract void test(Store store, IMAPHandler handler) + throws Exception; + } + + /** + * Test that using a UTF-8 folder name results in the proper UTF-7 + * encoded name for the CREATE command. + */ + @Test + public void testUtf7FolderNameCreate() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Store store, IMAPHandler handler) + throws MessagingException, IOException { + Folder test = store.getFolder(utf8Folder); + assertTrue(test.create(Folder.HOLDS_MESSAGES)); + } + }, + new IMAPHandler() { + @Override + public void create(String line) throws IOException { + StringTokenizer st = new StringTokenizer(line); + st.nextToken(); // skip tag + st.nextToken(); // skip "CREATE" + String name = st.nextToken(); + if (name.equals(utf7Folder)) + ok(); + else + no("wrong name"); + } + + @Override + public void list(String line) throws IOException { + untagged("LIST (\\HasNoChildren) \"/\" " + utf7Folder); + ok(); + } + }); + } + + /** + * Test that using a UTF-8 folder name results in the proper UTF-8 + * unencoded name for the CREATE command. + */ + @Test + public void testUtf8FolderNameCreate() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Store store, IMAPHandler handler) + throws MessagingException, IOException { + Folder test = store.getFolder(utf8Folder); + assertTrue(test.create(Folder.HOLDS_MESSAGES)); + } + }, + new IMAPUtf8Handler() { + @Override + public void create(String line) throws IOException { + StringTokenizer st = new StringTokenizer(line); + st.nextToken(); // skip tag + st.nextToken(); // skip "CREATE" + String name = unquote(st.nextToken()); + if (name.equals(utf8Folder)) + ok(); + else + no("wrong name"); + } + + @Override + public void list(String line) throws IOException { + untagged("LIST (\\HasNoChildren) \"/\" \"" + + utf8Folder + "\""); + ok(); + } + }); + } + + /** + * Test that using a UTF-8 folder name results in the proper UTF-7 + * encoded name for the DELETE command. + */ + @Test + public void testUtf7FolderNameDelete() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Store store, IMAPHandler handler) + throws MessagingException, IOException { + Folder test = store.getFolder(utf8Folder); + assertTrue(test.delete(false)); + } + }, + new IMAPHandler() { + @Override + public void delete(String line) throws IOException { + StringTokenizer st = new StringTokenizer(line); + st.nextToken(); // skip tag + st.nextToken(); // skip "DELETE" + String name = st.nextToken(); + if (name.equals(utf7Folder)) + ok(); + else + no("wrong name"); + } + }); + } + + /** + * Test that using a UTF-8 folder name results in the proper UTF-8 + * unencoded name for the DELETE command. + */ + @Test + public void testUtf8FolderNameDelete() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Store store, IMAPHandler handler) + throws MessagingException, IOException { + Folder test = store.getFolder(utf8Folder); + assertTrue(test.delete(false)); + } + }, + new IMAPUtf8Handler() { + @Override + public void delete(String line) throws IOException { + StringTokenizer st = new StringTokenizer(line); + st.nextToken(); // skip tag + st.nextToken(); // skip "DELETE" + String name = unquote(st.nextToken()); + if (name.equals(utf8Folder)) + ok(); + else + no("wrong name"); + } + }); + } + + /** + * Test that using a UTF-8 folder name results in the proper UTF-7 + * encoded name for the SELECT command. + */ + @Test + public void testUtf7FolderNameSelect() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Store store, IMAPHandler handler) + throws MessagingException, IOException { + Folder test = store.getFolder(utf8Folder); + test.open(Folder.READ_WRITE); + test.close(true); + // no exception means success + } + }, + new IMAPHandler() { + @Override + public void select(String line) throws IOException { + StringTokenizer st = new StringTokenizer(line); + st.nextToken(); // skip tag + st.nextToken(); // skip "SELECT" + String name = st.nextToken(); + if (name.equals(utf7Folder)) + ok(); + else + no("wrong name"); + } + }); + } + + /** + * Test that using a UTF-8 folder name results in the proper UTF-8 + * unencoded name for the SELECT command. + */ + @Test + public void testUtf8FolderNameSelect() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Store store, IMAPHandler handler) + throws MessagingException, IOException { + Folder test = store.getFolder(utf8Folder); + test.open(Folder.READ_WRITE); + test.close(true); + // no exception means success + } + }, + new IMAPUtf8Handler() { + @Override + public void select(String line) throws IOException { + StringTokenizer st = new StringTokenizer(line); + st.nextToken(); // skip tag + st.nextToken(); // skip "SELECT" + String name = unquote(st.nextToken()); + if (name.equals(utf8Folder)) + ok(); + else + no("wrong name"); + } + }); + } + + /** + * Test that using a UTF-8 folder name results in the proper UTF-7 + * encoded name for the EXAMINE command. + */ + @Test + public void testUtf7FolderNameExamine() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Store store, IMAPHandler handler) + throws MessagingException, IOException { + Folder test = store.getFolder(utf8Folder); + test.open(Folder.READ_ONLY); + test.close(true); + // no exception means success + } + }, + new IMAPHandler() { + @Override + public void examine(String line) throws IOException { + StringTokenizer st = new StringTokenizer(line); + st.nextToken(); // skip tag + st.nextToken(); // skip "EXAMINE" + String name = st.nextToken(); + if (name.equals(utf7Folder)) + ok(); + else + no("wrong name"); + } + }); + } + + /** + * Test that using a UTF-8 folder name results in the proper UTF-8 + * unencoded name for the EXAMINE command. + */ + @Test + public void testUtf8FolderNameExamine() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Store store, IMAPHandler handler) + throws MessagingException, IOException { + Folder test = store.getFolder(utf8Folder); + test.open(Folder.READ_ONLY); + test.close(true); + // no exception means success + } + }, + new IMAPUtf8Handler() { + @Override + public void examine(String line) throws IOException { + StringTokenizer st = new StringTokenizer(line); + st.nextToken(); // skip tag + st.nextToken(); // skip "EXAMINE" + String name = unquote(st.nextToken()); + if (name.equals(utf8Folder)) + ok(); + else + no("wrong name"); + } + }); + } + + /** + * Test that using a UTF-8 folder name results in the proper UTF-7 + * encoded name for the STATUS command. + */ + @Test + public void testUtf7FolderNameStatus() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Store store, IMAPHandler handler) + throws MessagingException, IOException { + Folder test = store.getFolder(utf8Folder); + assertEquals(123, ((UIDFolder)test).getUIDValidity()); + } + }, + new IMAPHandler() { + @Override + public void status(String line) throws IOException { + StringTokenizer st = new StringTokenizer(line); + st.nextToken(); // skip tag + st.nextToken(); // skip "STATUS" + String name = st.nextToken(); + if (name.equals(utf7Folder)) { + untagged("STATUS " + utf7Folder + + " (UIDVALIDITY 123)"); + ok(); + } else + no("wrong name"); + } + }); + } + + /** + * Test that using a UTF-8 folder name results in the proper UTF-8 + * unencoded name for the STATUS command. + */ + @Test + public void testUtf8FolderNameStatus() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Store store, IMAPHandler handler) + throws MessagingException, IOException { + Folder test = store.getFolder(utf8Folder); + assertEquals(123, ((UIDFolder)test).getUIDValidity()); + } + }, + new IMAPUtf8Handler() { + @Override + public void status(String line) throws IOException { + StringTokenizer st = new StringTokenizer(line); + st.nextToken(); // skip tag + st.nextToken(); // skip "STATUS" + String name = unquote(st.nextToken()); + if (name.equals(utf8Folder)) { + untagged("STATUS \"" + utf8Folder + + "\" (UIDVALIDITY 123)"); + ok(); + } else + no("wrong name"); + } + }); + } + + /** + * Test that UIDNOTSTICKY is false in the formal case. + */ + @Test + public void testUidNotStickyFalse() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Store store, IMAPHandler handler) + throws MessagingException, IOException { + Folder test = store.getFolder("test"); + try { + test.open(Folder.READ_WRITE); + assertFalse(((IMAPFolder)test).getUIDNotSticky()); + } finally { + test.close(); + } + } + }, + new IMAPHandler()); + } + + /** + * Test that UIDNOTSTICKY is true when the untagged response is included. + */ + @Test + public void testUidNotStickyTrue() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Store store, IMAPHandler handler) + throws MessagingException, IOException { + Folder test = store.getFolder("test"); + try { + test.open(Folder.READ_WRITE); + assertTrue(((IMAPFolder)test).getUIDNotSticky()); + } finally { + test.close(); + } + } + }, + new IMAPHandler() { + @Override + public void select(String line) throws IOException { + untagged("NO [UIDNOTSTICKY]"); + super.select(line); + } + }); + } + + /** + * Test that EXPUNGE responses with out-of-range message numbers + * are ignored. + */ + @Test + public void testExpungeOutOfRange() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Store store, IMAPHandler handler) + throws MessagingException, IOException { + Folder test = store.getFolder("test"); + try { + test.open(Folder.READ_WRITE); + // no way to force a noop without waiting so do this + assertEquals(0, test.getUnreadMessageCount()); + assertEquals(0, test.getMessageCount()); + } finally { + test.close(); + } + } + }, + new IMAPHandler() { + @Override + public void search(String line) throws IOException { + untagged("1 EXPUNGE"); + untagged("0 EXISTS"); + super.search(line); + } + }); + } + + private void testWithHandler(IMAPTest test, IMAPHandler handler) { + TestServer server = null; + try { + server = new TestServer(handler); + server.start(); + + final Properties properties = new Properties(); + properties.setProperty("mail.imap.host", "localhost"); + properties.setProperty("mail.imap.port", "" + server.getPort()); + test.init(properties); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + final Store store = session.getStore("imap"); + try { + store.connect("test", "test"); + test.test(store, handler); + } catch (Exception ex) { + System.out.println(ex); + //ex.printStackTrace(); + fail(ex.toString()); + } finally { + store.close(); + } + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + } + } + } + + private static String unquote(String s) { + if (s.startsWith("\"") && s.endsWith("\"") && s.length() > 1) { + s = s.substring(1, s.length() - 1); + // check for any escaped characters + if (s.indexOf('\\') >= 0) { + StringBuilder sb = new StringBuilder(s.length()); // approx + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c == '\\' && i < s.length() - 1) + c = s.charAt(++i); + sb.append(c); + } + s = sb.toString(); + } + } + return s; + } + + /** + * An IMAPHandler that enables UTF-8 support. + */ + private static class IMAPUtf8Handler extends IMAPHandler { + {{ capabilities += " ENABLE UTF8=ACCEPT"; }} + + @Override + public void enable(String line) throws IOException { + ok(); + } + } +}
diff --git a/mail/src/test/java/com/sun/mail/imap/IMAPHandler.java b/mail/src/test/java/com/sun/mail/imap/IMAPHandler.java new file mode 100644 index 0000000..7ebe4d3 --- /dev/null +++ b/mail/src/test/java/com/sun/mail/imap/IMAPHandler.java
@@ -0,0 +1,586 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.io.IOException; +import java.io.Reader; +import java.util.StringTokenizer; +import java.util.logging.Level; +import java.nio.charset.StandardCharsets; + +import com.sun.mail.util.BASE64EncoderStream; + +import com.sun.mail.test.ProtocolHandler; + +/** + * Handle IMAP connection. + * + * @author Bill Shannon + */ +public class IMAPHandler extends ProtocolHandler { + + /** Current line. */ + private String currentLine; + + /** Tag for current command */ + protected String tag; + + /** IMAP capabilities supported */ + protected String capabilities = "IMAP4REV1 IDLE ID"; + + /** Number of messages */ + protected int numberOfMessages = 0; + + /** Number of recent messages */ + protected int numberOfRecentMessages = 0; + + /** + * Send greetings. + * + * @throws IOException unable to write to socket + */ + @Override + public void sendGreetings() throws IOException { + untagged("OK [CAPABILITY " + capabilities + "] IMAPHandler"); + } + + /** + * Send String to socket. + * + * @param str String to send + * @throws IOException unable to write to socket + */ + public void println(final String str) throws IOException { + writer.print(str); + writer.print("\r\n"); + writer.flush(); + } + + /** + * Send a tagged response. + * + * @param resp the response to send + * @throws IOException unable to read/write to socket + */ + public void tagged(final String resp) throws IOException { + println(tag + " " + resp); + } + + /** + * Send an untagged response. + * + * @param resp the response to send + * @throws IOException unable to read/write to socket + */ + public void untagged(final String resp) throws IOException { + println("* " + resp); + } + + /** + * Send a tagged OK response. + * + * @throws IOException unable to read/write to socket + */ + public void ok() throws IOException { + tagged("OK"); + } + + /** + * Send a tagged OK response with a message. + * + * @param msg the message to send + * @throws IOException unable to read/write to socket + */ + public void ok(final String msg) throws IOException { + tagged("OK " + (msg != null ? msg : "")); + } + + /** + * Send a tagged NO response with a message. + * + * @param msg the message to send + * @throws IOException unable to read/write to socket + */ + public void no(final String msg) throws IOException { + tagged("NO " + (msg != null ? msg : "")); + } + + /** + * Send a tagged BAD response with a message. + * + * @param msg the message to send + * @throws IOException unable to read/write to socket + */ + public void bad(final String msg) throws IOException { + tagged("BAD " + (msg != null ? msg : "")); + } + + /** + * Send an untagged BYE response with a message, then exit. + * + * @param msg the message to send + * @throws IOException unable to read/write to socket + */ + public void bye(final String msg) throws IOException { + untagged("BYE " + (msg != null ? msg : "")); + exit(); + } + + /** + * Send a "continue" command. + * + * @throws IOException unable to read/write to socket + */ + public void cont() throws IOException { + println("+ please continue"); + } + + /** + * Send a "continue" command with a message. + * + * @throws IOException unable to read/write to socket + */ + public void cont(String msg) throws IOException { + println("+ " + (msg != null ? msg : "")); + } + + /** + * Handle command. + * + * @throws IOException unable to read/write to socket + */ + @Override + public void handleCommand() throws IOException { + currentLine = super.readLine(); + + if (currentLine == null) { + // probably just EOF because the socket was closed + //LOGGER.severe("Current line is null!"); + exit(); + return; + } + + StringTokenizer ct = new StringTokenizer(currentLine, " "); + if (!ct.hasMoreTokens()) { + LOGGER.log(Level.SEVERE, "ERROR no command tag: {0}", + escape(currentLine)); + bad("no command tag"); + return; + } + tag = ct.nextToken(); + if (!ct.hasMoreTokens()) { + LOGGER.log(Level.SEVERE, "ERROR no command: {0}", + escape(currentLine)); + bad("no command"); + return; + } + final String commandName = ct.nextToken().toUpperCase(); + if (commandName == null) { + LOGGER.severe("Command name is empty!"); + exit(); + return; + } + + if (commandName.equals("LOGIN")) { + login(); + } else if (commandName.equals("AUTHENTICATE")) { + String mech = ct.nextToken().toUpperCase(); + String ir = null; + if (ct.hasMoreTokens()) + ir = ct.nextToken(); + authenticate(mech, ir); + } else if (commandName.equals("CAPABILITY")) { + capability(); + } else if (commandName.equals("NOOP")) { + noop(); + } else if (commandName.equals("SELECT")) { + select(currentLine); + } else if (commandName.equals("EXAMINE")) { + examine(currentLine); + } else if (commandName.equals("LIST")) { + list(currentLine); + } else if (commandName.equals("IDLE")) { + idle(); + } else if (commandName.equals("FETCH")) { + fetch(currentLine); + } else if (commandName.equals("STORE")) { + store(currentLine); + } else if (commandName.equals("SEARCH")) { + search(currentLine); + } else if (commandName.equals("APPEND")) { + append(currentLine); + } else if (commandName.equals("CLOSE")) { + close(); + } else if (commandName.equals("LOGOUT")) { + logout(); + } else if (commandName.equals("UID")) { + String subcommandName = ct.nextToken().toUpperCase(); + if (subcommandName.equals("FETCH")) { + uidfetch(currentLine); + } else if (subcommandName.equals("STORE")) { + uidstore(currentLine); + } else { + LOGGER.log(Level.SEVERE, "ERROR UID command unknown: {0}", + subcommandName); + bad("unknown UID command"); + } + } else if (commandName.equals("ID")) { + id(currentLine); + } else if (commandName.equals("ENABLE")) { + enable(currentLine); + } else if (commandName.equals("CREATE")) { + create(currentLine); + } else if (commandName.equals("DELETE")) { + delete(currentLine); + } else if (commandName.equals("STATUS")) { + status(currentLine); + } else if (commandName.equals("NAMESPACE")) { + namespace(); + } else { + LOGGER.log(Level.SEVERE, "ERROR command unknown: {0}", + escape(currentLine)); + bad("unknown command"); + } + } + + /** + * LOGIN command. + * + * @throws IOException unable to read/write to socket + */ + public void login() throws IOException { + ok("[CAPABILITY " + capabilities + "]"); + } + + /** + * AUTHENTICATE command. + * + * @throws IOException unable to read/write to socket + */ + public void authenticate(String mech, String ir) throws IOException { + if (mech.equals("LOGIN")) + authlogin(ir); + else if (mech.equals("PLAIN")) + authplain(ir); + else + bad("AUTHENTICATE not supported"); + } + + /** + * AUTHENTICATE LOGIN command. + * + * @throws IOException unable to read/write to socket + */ + public void authlogin(String ir) throws IOException { + if (ir != null) + bad("AUTHENTICATE LOGIN does not support initial response"); + cont(base64encode("Username")); + String username = readLine(); + cont(base64encode("Password")); + String password = readLine(); + ok("[CAPABILITY " + capabilities + "]"); + } + + /** + * AUTHENTICATE PLAIN command. + * + * @throws IOException unable to read/write to socket + */ + public void authplain(String ir) throws IOException { + if (ir == null) { + cont(""); + String resp = readLine(); + } + ok("[CAPABILITY " + capabilities + "]"); + } + + /** + * CAPABILITY command. + * + * @throws IOException unable to read/write to socket + */ + public void capability() throws IOException { + untagged("CAPABILITY " + capabilities); + ok(); + } + + /** + * SELECT command. + * + * @throws IOException unable to read/write to socket + */ + public void select(String line) throws IOException { + untagged(numberOfMessages + " EXISTS"); + untagged(numberOfRecentMessages + " RECENT"); + ok(); + } + + /** + * EXAMINE command. + * + * @throws IOException unable to read/write to socket + */ + public void examine(String line) throws IOException { + untagged(numberOfMessages + " EXISTS"); + untagged(numberOfRecentMessages + " RECENT"); + ok(); + } + + /** + * LIST command. + * + * @throws IOException unable to read/write to socket + */ + public void list(String line) throws IOException { + ok(); + } + + /** + * IDLE command. + * + * @throws IOException unable to read/write to socket + */ + public void idle() throws IOException { + cont(); + idleWait(); + ok(); + } + + @Override + protected String readLine() throws IOException { + currentLine = super.readLine(); + if (currentLine == null) { + LOGGER.severe("Current line is null!"); + exit(); + } + return currentLine; + } + + protected void idleWait() throws IOException { + String line = readLine(); + + if (line != null && !line.equalsIgnoreCase("DONE")) { + LOGGER.severe("Didn't get DONE response to IDLE"); + exit(); + return; + } + } + + /** + * FETCH command. + * + * @throws IOException unable to read/write to socket + */ + public void fetch(String line) throws IOException { + ok(); // XXX + } + + /** + * STORE command. + * + * @throws IOException unable to read/write to socket + */ + public void store(String line) throws IOException { + ok(); // XXX + } + + /** + * SEARCH command. + * + * @throws IOException unable to read/write to socket + */ + public void search(String line) throws IOException { + untagged("SEARCH"); + ok(); // XXX + } + + /** + * UID FETCH command. + * + * @throws IOException unable to read/write to socket + */ + public void uidfetch(String line) throws IOException { + ok(); // XXX + } + + /** + * UID STORE command. + * + * @throws IOException unable to read/write to socket + */ + public void uidstore(String line) throws IOException { + ok(); // XXX + } + + /** + * APPEND command. + * + * @throws IOException unable to read/write to socket + */ + public void append(String line) throws IOException { + int left = line.lastIndexOf('{'); + int right = line.indexOf('}', left); + int bytes = Integer.parseInt(line.substring(left + 1, right)); + cont("waiting for message"); + collectMessage(bytes); + ok(); // XXX + } + + /** + * ID command. + * + * @throws IOException unable to read/write to socket + */ + public void id(String line) throws IOException { + untagged("ID NIL"); + ok(); + } + + /** + * ENABLE command. + * + * @throws IOException unable to read/write to socket + */ + public void enable(String line) throws IOException { + no("can't enable"); + } + + /** + * CREATE command. + * + * @throws IOException unable to read/write to socket + */ + public void create(String line) throws IOException { + no("can't create"); + } + + /** + * DELETE command. + * + * @throws IOException unable to read/write to socket + */ + public void delete(String line) throws IOException { + no("can't delete"); + } + + /** + * STATUS command. + * + * @throws IOException unable to read/write to socket + */ + public void status(String line) throws IOException { + no("can't get status"); + } + + /** + * NAMESPACE command. + * + * @throws IOException unable to read/write to socket + */ + public void namespace() throws IOException { + no("no namespaces"); + } + + /** + * Collect "bytes" worth of data for the message being appended. + */ + protected void collectMessage(int bytes) throws IOException { + readLiteral(bytes); // read the data and throw it away + super.readLine(); // data followed by a newline + } + + /** + * Read a literal of "bytes" bytes and return it as a UTF-8 string. + */ + protected String readLiteral(int bytes) throws IOException { + println("+"); + byte[] data = new byte[bytes]; + int len = data.length; + int off = 0; + int n; + while (len > 0 && (n = in.read(data, off, len)) > 0) { + off += n; + len -= n; + } + return new String(data, StandardCharsets.UTF_8); + } + + /** + * CLOSE command. + * + * @throws IOException unable to read/write to socket + */ + public void close() throws IOException { + ok(); + } + + /** + * NOOP command. + * + * @throws IOException unable to read/write to socket + */ + public void noop() throws IOException { + ok(); + } + + /** + * LOGOUT command. + * + * @throws IOException unable to read/write to socket + */ + public void logout() throws IOException { + ok(); + exit(); + } + + /** + * Base64 encode the string. + */ + protected String base64encode(String s) throws IOException { + return new String(BASE64EncoderStream.encode(s.getBytes("US-ASCII")), + "US-ASCII"); + } + + /** + * Escape any non-printable characters in "s", + * limiting total length to about 100 characters. + */ + private String escape(String s) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < s.length(); i++) { + if (sb.length() >= 100) { + sb.append("..."); + break; + } + char c = s.charAt(i); + if (c < ' ' || c == '\177') { + if (c == '\r') + sb.append("\\r"); + else if (c == '\n') + sb.append("\\n"); + else if (c == '\t') + sb.append("\\t"); + else + sb.append('\\').append(String.format("%03o", (int)c)); + } else if (c >= '\200') { + sb.append("\\u").append(String.format("%04x", (int)c)); + } else + sb.append(c); + } + return sb.toString(); + } +}
diff --git a/mail/src/test/java/com/sun/mail/imap/IMAPIDTest.java b/mail/src/test/java/com/sun/mail/imap/IMAPIDTest.java new file mode 100644 index 0000000..e6ce7c2 --- /dev/null +++ b/mail/src/test/java/com/sun/mail/imap/IMAPIDTest.java
@@ -0,0 +1,96 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.io.IOException; +import java.util.Properties; +import java.util.StringTokenizer; +import java.util.Map; + +import javax.mail.Session; +import javax.mail.Store; + +import com.sun.mail.test.TestServer; + +import org.junit.Test; +import org.junit.Rule; +import org.junit.rules.Timeout; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +/** + * Test the IMAP ID command. + */ +public final class IMAPIDTest { + + // timeout the test in case of deadlock + @Rule + public Timeout deadlockTimeout = Timeout.seconds(20); + + @Test + public void testIDNIL() { + TestServer server = null; + try { + final IMAPHandler handler = new IMAPHandlerID(); + server = new TestServer(handler); + server.start(); + + final Properties properties = new Properties(); + properties.setProperty("mail.imap.host", "localhost"); + properties.setProperty("mail.imap.port", "" + server.getPort()); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + final IMAPStore store = (IMAPStore)session.getStore("imap"); + try { + store.connect("test", "test"); + Map<String,String> id = store.id(null); + assertEquals("true", id.get("test")); + + } catch (Exception ex) { + System.out.println(ex); + ex.printStackTrace(); + fail(ex.toString()); + } finally { + store.close(); + } + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + } + } + } + + /** + * Custom handler. + */ + private static final class IMAPHandlerID extends IMAPHandler { + + @Override + public void id(String line) throws IOException { + StringTokenizer st = new StringTokenizer(line); + String tag = st.nextToken(); + String cmd = st.nextToken(); + String arg = st.nextToken(); + untagged("ID (\"test\" \"" + arg.equals("NIL") + "\")"); + ok(); + } + } +}
diff --git a/mail/src/test/java/com/sun/mail/imap/IMAPIdleManagerTest.java b/mail/src/test/java/com/sun/mail/imap/IMAPIdleManagerTest.java new file mode 100644 index 0000000..dbff132 --- /dev/null +++ b/mail/src/test/java/com/sun/mail/imap/IMAPIdleManagerTest.java
@@ -0,0 +1,514 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.io.IOException; +import java.util.Properties; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import javax.mail.Session; +import javax.mail.Store; +import javax.mail.Folder; +import javax.mail.FetchProfile; +import javax.mail.MessagingException; +import javax.mail.event.ConnectionAdapter; +import javax.mail.event.ConnectionEvent; + +import com.sun.mail.test.TestServer; + +import org.junit.Test; +import org.junit.Rule; +import org.junit.rules.Timeout; +import static org.junit.Assert.fail; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * Test IdleManager. + */ +public final class IMAPIdleManagerTest { + + private static final int TIMEOUT = 1000; // 1 second + + // timeout the test in case of deadlock + @Rule + public Timeout deadlockTimeout = Timeout.millis(10 * TIMEOUT); + + /** + * Test that IdleManager handles multiple responses in a single packet. + */ + @Test + public void testDone() { + testSuccess(new IMAPHandlerIdleDone()); + } + + @Test + public void testExists() { + testSuccess(new IMAPHandlerIdleExists()); + } + + private void testSuccess(IMAPHandlerIdle handler) { + TestServer server = null; + IdleManager idleManager = null; + try { + server = new TestServer(handler); + server.start(); + + final Properties properties = new Properties(); + properties.setProperty("mail.imap.host", "localhost"); + properties.setProperty("mail.imap.port", "" + server.getPort()); + properties.setProperty("mail.imap.usesocketchannels", "true"); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + ExecutorService executor = Executors.newCachedThreadPool(); + idleManager = new IdleManager(session, executor); + + final IMAPStore store = (IMAPStore)session.getStore("imap"); + Folder folder = null; + try { + store.connect("test", "test"); + folder = store.getFolder("INBOX"); + folder.open(Folder.READ_WRITE); + idleManager.watch(folder); + handler.waitForIdle(); + + // now do something that is sure to touch the server + FetchProfile fp = new FetchProfile(); + fp.add(FetchProfile.Item.ENVELOPE); + folder.fetch(folder.getMessages(), fp); + + // check that the new message was seen + int count = folder.getMessageCount(); + folder.close(true); + + assertEquals(3, count); + } catch (Exception ex) { + System.out.println(ex); + ex.printStackTrace(); + fail(ex.toString()); + } finally { + try { + folder.close(false); + } catch (Exception ex2) { } + store.close(); + } + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (idleManager != null) + idleManager.stop(); + if (server != null) { + server.quit(); + } + } + } + + /** + * Test that IdleManager handles timeouts. + */ + @Test + public void testBeforeIdleTimeout() { + testFailure(new IMAPHandlerBeforeIdleTimeout(), true); + } + + @Test + public void testIdleTimeout() { + testFailure(new IMAPHandlerIdleTimeout(), true); + } + + @Test + public void testDoneTimeout() { + testFailure(new IMAPHandlerDoneTimeout(), true); + } + + /** + * Test that IdleManager handles connection failures. + */ + @Test + public void testBeforeIdleDrop() { + testFailure(new IMAPHandlerBeforeIdleDrop(), false); + } + + @Test + public void testIdleDrop() { + testFailure(new IMAPHandlerIdleDrop(), false); + } + + @Test + public void testDoneDrop() { + testFailure(new IMAPHandlerDoneDrop(), false); + } + + private void testFailure(IMAPHandlerIdle handler, boolean setTimeout) { + TestServer server = null; + IdleManager idleManager = null; + try { + server = new TestServer(handler); + server.start(); + + final Properties properties = new Properties(); + properties.setProperty("mail.imap.host", "localhost"); + properties.setProperty("mail.imap.port", "" + server.getPort()); + if (setTimeout) + properties.setProperty("mail.imap.timeout", "" + TIMEOUT); + properties.setProperty("mail.imap.usesocketchannels", "true"); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + ExecutorService executor = Executors.newCachedThreadPool(); + idleManager = new IdleManager(session, executor); + + final IMAPStore store = (IMAPStore)session.getStore("imap"); + Folder folder = null; + try { + store.connect("test", "test"); + folder = store.getFolder("INBOX"); + folder.open(Folder.READ_WRITE); + idleManager.watch(folder); + handler.waitForIdle(); + + // now do something that is sure to touch the server + FetchProfile fp = new FetchProfile(); + fp.add(FetchProfile.Item.ENVELOPE); + folder.fetch(folder.getMessages(), fp); + + fail("No exception"); + } catch (MessagingException mex) { + // success! + } catch (Exception ex) { + System.out.println("Failed with exception: " + ex); + ex.printStackTrace(); + fail(ex.toString()); + } finally { + try { + folder.close(false); + } catch (Exception ex2) { } + store.close(); + } + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (idleManager != null) + idleManager.stop(); + if (server != null) { + server.quit(); + } + } + } + + @Test + public void testNotOpened() { + TestServer server = null; + IdleManager idleManager = null; + try { + server = new TestServer(new IMAPHandler()); + server.start(); + + final Properties properties = new Properties(); + properties.setProperty("mail.imap.host", "localhost"); + properties.setProperty("mail.imap.port", "" + server.getPort()); + properties.setProperty("mail.imap.usesocketchannels", "true"); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + ExecutorService executor = Executors.newCachedThreadPool(); + idleManager = new IdleManager(session, executor); + + final IMAPStore store = (IMAPStore)session.getStore("imap"); + Folder folder = null; + try { + store.connect("test", "test"); + folder = store.getFolder("INBOX"); + idleManager.watch(folder); + + fail("No exception"); + } catch (MessagingException mex) { + // make sure we get the expected exception + assertTrue(mex.getMessage().contains("open")); + // success! + } catch (Exception ex) { + System.out.println("Failed with exception: " + ex); + ex.printStackTrace(); + fail(ex.toString()); + } finally { + try { + folder.close(false); + } catch (Exception ex2) { } + store.close(); + } + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (idleManager != null) + idleManager.stop(); + if (server != null) { + server.quit(); + } + } + } + + @Test + public void testNoSocketChannel() { + TestServer server = null; + IdleManager idleManager = null; + try { + server = new TestServer(new IMAPHandler()); + server.start(); + + final Properties properties = new Properties(); + properties.setProperty("mail.imap.host", "localhost"); + properties.setProperty("mail.imap.port", "" + server.getPort()); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + ExecutorService executor = Executors.newCachedThreadPool(); + idleManager = new IdleManager(session, executor); + + final IMAPStore store = (IMAPStore)session.getStore("imap"); + Folder folder = null; + try { + store.connect("test", "test"); + folder = store.getFolder("INBOX"); + folder.open(Folder.READ_WRITE); + idleManager.watch(folder); + + fail("No exception"); + } catch (MessagingException mex) { + // make sure we get the expected exception + assertTrue(!mex.getMessage().contains("open")); + // success! + } catch (Exception ex) { + System.out.println("Failed with exception: " + ex); + ex.printStackTrace(); + fail(ex.toString()); + } finally { + try { + folder.close(false); + } catch (Exception ex2) { } + store.close(); + } + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (idleManager != null) + idleManager.stop(); + if (server != null) { + server.quit(); + } + } + } + + /** + * Base class for custom handler. + */ + private static abstract class IMAPHandlerIdle extends IMAPHandler { + @Override + public void select(String line) throws IOException { + numberOfMessages = 1; + super.select(line); + } + + public abstract void waitForIdle() throws InterruptedException; + } + + /** + * Custom handler. Respond to DONE with a single packet containing + * EXISTS and OK. + */ + private static final class IMAPHandlerIdleDone extends IMAPHandlerIdle { + // must be static because handler is cloned for each connection + private static CountDownLatch latch = new CountDownLatch(1); + + @Override + public void idle() throws IOException { + cont(); + latch.countDown(); + idleWait(); + println("* 3 EXISTS\r\n" + tag + " OK"); + } + + @Override + public void waitForIdle() throws InterruptedException { + latch.await(); + } + } + + /** + * Custom handler. Send two EXISTS responses in a single packet. + */ + private static final class IMAPHandlerIdleExists extends IMAPHandlerIdle { + // must be static because handler is cloned for each connection + private static CountDownLatch latch = new CountDownLatch(1); + + @Override + public void idle() throws IOException { + cont(); + latch.countDown(); + idleWait(); + println("* 2 EXISTS\r\n* 3 EXISTS"); + ok(); + } + + @Override + public void waitForIdle() throws InterruptedException { + latch.await(); + } + } + + /** + * Custom handler. Delay long enough before IDLE starts to force a timeout. + */ + private static final class IMAPHandlerBeforeIdleTimeout + extends IMAPHandlerIdle { + // must be static because handler is cloned for each connection + private static CountDownLatch latch = new CountDownLatch(1); + + @Override + public void idle() throws IOException { + try { + Thread.sleep(2 * TIMEOUT); + } catch (InterruptedException ex) { } + cont(); + latch.countDown(); + idleWait(); + ok(); + } + + @Override + public void waitForIdle() throws InterruptedException { + latch.await(); + } + } + + /** + * Custom handler. Delay long enough after IDLE starts to force a timeout. + */ + private static final class IMAPHandlerIdleTimeout extends IMAPHandlerIdle { + // must be static because handler is cloned for each connection + private static CountDownLatch latch = new CountDownLatch(1); + + @Override + public void idle() throws IOException { + cont(); + latch.countDown(); + try { + Thread.sleep(2 * TIMEOUT); + } catch (InterruptedException ex) { } + idleWait(); + ok(); + } + + @Override + public void waitForIdle() throws InterruptedException { + latch.await(); + } + } + + /** + * Custom handler. Delay long enough after DONE received to force a + * timeout. + */ + private static final class IMAPHandlerDoneTimeout extends IMAPHandlerIdle { + // must be static because handler is cloned for each connection + private static CountDownLatch latch = new CountDownLatch(1); + + @Override + public void idle() throws IOException { + cont(); + latch.countDown(); + idleWait(); + try { + Thread.sleep(2 * TIMEOUT); + } catch (InterruptedException ex) { } + ok(); + } + + @Override + public void waitForIdle() throws InterruptedException { + latch.await(); + } + } + + /** + * Custom handler. Drop the connection before IDLE started. + */ + private static final class IMAPHandlerBeforeIdleDrop extends + IMAPHandlerIdle { + // must be static because handler is cloned for each connection + private static CountDownLatch latch = new CountDownLatch(1); + + @Override + public void idle() throws IOException { + latch.countDown(); + exit(); + } + + @Override + public void waitForIdle() throws InterruptedException { + latch.await(); + } + } + + /** + * Custom handler. Drop the connection after IDLE started. + */ + private static final class IMAPHandlerIdleDrop extends IMAPHandlerIdle { + // must be static because handler is cloned for each connection + private static CountDownLatch latch = new CountDownLatch(1); + + @Override + public void idle() throws IOException { + cont(); + latch.countDown(); + exit(); + } + + @Override + public void waitForIdle() throws InterruptedException { + latch.await(); + } + } + + /** + * Custom handler. Drop the connection after DONE received. + */ + private static final class IMAPHandlerDoneDrop extends IMAPHandlerIdle { + // must be static because handler is cloned for each connection + private static CountDownLatch latch = new CountDownLatch(1); + + @Override + public void idle() throws IOException { + cont(); + latch.countDown(); + idleWait(); + exit(); + } + + @Override + public void waitForIdle() throws InterruptedException { + latch.await(); + } + } +}
diff --git a/mail/src/test/java/com/sun/mail/imap/IMAPIdleStateTest.java b/mail/src/test/java/com/sun/mail/imap/IMAPIdleStateTest.java new file mode 100644 index 0000000..4ef95aa --- /dev/null +++ b/mail/src/test/java/com/sun/mail/imap/IMAPIdleStateTest.java
@@ -0,0 +1,117 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.io.IOException; +import java.util.Properties; +import java.util.concurrent.CountDownLatch; + +import javax.mail.Session; +import javax.mail.Store; + +import com.sun.mail.imap.IMAPStore; +import com.sun.mail.test.TestServer; + +import org.junit.Test; +import org.junit.Rule; +import org.junit.rules.Timeout; +import static org.junit.Assert.fail; + +/** + * Test that IMAP idle state is handled properly. + */ +public final class IMAPIdleStateTest { + + // timeout the test in case of deadlock + @Rule + public Timeout deadlockTimeout = Timeout.seconds(20); + + @Test + public void test() { + TestServer server = null; + try { + final IMAPHandlerIdleBye handler = new IMAPHandlerIdleBye(); + server = new TestServer(handler); + server.start(); + + final Properties properties = new Properties(); + properties.setProperty("mail.imap.host", "localhost"); + properties.setProperty("mail.imap.port", "" + server.getPort()); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + final IMAPStore store = (IMAPStore)session.getStore("imap"); + try { + store.connect("test", "test"); + + // create a thread to run the IDLE command on the Store + Thread t = new Thread() { + @Override + public void run() { + try { + store.idle(); + } catch (Exception ex) { + } + } + }; + t.start(); + handler.waitForIdle(); + + // Now break it out of idle. + // Need to use a method that doesn't check that the Store + // is connected first. + store.hasCapability("XXX"); + // no NullPointerException means the bug is fixed! + + } catch (Exception ex) { + System.out.println(ex); + //ex.printStackTrace(); + fail(ex.toString()); + } finally { + store.close(); + } + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + } + } + } + + /** + * Custom handler. Simulates the server sending a BYE response + * to abort an IDLE. + */ + private static final class IMAPHandlerIdleBye extends IMAPHandler { + // must be static because handler is cloned for each connection + private static CountDownLatch latch = new CountDownLatch(1); + + @Override + public void idle() throws IOException { + cont(); + latch.countDown(); + // don't wait for DONE, just close the connection now + bye("closing"); + } + + public void waitForIdle() throws InterruptedException { + latch.await(); + } + } +}
diff --git a/mail/src/test/java/com/sun/mail/imap/IMAPIdleUntaggedResponseTest.java b/mail/src/test/java/com/sun/mail/imap/IMAPIdleUntaggedResponseTest.java new file mode 100644 index 0000000..36ebbfd --- /dev/null +++ b/mail/src/test/java/com/sun/mail/imap/IMAPIdleUntaggedResponseTest.java
@@ -0,0 +1,137 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.io.IOException; +import java.util.Properties; +import java.util.concurrent.CountDownLatch; + +import javax.mail.Folder; +import javax.mail.Session; +import javax.mail.Store; +import javax.mail.Message; +import javax.mail.FetchProfile; + +import com.sun.mail.test.TestServer; + +import org.junit.Test; +import org.junit.Rule; +import org.junit.rules.Timeout; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +/** + * Test untagged responses before IDLE continuation. + */ +public final class IMAPIdleUntaggedResponseTest { + + // timeout the test in case of deadlock + @Rule + public Timeout deadlockTimeout = Timeout.seconds(20); + + @Test + public void test() { + TestServer server = null; + try { + final IMAPHandlerIdleExists handler = new IMAPHandlerIdleExists(); + server = new TestServer(handler); + server.start(); + + final Properties properties = new Properties(); + properties.setProperty("mail.imap.host", "localhost"); + properties.setProperty("mail.imap.port", "" + server.getPort()); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + final Store store = session.getStore("imap"); + Folder folder0 = null; + try { + store.connect("test", "test"); + final Folder folder = store.getFolder("INBOX"); + folder0 = folder; + folder.open(Folder.READ_ONLY); + + // create a thread to make sure we're kicked out of idle + Thread t = new Thread() { + @Override + public void run() { + try { + handler.waitForIdle(); + // now do something that is sure to touch the server + FetchProfile fp = new FetchProfile(); + fp.add(FetchProfile.Item.ENVELOPE); + folder.fetch(folder.getMessages(), fp); + } catch (Exception ex) { + } + } + }; + t.start(); + + ((com.sun.mail.imap.IMAPFolder)folder).idle(); + + assertEquals("message count", 1, folder.getMessageCount()); + + } catch (Exception ex) { + System.out.println(ex); + //ex.printStackTrace(); + fail(ex.toString()); + } finally { + if (folder0 != null) + folder0.close(false); + store.close(); + } + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + } + } + } + + /** + * Custom handler. Returns untagged responses before continuation, + * followed by a flag change for one of the new messages, to make + * sure the notification of the new message is seen. + */ + private static final class IMAPHandlerIdleExists extends IMAPHandler { + // must be static because handler is cloned for each connection + private static CountDownLatch latch = new CountDownLatch(1); + + @Override + public void examine(String line) throws IOException { + numberOfMessages = 1; + super.examine(line); + } + + @Override + public void idle() throws IOException { + untagged("1 EXISTS"); + untagged("1 RECENT"); + cont(); + untagged("1 FETCH (FLAGS (\\Recent \\Seen))"); + latch.countDown(); + idleWait(); + ok(); + } + + public void waitForIdle() throws InterruptedException { + latch.await(); + } + } +}
diff --git a/mail/src/test/java/com/sun/mail/imap/IMAPLoginCapabilitiesTest.java b/mail/src/test/java/com/sun/mail/imap/IMAPLoginCapabilitiesTest.java new file mode 100644 index 0000000..17fc5e9 --- /dev/null +++ b/mail/src/test/java/com/sun/mail/imap/IMAPLoginCapabilitiesTest.java
@@ -0,0 +1,128 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.io.IOException; +import java.util.Properties; + +import javax.mail.Session; +import javax.mail.Store; + +import com.sun.mail.test.TestServer; + +import org.junit.Test; +import org.junit.Rule; +import org.junit.rules.Timeout; +import static org.junit.Assert.fail; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * Test that capabilities are updated after login. + */ +public final class IMAPLoginCapabilitiesTest { + + private static final String NEWCAP = "NEWCAP"; + + private static final int TIMEOUT = 1000; // 1 second + + // timeout the test in case of deadlock + @Rule + public Timeout deadlockTimeout = Timeout.millis(5 * TIMEOUT); + + /** + * Test untagged CAPABILITY response after LOGIN. + * This is illegal, but mail.ru and AOL do it. + */ + @Test + public void testUntaggedCapabilityAfterLogin() { + test(new IMAPHandler() { + @Override + public void login() throws IOException { + untagged("CAPABILITY " + capabilities + + " " + NEWCAP); + ok("LOGIN completed"); + } + }); + } + + /** + * Test multiple untagged CAPABILITY responses after LOGIN. + * This should NEVER happen, but we handle it just in case. + */ + @Test + public void testMultipleUntaggedCapabilityAfterLogin() { + test(new IMAPHandler() { + @Override + public void login() throws IOException { + untagged("CAPABILITY " + capabilities); + untagged("CAPABILITY " + NEWCAP); + ok("LOGIN completed"); + } + }); + } + + /** + * Test untagged CAPABILITY response after AUTHENTICATE. + */ + @Test + public void testUntaggedCapabilityAfterAuthenticate() { + test(new IMAPHandler() { + {{ capabilities += " AUTH=PLAIN"; }} + @Override + public void authplain(String ir) throws IOException { + untagged("CAPABILITY " + capabilities + + " " + NEWCAP); + ok("AUTHENTICATE completed"); + } + }); + } + + private void test(IMAPHandler handler) { + TestServer server = null; + try { + server = new TestServer(handler); + server.start(); + + final Properties properties = new Properties(); + properties.setProperty("mail.imap.host", "localhost"); + properties.setProperty("mail.imap.port", "" + server.getPort()); + //properties.setProperty("mail.debug.auth", "true"); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + final IMAPStore store = (IMAPStore)session.getStore("imap"); + try { + store.connect("test", "test"); + assertTrue(store.hasCapability(NEWCAP)); + } catch (Exception ex) { + System.out.println(ex); + //ex.printStackTrace(); + fail(ex.toString()); + } finally { + store.close(); + } + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + } + } + } +}
diff --git a/mail/src/test/java/com/sun/mail/imap/IMAPLoginFailureTest.java b/mail/src/test/java/com/sun/mail/imap/IMAPLoginFailureTest.java new file mode 100644 index 0000000..4a5b282 --- /dev/null +++ b/mail/src/test/java/com/sun/mail/imap/IMAPLoginFailureTest.java
@@ -0,0 +1,89 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.io.*; +import java.util.Properties; + +import javax.mail.Session; +import javax.mail.Store; +import javax.mail.MessagingException; + +import com.sun.mail.test.SavedSocketFactory; +import com.sun.mail.test.TestServer; + +import org.junit.Test; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.fail; + +/** + * Test that login failures are handled correctly. + */ +public final class IMAPLoginFailureTest { + + /** + * Test that login failures when no login methods are supported + * cause the socket to be closed. + */ + @Test + public void testSocketClosed() { + TestServer server = null; + try { + final IMAPHandler handler = new IMAPHandler() { + @Override + public void sendGreetings() throws IOException { + capabilities = "IMAP4REV1 LOGINDISABLED"; + super.sendGreetings(); + } + }; + server = new TestServer(handler); + server.start(); + + SavedSocketFactory ssf = new SavedSocketFactory(); + Properties properties = new Properties(); + properties.setProperty("mail.imap.host", "localhost"); + properties.setProperty("mail.imap.port", "" + server.getPort()); + properties.put("mail.imap.socketFactory", ssf); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + final Store store = session.getStore("imap"); + try { + store.connect("test", "test"); + fail("login did not fail"); + } catch (MessagingException mex) { + // this is what we expect, now check that the socket is closed + assertTrue(ssf.getSocket().isClosed()); + } catch (Exception ex) { + System.out.println(ex); + //ex.printStackTrace(); + fail(ex.toString()); + } finally { + store.close(); + } + + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + } + } + } +}
diff --git a/mail/src/test/java/com/sun/mail/imap/IMAPLoginHandler.java b/mail/src/test/java/com/sun/mail/imap/IMAPLoginHandler.java new file mode 100644 index 0000000..132a72f --- /dev/null +++ b/mail/src/test/java/com/sun/mail/imap/IMAPLoginHandler.java
@@ -0,0 +1,63 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import com.sun.mail.util.BASE64DecoderStream; + +/** + * Handle IMAP connection with LOGIN authentication. + * + * @author Bill Shannon + */ +public class IMAPLoginHandler extends IMAPHandler { + + protected String username = "test"; + protected String password = "test"; + + public IMAPLoginHandler() { + capabilities += " LOGINDISABLED AUTH=LOGIN"; + } + + /** + * AUTHENTICATE LOGIN command. + * + * @throws IOException unable to read/write to socket + */ + public void authlogin(String ir) throws IOException { + if (ir != null) + bad("AUTHENTICATE LOGIN does not support initial response"); + cont(base64encode("Username")); + String resp = readLine(); + String u = new String(BASE64DecoderStream.decode( + resp.getBytes(StandardCharsets.US_ASCII)), + StandardCharsets.UTF_8); + cont(base64encode("Password")); + resp = readLine(); + String p = new String(BASE64DecoderStream.decode( + resp.getBytes(StandardCharsets.US_ASCII)), + StandardCharsets.UTF_8); + //System.out.printf("USER: %s, PASSWORD: %s%n", u, p); + if (!u.equals(username) || !p.equals(password)) { + no("authentication failed"); + return; + } + ok("[CAPABILITY " + capabilities + "]"); + } +}
diff --git a/mail/src/test/java/com/sun/mail/imap/IMAPLoginReferralTest.java b/mail/src/test/java/com/sun/mail/imap/IMAPLoginReferralTest.java new file mode 100644 index 0000000..4f9cc63 --- /dev/null +++ b/mail/src/test/java/com/sun/mail/imap/IMAPLoginReferralTest.java
@@ -0,0 +1,161 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.io.IOException; +import java.util.Properties; + +import javax.mail.Session; +import javax.mail.Store; + +import com.sun.mail.imap.ReferralException; + +import com.sun.mail.test.TestServer; + +import org.junit.Test; +import org.junit.Rule; +import org.junit.rules.Timeout; +import static org.junit.Assert.fail; +import static org.junit.Assert.assertEquals; + +/** + * Test IMAP login referrals (RFC 2221). + */ +public final class IMAPLoginReferralTest { + + private static final String REFERRAL_URL = "imap://test@server/"; + private static final String REFERRAL_MSG = "try server"; + private static final String REFERRAL = + "[REFERRAL " + REFERRAL_URL + "] " + REFERRAL_MSG; + + private static final int TIMEOUT = 1000; // 1 second + + // timeout the test in case of deadlock + @Rule + public Timeout deadlockTimeout = Timeout.millis(5 * TIMEOUT); + + /** + * Test referral in BYE when connecting. + */ + @Test + public void testConnectReferral() { + test(new IMAPHandler() { + @Override + public void sendGreetings() throws IOException { + bye(REFERRAL); + } + }); + } + + /** + * Test referral in NO response to LOGIN. + */ + @Test + public void testLoginReferral() { + test(new IMAPHandler() { + {{ capabilities += " LOGIN-REFERRALS"; }} + @Override + public void login() throws IOException { + no(REFERRAL); + } + }); + } + + /** + * Test referral in OK response to LOGIN. + */ + @Test + public void testLoginOkReferral() { + test(new IMAPHandler() { + {{ capabilities += " LOGIN-REFERRALS"; }} + @Override + public void login() throws IOException { + ok(REFERRAL); + } + }); + } + + /** + * Test referral in NO response to AUTHENTICATE PLAIN. + */ + @Test + public void testPlainReferral() { + test(new IMAPHandler() { + {{ capabilities += " LOGIN-REFERRALS AUTH=PLAIN"; }} + @Override + public void authplain(String ir) throws IOException { + no(REFERRAL); + } + }); + } + + /** + * Test referral in OK response to AUTHENTICATE PLAIN. + */ + @Test + public void testPlainOkReferral() { + test(new IMAPHandler() { + {{ capabilities += " LOGIN-REFERRALS AUTH=PLAIN"; }} + @Override + public void authplain(String ir) throws IOException { + if (ir == null) { + cont(""); + String resp = readLine(); + } + ok(REFERRAL); + } + }); + } + + private void test(IMAPHandler handler) { + TestServer server = null; + try { + server = new TestServer(handler); + server.start(); + + final Properties properties = new Properties(); + properties.setProperty("mail.imap.host", "localhost"); + properties.setProperty("mail.imap.port", "" + server.getPort()); + properties.setProperty("mail.imap.referralexception", "true"); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + final IMAPStore store = (IMAPStore)session.getStore("imap"); + try { + store.connect("test", "test"); + fail("connect succeeded"); + } catch (ReferralException ex) { + // success! + assertEquals(ex.getUrl(), REFERRAL_URL); + assertEquals(ex.getText(), REFERRAL_MSG); + } catch (Exception ex) { + System.out.println(ex); + //ex.printStackTrace(); + fail(ex.toString()); + } finally { + store.close(); + } + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + } + } + } +}
diff --git a/mail/src/test/java/com/sun/mail/imap/IMAPMessageNumberOutOfRangeTest.java b/mail/src/test/java/com/sun/mail/imap/IMAPMessageNumberOutOfRangeTest.java new file mode 100644 index 0000000..688bef2 --- /dev/null +++ b/mail/src/test/java/com/sun/mail/imap/IMAPMessageNumberOutOfRangeTest.java
@@ -0,0 +1,117 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.io.IOException; +import java.util.Properties; + +import javax.mail.Folder; +import javax.mail.Session; +import javax.mail.Store; +import javax.mail.Message; +import javax.mail.Flags; +import javax.mail.search.FlagTerm; + +import com.sun.mail.test.TestServer; + +import org.junit.Test; +import org.junit.Rule; +import org.junit.rules.Timeout; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +/** + * Test FETCH and SEARCH responses with a message number that's out of range. + */ +public final class IMAPMessageNumberOutOfRangeTest { + + // timeout the test in case of deadlock + @Rule + public Timeout deadlockTimeout = Timeout.seconds(20); + + @Test + public void test() { + TestServer server = null; + try { + final IMAPHandlerBad handler = new IMAPHandlerBad(); + server = new TestServer(handler); + server.start(); + + final Properties properties = new Properties(); + properties.setProperty("mail.imap.host", "localhost"); + properties.setProperty("mail.imap.port", "" + server.getPort()); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + final Store store = session.getStore("imap"); + Folder folder = null; + try { + store.connect("test", "test"); + folder = store.getFolder("INBOX"); + folder.open(Folder.READ_ONLY); + Message msg = folder.getMessage(1); + Flags f = msg.getFlags(); + Message[] msgs = folder.search( + new FlagTerm(new Flags(Flags.Flag.RECENT), true)); + assertEquals(1, msgs.length); + assertEquals(msg, msgs[0]); + } catch (Exception ex) { + System.out.println(ex); + //ex.printStackTrace(); + fail(ex.toString()); + } finally { + if (folder != null) + folder.close(false); + store.close(); + } + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + } + } + } + + /** + * Custom handler. Returns responses for messages that don't + * exist in the folder. + */ + private static final class IMAPHandlerBad extends IMAPHandler { + + @Override + public void examine(String line) throws IOException { + numberOfMessages = 1; + numberOfRecentMessages = 1; + super.examine(line); + } + + @Override + public void search(String line) throws IOException { + untagged("SEARCH 1 2"); + ok(); + } + + @Override + public void fetch(String line) throws IOException { + untagged("1 FETCH (FLAGS (\\Recent))"); + untagged("2 FETCH (FLAGS (\\Deleted))"); + ok(); + } + } +}
diff --git a/mail/src/test/java/com/sun/mail/imap/IMAPMessageTest.java b/mail/src/test/java/com/sun/mail/imap/IMAPMessageTest.java new file mode 100644 index 0000000..3f2e670 --- /dev/null +++ b/mail/src/test/java/com/sun/mail/imap/IMAPMessageTest.java
@@ -0,0 +1,401 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.util.Properties; +import java.util.StringTokenizer; +import java.util.Set; +import java.util.HashSet; + +import javax.mail.Folder; +import javax.mail.Session; +import javax.mail.Store; +import javax.mail.Message; +import javax.mail.Multipart; +import javax.mail.BodyPart; +import javax.mail.MessagingException; +import javax.mail.internet.MimeUtility; + +import com.sun.mail.test.TestServer; + +import org.junit.Test; +import org.junit.Rule; +import org.junit.rules.Timeout; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.fail; + +/** + * Test IMAPMessage methods. + */ +public final class IMAPMessageTest { + + // timeout the test in case of deadlock + @Rule + public Timeout deadlockTimeout = Timeout.seconds(20); + + private static final String RDATE = "23-Jun-2004 06:26:26 -0700"; + private static final String ENV_DATE = + "\"Wed, 23 Jun 2004 18:56:42 +0530\""; + private static final String ENV_SUBJECT = "\"test\""; + private static final String ENV_UTF8_ENCODED_SUBJECT = + "=?UTF-8?B?VVRGOCB0ZXN0OiDgsqzgsr4g4LKH4LKy4LON4LKy4LK/IOCyuOCygg==?= " + + "=?UTF-8?B?4LKt4LK14LK/4LK44LOBIOCyh+CyguCypuCzhuCyqA==?= " + + "=?UTF-8?B?4LON4LKoIOCyueCzg+CypuCyr+CypuCysuCyvyA=?="; + private static final String ENV_ADDRS = + "((\"JavaMail\" NIL \"testuser\" \"example.com\")) " + + "((\"JavaMail\" NIL \"testuser\" \"example.com\")) " + + "((\"JavaMail\" NIL \"testuser\" \"example.com\")) " + + "((NIL NIL \"testuser\" \"example.com\")) NIL NIL NIL " + + "\"<40D98512.9040803@example.com>\""; + private static final String ENVELOPE = + "(" + ENV_DATE + " " + ENV_SUBJECT + " " + ENV_ADDRS + ")"; + + public static abstract class IMAPTest { + public void init(Properties props) { }; + public abstract void test(Folder folder, IMAPHandlerMessage handler) + throws Exception; + } + + /** + * Test that a small message size is returned correctly. + */ + @Test + public void testSizeSmall() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Folder folder, IMAPHandlerMessage handler) + throws MessagingException { + Message m = folder.getMessage(1); + assertEquals(123, m.getSize()); + } + }, + new IMAPHandlerMessage() { + {{ size = 123; }} + }); + } + + /** + * Test that a large message size is returned as Integer.MAX_VALUE + * from MimeMessage.getSize and returned as the actual value from + * IMAPMessage.getSizeLong. + */ + @Test + public void testSizeLarge() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Folder folder, IMAPHandlerMessage handler) + throws MessagingException { + Message m = folder.getMessage(1); + assertEquals(Integer.MAX_VALUE, m.getSize()); + assertEquals((long)Integer.MAX_VALUE + 1, + ((IMAPMessage)m).getSizeLong()); + } + }, + new IMAPHandlerMessage() { + {{ size = (long)Integer.MAX_VALUE + 1; }} + }); + } + + /** + * Test that returning NIL instead of an empty string for the content + * of the message works correctly. + */ + @Test + public void testEmptyBody() { + testWithHandler( + new IMAPTest() { + @Override + public void init(Properties props) { + props.setProperty("mail.imap.partialfetch","false"); + } + + @Override + public void test(Folder folder, IMAPHandlerMessage handler) + throws MessagingException, IOException { + Message m = folder.getMessage(1); + String t = (String)m.getContent(); + assertEquals("", t); + } + }, + new IMAPHandlerMessage() { + @Override + public void fetch(String line) throws IOException { + if (line.indexOf("BODYSTRUCTURE") >= 0) + untagged("1 FETCH (BODYSTRUCTURE " + + "(\"text\" \"plain\" (\"charset\" \"us-ascii\") " + + "NIL NIL \"7bit\" 0 0 NIL NIL NIL NIL)" + + ")"); + else if (line.indexOf("BODY[TEXT]") >= 0) + untagged("1 FETCH (BODY[TEXT] NIL " + + "FLAGS (\\Seen \\Recent))"); + ok(); + } + }); + } + + @Test + public void testAttachementFileName() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Folder folder, IMAPHandlerMessage handler) throws MessagingException, IOException { + Message m = folder.getMessage(1); + Multipart mp = (Multipart)m.getContent(); + BodyPart bp = mp.getBodyPart(1); + assertEquals("filename.csv", MimeUtility.decodeText(bp.getFileName())); + } + }, + new IMAPHandlerMessage() { + @Override + public void fetch(String line) throws IOException { + untagged("1 FETCH (BODYSTRUCTURE (" + + "(\"text\" \"html\" (\"charset\" \"utf-8\") NIL NIL \"base64\" 402 6 NIL NIL NIL NIL)" + + "(\"application\" \"octet-stream\" (\"name\" \"=?utf-8?B?ZmlsZW5hbWU=?= =?utf-8?B?LmNzdg==?=\") NIL NIL \"base64\" 658 NIL " + + "(\"attachment\" (\"filename\" \"\")) NIL NIL) \"mixed\" " + + "(\"boundary\" \"--boundary_539_27806e16-2599-4612-b98a-69335bedd206\") NIL NIL NIL))" + ); + ok(); + } + } + ); + } + + /** + * Test that returning NIL instead of an empty string for the content + * of an empty body part works correctly. + */ + @Test + public void testEmptyBodyAttachment() { + testWithHandler( + new IMAPTest() { + @Override + public void init(Properties props) { + props.setProperty("mail.imap.partialfetch","false"); + } + + @Override + public void test(Folder folder, IMAPHandlerMessage handler) + throws MessagingException, IOException { + Message m = folder.getMessage(1); + Multipart mp = (Multipart)m.getContent(); + BodyPart bp = mp.getBodyPart(1); + String t = (String)bp.getContent(); + assertEquals("", t); + } + }, + new IMAPHandlerMessage() { + @Override + public void fetch(String line) throws IOException { + if (line.indexOf("BODYSTRUCTURE") >= 0) + untagged("1 FETCH (BODYSTRUCTURE (" + + "(\"text\" \"plain\" (\"charset\" \"us-ascii\") " + + "NIL NIL \"7bit\" 4 0 NIL NIL NIL NIL)" + + "(\"text\" \"plain\" (\"charset\" \"us-ascii\") " + + "NIL NIL \"7bit\" 0 0 NIL NIL NIL NIL)" + + " \"mixed\" (\"boundary\" \"----=_x\") NIL NIL))"); + else if (line.indexOf("BODY[2]") >= 0) + untagged("1 FETCH (BODY[2] NIL " + + "FLAGS (\\Seen \\Recent))"); + ok(); + } + }); + } + + /** + * Test that returning NIL instead of an empty string for the content + * of an empty body part works correctly. + * This is a bug in office365.com. Note the space in "base64 ". + */ + @Test + public void testBadEncoding() { + testWithHandler( + new IMAPTest() { + @Override + public void init(Properties props) { + props.setProperty("mail.imap.partialfetch","false"); + } + + @Override + public void test(Folder folder, IMAPHandlerMessage handler) + throws MessagingException, IOException { + Message m = folder.getMessage(1); + Multipart mp = (Multipart)m.getContent(); + BodyPart bp = mp.getBodyPart(1); + StringBuilder sb = new StringBuilder(); + try (InputStream is = bp.getInputStream()) { + int c; + while ((c = is.read()) != -1) + sb.append((char)c); + } + assertEquals("test", sb.toString()); + } + }, + new IMAPHandlerMessage() { + @Override + public void fetch(String line) throws IOException { + if (line.indexOf("BODYSTRUCTURE") >= 0) + untagged("1 FETCH (BODYSTRUCTURE (" + + "(\"text\" \"plain\" (\"charset\" \"us-ascii\") " + + "NIL NIL \"7bit\" 0 0 NIL NIL NIL NIL)" + + "(\"application\" \"octet-stream\" " + + "(\"name\" \"test.txt\") NIL NIL \"base64 \" " + + "8 NIL NIL NIL NIL) " + + "\"mixed\" (\"boundary\" \"=_x\") NIL NIL))"); + else if (line.indexOf("BODY[2]") >= 0) + untagged("1 FETCH (BODY[2] \"dGVzdA==\" " + + "FLAGS (\\Seen \\Recent))"); + ok(); + } + }); + } + + + /** + * Test that a UTF-8 encoded Subject is decoded properly. + */ + @Test + public void testUtf8SubjectEncoded() { + String s = null; + try { + s = MimeUtility.decodeText(ENV_UTF8_ENCODED_SUBJECT); + } catch (UnsupportedEncodingException ex) { + } + final String subject = s; + + testWithHandler( + new IMAPTest() { + @Override + public void test(Folder folder, IMAPHandlerMessage handler) + throws MessagingException { + Message m = folder.getMessage(1); + assertEquals(subject, m.getSubject()); + } + }, + new IMAPHandlerMessage() { + {{ + envelope = "(" + ENV_DATE + " \"" + + ENV_UTF8_ENCODED_SUBJECT + "\" " + + ENV_ADDRS + ")"; + }} + }); + } + + /** + * Test that a UTF-8 Subject is decoded properly. + */ + @Test + public void testUtf8Subject() { + String s = null; + try { + s = MimeUtility.decodeText(ENV_UTF8_ENCODED_SUBJECT); + } catch (UnsupportedEncodingException ex) { + } + final String subject = s; + + testWithHandler( + new IMAPTest() { + @Override + public void test(Folder folder, IMAPHandlerMessage handler) + throws MessagingException { + Message m = folder.getMessage(1); + assertEquals(subject, m.getSubject()); + } + }, + new IMAPHandlerMessage() { + {{ + envelope = "(" + ENV_DATE + " \"" + subject + "\" " + + ENV_ADDRS + ")"; + capabilities += " ENABLE UTF8=ACCEPT"; + }} + + @Override + public void enable(String line) throws IOException { + ok(); + } + }); + } + + private void testWithHandler(IMAPTest test, IMAPHandlerMessage handler) { + TestServer server = null; + try { + server = new TestServer(handler); + server.start(); + + final Properties properties = new Properties(); + properties.setProperty("mail.imap.host", "localhost"); + properties.setProperty("mail.imap.port", "" + server.getPort()); + test.init(properties); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + final Store store = session.getStore("imap"); + Folder folder = null; + try { + store.connect("test", "test"); + folder = store.getFolder("INBOX"); + folder.open(Folder.READ_ONLY); + test.test(folder, handler); + } catch (Exception ex) { + System.out.println(ex); + //ex.printStackTrace(); + fail(ex.toString()); + } finally { + if (folder != null) + folder.close(false); + store.close(); + } + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + } + } + } + + /** + * Custom handler. + */ + private static class IMAPHandlerMessage extends IMAPHandler { + + String rdate = RDATE; + String envelope = ENVELOPE; + long size = 0; + + @Override + public void examine(String line) throws IOException { + numberOfMessages = 1; + super.examine(line); + } + + @Override + public void fetch(String line) throws IOException { + untagged("1 FETCH (ENVELOPE " + envelope + + " INTERNALDATE \"" + rdate + "\" " + + "RFC822.SIZE " + size + ")"); + ok(); + } + } +}
diff --git a/mail/src/test/java/com/sun/mail/imap/IMAPPlainHandler.java b/mail/src/test/java/com/sun/mail/imap/IMAPPlainHandler.java new file mode 100644 index 0000000..e0514f8 --- /dev/null +++ b/mail/src/test/java/com/sun/mail/imap/IMAPPlainHandler.java
@@ -0,0 +1,62 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import com.sun.mail.util.BASE64DecoderStream; + +/** + * Handle IMAP connection with PLAIN authentication. + * + * @author Bill Shannon + */ +public class IMAPPlainHandler extends IMAPHandler { + + protected String username = "test"; + protected String password = "test"; + + public IMAPPlainHandler() { + capabilities += " LOGINDISABLED AUTH=PLAIN"; + } + + /** + * AUTHENTICATE PLAIN command. + * + * @throws IOException unable to read/write to socket + */ + @Override + public void authplain(String ir) throws IOException { + if (ir == null) { + cont(""); + ir = readLine(); + } + String auth = new String(BASE64DecoderStream.decode( + ir.getBytes(StandardCharsets.US_ASCII)), + StandardCharsets.UTF_8); + String[] ap = auth.split("\000"); + String u = ap[1]; + String p = ap[2]; + //System.out.printf("USER: %s, PASSWORD: %s%n", u, p); + if (!u.equals(username) || !p.equals(password)) { + no("authentication failed"); + return; + } + ok("[CAPABILITY " + capabilities + "]"); + } +}
diff --git a/mail/src/test/java/com/sun/mail/imap/IMAPResponseEventTest.java b/mail/src/test/java/com/sun/mail/imap/IMAPResponseEventTest.java new file mode 100644 index 0000000..c2e3396 --- /dev/null +++ b/mail/src/test/java/com/sun/mail/imap/IMAPResponseEventTest.java
@@ -0,0 +1,131 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.io.IOException; +import java.util.Properties; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import javax.mail.Session; +import javax.mail.Store; +import javax.mail.event.StoreListener; +import javax.mail.event.StoreEvent; + +import com.sun.mail.test.TestServer; + +import org.junit.Test; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * Test IMAP response events. + */ +public final class IMAPResponseEventTest { + + private volatile boolean gotResponse; + + /** + * Test that response events are sent for the LOGIN command. + */ + @Test + public void testLoginResponseEvent() { + testLogin(""); + } + + /** + * Test that response events are sent for the AUTHENTICATE LOGIN command. + */ + @Test + public void testAuthLoginResponseEvent() { + testLogin("LOGINDISABLED AUTH=LOGIN"); + } + + /** + * Test that response events are sent for the AUTHENTICATE PLAIN command. + */ + @Test + public void testAuthPlainResponseEvent() { + testLogin("LOGINDISABLED AUTH=PLAIN"); + } + + private void testLogin(String type) { + TestServer server = null; + try { + final IMAPHandler handler = new IMAPHandlerLogin(type); + server = new TestServer(handler); + server.start(); + + final Properties properties = new Properties(); + properties.setProperty("mail.imap.host", "localhost"); + properties.setProperty("mail.imap.port", "" + server.getPort()); + properties.setProperty("mail.imap.enableresponseevents", "true"); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + final CountDownLatch latch = new CountDownLatch(1); + + final Store store = session.getStore("imap"); + store.addStoreListener(new StoreListener() { + @Override + public void notification(StoreEvent e) { + String s; + if (e.getMessageType() == IMAPStore.RESPONSE) { + s = "RESPONSE: "; + // is this the expected AUTHENTICATE response? + if (e.getMessage().indexOf("X-LOGIN-SUCCESS") >= 0) + gotResponse = true; + latch.countDown(); + } else + s = "OTHER: "; + //System.out.println(s + e.getMessage()); + } + }); + gotResponse = false; + try { + store.connect("test", "test"); + // time for event to be delivered + latch.await(5, TimeUnit.SECONDS); + assertTrue(gotResponse); + + } catch (Exception ex) { + System.out.println(ex); + //ex.printStackTrace(); + fail(ex.toString()); + } finally { + store.close(); + } + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + } + } + } + + /** + * Custom handler. Forces use of specific login type and includes + * a fake capability to be included in the OK response that we + * will check for success. + */ + private static final class IMAPHandlerLogin extends IMAPHandler { + public IMAPHandlerLogin(String type) { + capabilities += " " + type + " X-LOGIN-SUCCESS"; + } + } +}
diff --git a/mail/src/test/java/com/sun/mail/imap/IMAPSaslHandler.java b/mail/src/test/java/com/sun/mail/imap/IMAPSaslHandler.java new file mode 100644 index 0000000..b8c1062 --- /dev/null +++ b/mail/src/test/java/com/sun/mail/imap/IMAPSaslHandler.java
@@ -0,0 +1,156 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.io.IOException; +import java.util.StringTokenizer; +import java.util.logging.Logger; +import java.util.logging.Level; + +import javax.security.sasl.*; +import javax.security.auth.callback.*; + +import com.sun.mail.util.BASE64EncoderStream; +import com.sun.mail.util.BASE64DecoderStream; +import com.sun.mail.util.ASCIIUtility; + +/** + * Handle IMAP connection with SASL authentication. + * + * @author Bill Shannon + */ +public class IMAPSaslHandler extends IMAPHandler { + + public IMAPSaslHandler() { + capabilities += " LOGINDISABLED AUTH=DIGEST-MD5"; + } + + /** + * AUTHENTICATE command. + * + * @throws IOException unable to read/write to socket + */ + @Override + public void authenticate(String mech, String ir) throws IOException { + final String u = "test"; + final String p = "test"; + final String realm = "test"; + + CallbackHandler cbh = new CallbackHandler() { + @Override + public void handle(Callback[] callbacks) { + if (LOGGER.isLoggable(Level.FINE)) + LOGGER.fine("SASL callback length: " + callbacks.length); + for (int i = 0; i < callbacks.length; i++) { + if (LOGGER.isLoggable(Level.FINE)) + LOGGER.fine("SASL callback " + i + ": " + callbacks[i]); + if (callbacks[i] instanceof NameCallback) { + NameCallback ncb = (NameCallback)callbacks[i]; + ncb.setName(u); + } else if (callbacks[i] instanceof PasswordCallback) { + PasswordCallback pcb = (PasswordCallback)callbacks[i]; + pcb.setPassword(p.toCharArray()); + } else if (callbacks[i] instanceof AuthorizeCallback) { + AuthorizeCallback ac = (AuthorizeCallback)callbacks[i]; + if (LOGGER.isLoggable(Level.FINE)) + LOGGER.fine("SASL authorize: " + + "authn: " + ac.getAuthenticationID() + ", " + + "authz: " + ac.getAuthorizationID() + ", " + + "authorized: " + ac.getAuthorizedID()); + ac.setAuthorized(true); + } else if (callbacks[i] instanceof RealmCallback) { + RealmCallback rcb = (RealmCallback)callbacks[i]; + rcb.setText(realm != null ? + realm : rcb.getDefaultText()); + } else if (callbacks[i] instanceof RealmChoiceCallback) { + RealmChoiceCallback rcb = + (RealmChoiceCallback)callbacks[i]; + if (realm == null) + rcb.setSelectedIndex(rcb.getDefaultChoice()); + else { + // need to find specified realm in list + String[] choices = rcb.getChoices(); + for (int k = 0; k < choices.length; k++) { + if (choices[k].equals(realm)) { + rcb.setSelectedIndex(k); + break; + } + } + } + } + } + } + }; + + SaslServer ss; + try { + ss = Sasl.createSaslServer(mech, "imap", "localhost", null, cbh); + } catch (SaslException sex) { + LOGGER.log(Level.FINE, "Failed to create SASL server", sex); + no("Failed to create SASL server"); + return; + } + if (ss == null) { + LOGGER.fine("No SASL support"); + no("No SASL support"); + return; + } + if (LOGGER.isLoggable(Level.FINE)) + LOGGER.fine("SASL server " + ss.getMechanismName()); + + byte[] response = new byte[0]; + while (!ss.isComplete()) { + try { + byte[] chal = ss.evaluateResponse(response); + if (ss.isComplete()) { + break; + } else { + // send challenge + if (LOGGER.isLoggable(Level.FINE)) + LOGGER.fine("SASL challenge: " + + ASCIIUtility.toString(chal, 0, chal.length)); + byte[] ba = BASE64EncoderStream.encode(chal); + if (ba.length > 0) + cont(ASCIIUtility.toString(ba, 0, ba.length)); + else + cont(); + // read response + String resp = readLine(); + response = resp.getBytes(); + response = BASE64DecoderStream.decode(response); + } + } catch (SaslException ex) { + no(ex.toString()); + break; + } + } + + if (ss.isComplete() /*&& status == SUCCESS*/) { + String qop = (String)ss.getNegotiatedProperty(Sasl.QOP); + if (qop != null && (qop.equalsIgnoreCase("auth-int") || + qop.equalsIgnoreCase("auth-conf"))) { + // XXX - NOT SUPPORTED!!! + LOGGER.fine( + "SASL Mechanism requires integrity or confidentiality"); + no("SASL Mechanism requires integrity or confidentiality"); + return; + } + } + + ok("[CAPABILITY " + capabilities + "]"); + } +}
diff --git a/mail/src/test/java/com/sun/mail/imap/IMAPSaslLoginTest.java b/mail/src/test/java/com/sun/mail/imap/IMAPSaslLoginTest.java new file mode 100644 index 0000000..f8c27f1 --- /dev/null +++ b/mail/src/test/java/com/sun/mail/imap/IMAPSaslLoginTest.java
@@ -0,0 +1,78 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.io.*; +import java.util.Properties; + +import javax.mail.Session; +import javax.mail.Store; +import javax.mail.MessagingException; + +import com.sun.mail.test.TestServer; + +import org.junit.Test; +import static org.junit.Assert.fail; + +/** + * Test login using a SASL mechanism. + */ +public final class IMAPSaslLoginTest { + + /** + * Test that login using a SASL mechanism works. + */ + @Test + public void testSaslLogin() { + TestServer server = null; + try { + IMAPHandler handler = new IMAPSaslHandler(); + server = new TestServer(handler); + server.start(); + + Properties properties = new Properties(); + properties.setProperty("mail.imap.host", "localhost"); + properties.setProperty("mail.imap.port", "" + server.getPort()); + properties.setProperty("mail.imap.sasl.enable", "true"); + properties.setProperty("mail.imap.sasl.mechanisms", "DIGEST-MD5"); + Session session = Session.getInstance(properties); + //session.setDebug(true); + + Store store = session.getStore("imap"); + try { + store.connect("test", "test"); + // success! + } catch (MessagingException mex) { + fail("login failed"); + } catch (Exception ex) { + System.out.println(ex); + //ex.printStackTrace(); + fail(ex.toString()); + } finally { + store.close(); + } + + } catch (Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + } + } + } +}
diff --git a/mail/src/test/java/com/sun/mail/imap/IMAPSearchTest.java b/mail/src/test/java/com/sun/mail/imap/IMAPSearchTest.java new file mode 100644 index 0000000..be262a7 --- /dev/null +++ b/mail/src/test/java/com/sun/mail/imap/IMAPSearchTest.java
@@ -0,0 +1,157 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.io.IOException; +import java.util.Properties; + +import javax.mail.Folder; +import javax.mail.Session; +import javax.mail.Store; +import javax.mail.Message; +import javax.mail.search.*; + +import com.sun.mail.test.TestServer; + +import org.junit.Test; +import org.junit.Rule; +import org.junit.rules.Timeout; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +/** + * Test the search method. + */ +public final class IMAPSearchTest { + + // timeout the test in case of deadlock + @Rule + public Timeout deadlockTimeout = Timeout.seconds(20); + + @Test + public void testWithinNotSupported() { + TestServer server = null; + try { + server = new TestServer(new IMAPHandler() { + @Override + public void search(String line) throws IOException { + bad("WITHIN not supported"); + } + }); + server.start(); + + final Properties properties = new Properties(); + properties.setProperty("mail.imap.host", "localhost"); + properties.setProperty("mail.imap.port", "" + server.getPort()); + properties.setProperty("mail.imap.throwsearchexception", "true"); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + final Store store = session.getStore("imap"); + Folder folder = null; + try { + store.connect("test", "test"); + folder = store.getFolder("INBOX"); + folder.open(Folder.READ_ONLY); + Message[] msgs = folder.search(new YoungerTerm(1)); + fail("search didn't fail"); + } catch (SearchException ex) { + // success! + } catch (Exception ex) { + System.out.println(ex); + //ex.printStackTrace(); + fail(ex.toString()); + } finally { + if (folder != null) + folder.close(false); + store.close(); + } + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + } + } + } + + /** + * Test that when the server supports UTF8 and the client enables it, + * the client doesn't issue a SEARCH CHARSET command even if the search + * term includes a non-ASCII character. + * (see RFC 6855, section 3, last paragraph) + */ + @Test + public void testUtf8Search() { + TestServer server = null; + try { + server = new TestServer(new IMAPUtf8Handler() { + @Override + public void search(String line) throws IOException { + if (line.contains("CHARSET")) + bad("CHARSET not supported"); + else + ok(); + } + }); + server.start(); + + final Properties properties = new Properties(); + properties.setProperty("mail.imap.host", "localhost"); + properties.setProperty("mail.imap.port", "" + server.getPort()); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + final Store store = session.getStore("imap"); + Folder folder = null; + try { + store.connect("test", "test"); + folder = store.getFolder("INBOX"); + folder.open(Folder.READ_ONLY); + Message[] msgs = folder.search(new SubjectTerm("\u2019")); + } catch (Exception ex) { + System.out.println(ex); + //ex.printStackTrace(); + fail(ex.toString()); + } finally { + if (folder != null) + folder.close(false); + store.close(); + } + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + } + } + } + + /** + * An IMAPHandler that enables UTF-8 support. + */ + private static class IMAPUtf8Handler extends IMAPHandler { + {{ capabilities += " ENABLE UTF8=ACCEPT"; }} + + @Override + public void enable(String line) throws IOException { + ok(); + } + } +}
diff --git a/mail/src/test/java/com/sun/mail/imap/IMAPStoreTest.java b/mail/src/test/java/com/sun/mail/imap/IMAPStoreTest.java new file mode 100644 index 0000000..5bb6312 --- /dev/null +++ b/mail/src/test/java/com/sun/mail/imap/IMAPStoreTest.java
@@ -0,0 +1,417 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.io.IOException; +import java.util.Properties; +import java.util.StringTokenizer; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.mail.Folder; +import javax.mail.Session; +import javax.mail.Store; +import javax.mail.MessagingException; + +import com.sun.mail.test.TestServer; + +import org.junit.Test; +import org.junit.Rule; +import org.junit.rules.Timeout; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.fail; + +/** + * Test IMAPStore methods. + */ +public final class IMAPStoreTest { + + // timeout the test in case of deadlock + @Rule + public Timeout deadlockTimeout = Timeout.seconds(20); + + private static final String utf8Folder = "test\u03b1"; + private static final String utf7Folder = "test&A7E-"; + + public static abstract class IMAPTest { + public void init(Properties props) { }; + public void test(Store store, TestServer server) throws Exception { }; + } + + /** + * Test that UTF-8 user name works with LOGIN authentication. + */ + @Test + public void testUtf8UsernameLogin() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Store store, TestServer server) + throws MessagingException, IOException { + store.connect(utf8Folder, utf8Folder); + } + }, + new IMAPLoginHandler() { + @Override + public void authlogin(String ir) + throws IOException { + username = utf8Folder; + password = utf8Folder; + super.authlogin(ir); + } + }); + } + + /** + * Test that UTF-8 user name works with PLAIN authentication. + */ + @Test + public void testUtf8UsernamePlain() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Store store, TestServer server) + throws MessagingException, IOException { + store.connect(utf8Folder, utf8Folder); + } + }, + new IMAPPlainHandler() { + @Override + public void authplain(String ir) + throws IOException { + username = utf8Folder; + password = utf8Folder; + super.authplain(ir); + } + }); + } + + /** + * Test that UTF-7 folder names in the NAMESPACE command are + * decoded properly. + */ + @Test + public void testUtf7Namespaces() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Store store, TestServer server) + throws MessagingException, IOException { + store.connect("test", "test"); + Folder[] pub = ((IMAPStore)store).getSharedNamespaces(); + assertEquals(utf8Folder, pub[0].getName()); + } + }, + new IMAPHandler() { + {{ capabilities += " NAMESPACE"; }} + @Override + public void namespace() throws IOException { + untagged("NAMESPACE ((\"\" \"/\")) ((\"~\" \"/\")) " + + "((\"" + utf7Folder + "/\" \"/\"))"); + ok(); + } + }); + } + + /** + * Test that using a UTF-8 folder name results in the proper UTF-8 + * unencoded name for the CREATE command. + */ + @Test + public void testUtf8FolderNameCreate() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Store store, TestServer server) + throws MessagingException, IOException { + store.connect("test", "test"); + Folder test = store.getFolder(utf8Folder); + assertTrue(test.create(Folder.HOLDS_MESSAGES)); + } + }, + new IMAPUtf8Handler() { + @Override + public void create(String line) throws IOException { + StringTokenizer st = new StringTokenizer(line); + st.nextToken(); // skip tag + st.nextToken(); // skip "CREATE" + String name = unquote(st.nextToken()); + if (name.equals(utf8Folder)) + ok(); + else + no("wrong name"); + } + + @Override + public void list(String line) throws IOException { + untagged("LIST (\\HasNoChildren) \"/\" \"" + + utf8Folder + "\""); + ok(); + } + }); + } + + /** + * Test that Store.close also closes open Folders. + */ + @Test + public void testCloseClosesFolder() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Store store, TestServer server) + throws MessagingException, IOException { + store.connect("test", "test"); + Folder test = store.getFolder("INBOX"); + test.open(Folder.READ_ONLY); + store.close(); + assertFalse(test.isOpen()); + assertEquals(1, server.clientCount()); + server.waitForClients(1); + // test will timeout if clients don't terminate + } + }, + new IMAPHandler() { + }); + } + + /** + * Test that Store.close closes connections in the pool. + */ + @Test + public void testCloseEmptiesPool() { + testWithHandler( + new IMAPTest() { + @Override + public void init(Properties props) { + props.setProperty("mail.imap.connectionpoolsize", "2"); + } + + @Override + public void test(Store store, TestServer server) + throws MessagingException, IOException { + store.connect("test", "test"); + Folder test = store.getFolder("INBOX"); + test.open(Folder.READ_ONLY); + Folder test2 = store.getFolder("INBOX"); + test2.open(Folder.READ_ONLY); + test.close(false); + test2.close(false); + store.close(); + assertEquals(2, server.clientCount()); + server.waitForClients(2); + // test will timeout if clients don't terminate + } + }, + new IMAPHandler() { + }); + } + + /** + * Test that Store failures don't close Folders. + */ + @Test + public void testStoreFailureDoesNotCloseFolder() { + testWithHandler( + new IMAPTest() { + @Override + public void init(Properties props) { + props.setProperty( + "mail.imap.closefoldersonstorefailure", "false"); + } + + @Override + public void test(Store store, TestServer server) + throws MessagingException, IOException { + store.connect("test", "test"); + Folder test = store.getFolder("INBOX"); + test.open(Folder.READ_ONLY); + try { + ((IMAPStore)store).getSharedNamespaces(); + fail("MessagingException expected"); + } catch (MessagingException mex) { + // expected + } + assertTrue(test.isOpen()); + store.close(); + assertFalse(test.isOpen()); + assertEquals(2, server.clientCount()); + server.waitForClients(2); + // test will timeout if clients don't terminate + } + }, + new IMAPHandler() { + {{ capabilities += " NAMESPACE"; }} + + @Override + public void namespace() throws IOException { + exit(); + } + }); + } + + /** + * Test that Store.close after Store failure will close all Folders + * and empty the connectin pool. + */ + @Test + public void testCloseAfterFailure() { + testWithHandler( + new IMAPTest() { + @Override + public void init(Properties props) { + props.setProperty( + "mail.imap.closefoldersonstorefailure", "false"); + } + + @Override + public void test(Store store, TestServer server) + throws MessagingException, IOException { + store.connect("test", "test"); + Folder test = store.getFolder("INBOX"); + test.open(Folder.READ_ONLY); + try { + ((IMAPStore)store).getSharedNamespaces(); + fail("MessagingException expected"); + } catch (MessagingException mex) { + // expected + } + assertTrue(test.isOpen()); + test.close(); // put it back in the pool + store.close(); + assertEquals(2, server.clientCount()); + server.waitForClients(2); + // test will timeout if clients don't terminate + } + }, + new IMAPHandler() { + {{ capabilities += " NAMESPACE"; }} + + @Override + public void namespace() throws IOException { + exit(); + } + }); + } + + /** + * Test that Store failures do close Folders. + */ + @Test + public void testStoreFailureDoesCloseFolder() { + testWithHandler( + new IMAPTest() { + @Override + public void init(Properties props) { + props.setProperty( + // the default, but just to be sure... + "mail.imap.closefoldersonstorefailure", "true"); + } + + @Override + public void test(Store store, TestServer server) + throws MessagingException, IOException { + store.connect("test", "test"); + Folder test = store.getFolder("INBOX"); + test.open(Folder.READ_ONLY); + try { + ((IMAPStore)store).getSharedNamespaces(); + fail("MessagingException expected"); + } catch (MessagingException mex) { + // expected + } + assertFalse(test.isOpen()); + store.close(); + assertEquals(2, server.clientCount()); + server.waitForClients(2); + // test will timeout if clients don't terminate + } + }, + new IMAPHandler() { + {{ capabilities += " NAMESPACE"; }} + + @Override + public void namespace() throws IOException { + exit(); + } + }); + } + + private void testWithHandler(IMAPTest test, IMAPHandler handler) { + TestServer server = null; + try { + server = new TestServer(handler); + server.start(); + + final Properties properties = new Properties(); + properties.setProperty("mail.imap.host", "localhost"); + properties.setProperty("mail.imap.port", "" + server.getPort()); + test.init(properties); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + final Store store = session.getStore("imap"); + try { + test.test(store, server); + } catch (Exception ex) { + System.out.println(ex); + //ex.printStackTrace(); + fail(ex.toString()); + } finally { + store.close(); + } + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + } + } + } + + private static String unquote(String s) { + if (s.startsWith("\"") && s.endsWith("\"") && s.length() > 1) { + s = s.substring(1, s.length() - 1); + // check for any escaped characters + if (s.indexOf('\\') >= 0) { + StringBuilder sb = new StringBuilder(s.length()); // approx + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c == '\\' && i < s.length() - 1) + c = s.charAt(++i); + sb.append(c); + } + s = sb.toString(); + } + } + return s; + } + + /** + * An IMAPHandler that enables UTF-8 support. + */ + private static class IMAPUtf8Handler extends IMAPHandler { + {{ capabilities += " ENABLE UTF8=ACCEPT"; }} + + @Override + public void enable(String line) throws IOException { + ok(); + } + } +}
diff --git a/mail/src/test/java/com/sun/mail/imap/IMAPUidExpungeTest.java b/mail/src/test/java/com/sun/mail/imap/IMAPUidExpungeTest.java new file mode 100644 index 0000000..bdbc4e1 --- /dev/null +++ b/mail/src/test/java/com/sun/mail/imap/IMAPUidExpungeTest.java
@@ -0,0 +1,468 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.io.IOException; +import java.util.Properties; +import java.util.StringTokenizer; + +import javax.mail.Folder; +import javax.mail.Session; +import javax.mail.Store; +import javax.mail.Message; +import javax.mail.UIDFolder; +import javax.mail.MessagingException; + +import com.sun.mail.test.TestServer; + +import org.junit.Test; +import org.junit.Rule; +import org.junit.rules.Timeout; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * Test EXPUNGE responses during UID FETCH. + */ +public final class IMAPUidExpungeTest { + + // timeout the test in case of deadlock + @Rule + public Timeout deadlockTimeout = Timeout.seconds(20); + + public static interface IMAPTest { + public void test(Folder folder, IMAPHandlerExpunge handler) + throws MessagingException; + } + + @Test + public void testUIDSingle() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Folder folder, IMAPHandlerExpunge handler) + throws MessagingException { + Message m = ((UIDFolder)folder).getMessageByUID(2); + m.getFlags(); + assertEquals(1, handler.getSeqNum()); + } + }, + new IMAPHandlerExpunge() { + @Override + public void uidfetch(String line) throws IOException { + untagged("2 FETCH (UID 2)"); + untagged("1 EXPUNGE"); + untagged("3 EXISTS"); + numberOfMessages--; + ok(); + } + }); + } + + @Test + public void testUIDSingle2() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Folder folder, IMAPHandlerExpunge handler) + throws MessagingException { + Message m = ((UIDFolder)folder).getMessageByUID(2); + m.getFlags(); + assertEquals(2, handler.getSeqNum()); + } + }, + new IMAPHandlerExpunge() { + @Override + public void uidfetch(String line) throws IOException { + untagged("2 FETCH (UID 2)"); + untagged("3 EXPUNGE"); + untagged("3 EXISTS"); + numberOfMessages--; + ok(); + } + }); + } + + @Test + public void testUIDRange() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Folder folder, IMAPHandlerExpunge handler) + throws MessagingException { + Message[] msgs = ((UIDFolder)folder).getMessagesByUID(2, 4); + assertTrue(msgs[1] == null || msgs[1].isExpunged()); + msgs[0].getFlags(); + assertEquals(2, handler.getSeqNum()); + msgs[2].getFlags(); + assertEquals(3, handler.getSeqNum()); + } + }, + new IMAPHandlerExpunge() { + @Override + public void uidfetch(String line) throws IOException { + untagged("2 FETCH (UID 2)"); + untagged("3 FETCH (UID 3)"); + untagged("4 FETCH (UID 4)"); + untagged("3 EXPUNGE"); + untagged("3 EXISTS"); + numberOfMessages--; + ok(); + } + }); + } + + @Test + public void testUIDRange2() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Folder folder, IMAPHandlerExpunge handler) + throws MessagingException { + Message[] msgs = ((UIDFolder)folder).getMessagesByUID(2, 4); + assertTrue(msgs[1] == null || msgs[1].isExpunged()); + msgs[0].getFlags(); + assertEquals(2, handler.getSeqNum()); + msgs[2].getFlags(); + assertEquals(3, handler.getSeqNum()); + } + }, + new IMAPHandlerExpunge() { + @Override + public void uidfetch(String line) throws IOException { + untagged("2 FETCH (UID 2)"); + untagged("3 FETCH (UID 3)"); + untagged("3 EXPUNGE"); + untagged("3 EXISTS"); + untagged("3 FETCH (UID 4)"); + numberOfMessages--; + ok(); + } + }); + } + + @Test + public void testUIDRange3() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Folder folder, IMAPHandlerExpunge handler) + throws MessagingException { + Message[] msgs = ((UIDFolder)folder).getMessagesByUID(2, 4); + // UID 3 is unknown and not returned + assertEquals(2, msgs.length); + msgs[0].getFlags(); + assertEquals(2, handler.getSeqNum()); + msgs[1].getFlags(); + assertEquals(3, handler.getSeqNum()); + } + }, + new IMAPHandlerExpunge() { + @Override + public void uidfetch(String line) throws IOException { + untagged("2 FETCH (UID 2)"); + untagged("3 EXPUNGE"); + untagged("3 EXISTS"); + untagged("3 FETCH (UID 4)"); + numberOfMessages--; + ok(); + } + }); + } + + @Test + public void testUIDRange4() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Folder folder, IMAPHandlerExpunge handler) + throws MessagingException { + Message[] msgs = ((UIDFolder)folder).getMessagesByUID(1, 3); + assertEquals(3, msgs.length); + msgs[0].getFlags(); + assertEquals(1, handler.getSeqNum()); + msgs[1].getFlags(); + assertEquals(2, handler.getSeqNum()); + msgs[2].getFlags(); + assertEquals(3, handler.getSeqNum()); + } + }, + new IMAPHandlerExpunge() { + @Override + public void uidfetch(String line) throws IOException { + untagged("1 FETCH (UID 1)"); + untagged("2 FETCH (UID 2)"); + untagged("3 FETCH (UID 3)"); + untagged("4 EXPUNGE"); + untagged("3 EXISTS"); + numberOfMessages--; + ok(); + } + }); + } + + @Test + public void testUIDRange5() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Folder folder, IMAPHandlerExpunge handler) + throws MessagingException { + Message[] msgs = ((UIDFolder)folder).getMessagesByUID(2, 4); + assertEquals(3, msgs.length); + msgs[0].getFlags(); + assertEquals(1, handler.getSeqNum()); + msgs[1].getFlags(); + assertEquals(2, handler.getSeqNum()); + msgs[2].getFlags(); + assertEquals(3, handler.getSeqNum()); + } + }, + new IMAPHandlerExpunge() { + @Override + public void uidfetch(String line) throws IOException { + untagged("2 FETCH (UID 2)"); + untagged("3 FETCH (UID 3)"); + untagged("4 FETCH (UID 4)"); + untagged("1 EXPUNGE"); + untagged("3 EXISTS"); + numberOfMessages--; + ok(); + } + }); + } + + @Test + public void testUIDList() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Folder folder, IMAPHandlerExpunge handler) + throws MessagingException { + Message[] msgs = ((UIDFolder)folder).getMessagesByUID( + new long[] { 2, 3, 4 }); + assertTrue(msgs[1] == null || msgs[1].isExpunged()); + msgs[0].getFlags(); + assertEquals(2, handler.getSeqNum()); + msgs[2].getFlags(); + assertEquals(3, handler.getSeqNum()); + } + }, + new IMAPHandlerExpunge() { + @Override + public void uidfetch(String line) throws IOException { + untagged("2 FETCH (UID 2)"); + untagged("3 FETCH (UID 3)"); + untagged("4 FETCH (UID 4)"); + untagged("3 EXPUNGE"); + untagged("3 EXISTS"); + numberOfMessages--; + ok(); + } + }); + } + + @Test + public void testUIDList2() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Folder folder, IMAPHandlerExpunge handler) + throws MessagingException { + Message[] msgs = ((UIDFolder)folder).getMessagesByUID( + new long[] { 2, 3, 4 }); + assertTrue(msgs[1] == null || msgs[1].isExpunged()); + msgs[0].getFlags(); + assertEquals(2, handler.getSeqNum()); + msgs[2].getFlags(); + assertEquals(3, handler.getSeqNum()); + } + }, + new IMAPHandlerExpunge() { + @Override + public void uidfetch(String line) throws IOException { + untagged("2 FETCH (UID 2)"); + untagged("3 FETCH (UID 3)"); + untagged("3 EXPUNGE"); + untagged("3 EXISTS"); + untagged("3 FETCH (UID 4)"); + numberOfMessages--; + ok(); + } + }); + } + + @Test + public void testUIDList3() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Folder folder, IMAPHandlerExpunge handler) + throws MessagingException { + Message[] msgs = ((UIDFolder)folder).getMessagesByUID( + new long[] { 2, 3, 4 }); + assertTrue(msgs[1] == null || msgs[1].isExpunged()); + msgs[0].getFlags(); + assertEquals(2, handler.getSeqNum()); + msgs[2].getFlags(); + assertEquals(3, handler.getSeqNum()); + } + }, + new IMAPHandlerExpunge() { + @Override + public void uidfetch(String line) throws IOException { + untagged("2 FETCH (UID 2)"); + untagged("3 EXPUNGE"); + untagged("3 EXISTS"); + untagged("3 FETCH (UID 4)"); + numberOfMessages--; + ok(); + } + }); + } + + @Test + public void testUIDList4() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Folder folder, IMAPHandlerExpunge handler) + throws MessagingException { + Message[] msgs = ((UIDFolder)folder).getMessagesByUID( + new long[] { 1, 2, 3 }); + assertEquals(3, msgs.length); + msgs[0].getFlags(); + assertEquals(1, handler.getSeqNum()); + msgs[1].getFlags(); + assertEquals(2, handler.getSeqNum()); + msgs[2].getFlags(); + assertEquals(3, handler.getSeqNum()); + } + }, + new IMAPHandlerExpunge() { + @Override + public void uidfetch(String line) throws IOException { + untagged("1 FETCH (UID 1)"); + untagged("2 FETCH (UID 2)"); + untagged("3 FETCH (UID 3)"); + untagged("4 EXPUNGE"); + untagged("3 EXISTS"); + numberOfMessages--; + ok(); + } + }); + } + + @Test + public void testUIDList5() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Folder folder, IMAPHandlerExpunge handler) + throws MessagingException { + Message[] msgs = ((UIDFolder)folder).getMessagesByUID( + new long[] { 2, 3, 4 }); + assertEquals(3, msgs.length); + msgs[0].getFlags(); + assertEquals(1, handler.getSeqNum()); + msgs[1].getFlags(); + assertEquals(2, handler.getSeqNum()); + msgs[2].getFlags(); + assertEquals(3, handler.getSeqNum()); + } + }, + new IMAPHandlerExpunge() { + @Override + public void uidfetch(String line) throws IOException { + untagged("2 FETCH (UID 2)"); + untagged("3 FETCH (UID 3)"); + untagged("4 FETCH (UID 4)"); + untagged("1 EXPUNGE"); + untagged("3 EXISTS"); + numberOfMessages--; + ok(); + } + }); + } + + public void testWithHandler(IMAPTest test, IMAPHandlerExpunge handler) { + TestServer server = null; + try { + server = new TestServer(handler); + server.start(); + + final Properties properties = new Properties(); + properties.setProperty("mail.imap.host", "localhost"); + properties.setProperty("mail.imap.port", "" + server.getPort()); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + final Store store = session.getStore("imap"); + Folder folder = null; + try { + store.connect("test", "test"); + folder = store.getFolder("INBOX"); + folder.open(Folder.READ_WRITE); + test.test(folder, handler); + } catch (Exception ex) { + System.out.println(ex); + //ex.printStackTrace(); + fail(ex.toString()); + } finally { + if (folder != null) + folder.close(false); + store.close(); + } + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + } + } + } + + /** + * Custom handler. + */ + private static class IMAPHandlerExpunge extends IMAPHandler { + // must be static because handler is cloned for each connection + private static int seqnum; + + @Override + public void select(String line) throws IOException { + numberOfMessages = 4; + super.select(line); + } + + @Override + public void fetch(String line) throws IOException { + StringTokenizer st = new StringTokenizer(line, " "); + String tag = st.nextToken(); + String command = st.nextToken(); + seqnum = Integer.parseInt(st.nextToken()); + ok(); + } + + public int getSeqNum() { + return seqnum; + } + } +}
diff --git a/mail/src/test/java/com/sun/mail/imap/MessageCacheTest.java b/mail/src/test/java/com/sun/mail/imap/MessageCacheTest.java new file mode 100644 index 0000000..89344f2 --- /dev/null +++ b/mail/src/test/java/com/sun/mail/imap/MessageCacheTest.java
@@ -0,0 +1,70 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import static org.junit.Assert.assertEquals; +import org.junit.Test; + +/** + * Test the IMAP MessageCache. + */ +public class MessageCacheTest { + /** + * Test that when a message is expunged and a new message is added, + * the new message has the expected sequence number. + */ + @Test + public void testExpungeAdd() throws Exception { + // test a range of values to find boundary condition errors + for (int n = 1; n <= 100; n++) { + //System.out.println("MessageCache.testExpungeAdd: test " + n); + // start with one message + MessageCache mc = new MessageCache(1, false); + // add the remaining messages (eat into SLOP) + mc.addMessages(n - 1, 2); + // now expunge a message to cause the seqnums array to be created + mc.expungeMessage(1); + // and add one more message + mc.addMessages(1, n); + //System.out.println(" new seqnum " + mc.seqnumOf(n + 1)); + // does the new message have the expected sequence number? + assertEquals(mc.seqnumOf(n + 1), n); + } + } + + /** + * Test that when a message is expunged and new messages are added, + * the new messages have the expected sequence number. Similar to + * the above, but the seqnums array is created first, then expanded. + */ + @Test + public void testExpungeAddExpand() throws Exception { + // test a range of values to find boundary condition errors + for (int n = 2; n <= 100; n++) { + //System.out.println("MessageCache.testExpungeAdd: test " + n); + // start with two messages + MessageCache mc = new MessageCache(2, false); + // now expunge a message to cause the seqnums array to be created + mc.expungeMessage(1); + // add the remaining messages (eat into SLOP) + mc.addMessages(n - 1, 2); + //System.out.println(" new seqnum " + mc.seqnumOf(n + 1)); + // does the new message have the expected sequence number? + assertEquals(mc.seqnumOf(n + 1), n); + } + } +}
diff --git a/mail/src/test/java/com/sun/mail/imap/protocol/BODYSTRUCTURETest.java b/mail/src/test/java/com/sun/mail/imap/protocol/BODYSTRUCTURETest.java new file mode 100644 index 0000000..2bd0a0d --- /dev/null +++ b/mail/src/test/java/com/sun/mail/imap/protocol/BODYSTRUCTURETest.java
@@ -0,0 +1,70 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import javax.mail.internet.ParameterList; + +import com.sun.mail.iap.Response; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertEquals; +import org.junit.Test; + +/** + * Test the BODYSTRUCTURE class. + */ +public class BODYSTRUCTURETest { + /** + * Test workaround for Exchange bug that returns NIL instead of "" + * for a parameter with an empty value (name=""). + */ + @Test + public void testExchangeEmptyParameterValueBug() throws Exception { + IMAPResponse response = new IMAPResponse( + "* 3 FETCH (BODYSTRUCTURE ((\"text\" \"plain\" (\"charset\" \"UTF-8\") " + + "NIL NIL \"quoted-printable\" 512 13 NIL (\"inline\" NIL) NIL NIL)" + + "(\"text\" \"html\" (\"charset\" \"UTF-8\") NIL NIL \"quoted-printable\" " + + "784 11 NIL (\"inline\" NIL) NIL NIL) \"alternative\" " + + "(\"boundary\" \"__139957996218379.example.com\" \"name\" NIL) NIL NIL))"); + // here's the incorrect NIL that should be "" ............^ + FetchResponse fr = new FetchResponse(response); + BODYSTRUCTURE bs = fr.getItem(BODYSTRUCTURE.class); + ParameterList p = bs.cParams; + assertNotNull(p.get("name")); + } + + /** + * Test workaround for Exchange bug that returns the Content-Description + * header value instead of the Content-Disposition for some kinds of + * (formerly S/MIME encrypted?) messages. + */ + @Test + public void testExchangeBadDisposition() throws Exception { + IMAPResponse response = new IMAPResponse( + "* 1 FETCH (BODYSTRUCTURE (" + + "(\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" " + + "21 0 NIL (\"inline\" NIL) NIL NIL)" + + "(\"application\" \"octet-stream\" (\"name\" \"private.txt\") " + + "NIL NIL \"base64\" 690 NIL " + + "(\"attachment\" (\"filename\" \"private.txt\")) NIL NIL) " + + "\"mixed\" (\"boundary\" \"----=_Part_0_-1731707885.1504253815584\") " + + "\"S/MIME Encrypted Message\" NIL))"); + // ^^^^^^^ here's the string that should be the disposition + FetchResponse fr = new FetchResponse(response); + BODYSTRUCTURE bs = fr.getItem(BODYSTRUCTURE.class); + assertEquals("S/MIME Encrypted Message", bs.description); + } +}
diff --git a/mail/src/test/java/com/sun/mail/imap/protocol/EnvelopeTest.java b/mail/src/test/java/com/sun/mail/imap/protocol/EnvelopeTest.java new file mode 100644 index 0000000..ef93ba8 --- /dev/null +++ b/mail/src/test/java/com/sun/mail/imap/protocol/EnvelopeTest.java
@@ -0,0 +1,66 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import com.sun.mail.iap.Response; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; +import org.junit.Test; + +/** + * Test the ENVELOPE class. + */ +public class EnvelopeTest { + /** + * Test workaround for Yahoo IMAP bug that returns a bogus space + * character when one of the recipients is "undisclosed-recipients". + */ + @Test + public void testYahooUndisclosedRecipientsBug() throws Exception { + IMAPResponse response = new IMAPResponse( + "* 2 FETCH (INTERNALDATE \"24-Apr-2012 20:28:58 +0000\" " + + "RFC822.SIZE 155937 " + + "ENVELOPE (\"Wed, 28 Sep 2011 11:16:17 +0100\" \"test\" " + + "((NIL NIL \"xxx\" \"tju.edu.cn\")) " + + "((NIL NIL \"xxx\" \"gmail.com\")) " + + "((NIL NIL \"xxx\" \"tju.edu.cn\")) " + + "((\"undisclosed-recipients\" NIL " + + "\"\\\"undisclosed-recipients\\\"\" NIL )) " + + // here's the space inserted by Yahoo IMAP ^ + "NIL NIL NIL " + + "\"<xxx@mail.gmail.com>\"))"); + FetchResponse fr = new FetchResponse(response); + // no exception means it worked + } + + /** + * Test workaround for Yahoo IMAP bug that returns an empty list + * instad of NIL for some addresses in ENVELOPE response. + */ + @Test + public void testYahooEnvelopeAddressListBug() throws Exception { + IMAPResponse response = new IMAPResponse( + "* 2 FETCH (RFC822.SIZE 2567 INTERNALDATE \"29-Apr-2011 13:49:01 +0000\" " + + "ENVELOPE (\"Fri, 29 Apr 2011 19:19:01 +0530\" \"test\" " + + "((\"xxx\" NIL \"xxx\" \"milium.com.br\")) " + + "((\"xxx\" NIL \"xxx\" \"milium.com.br\")) " + + "((NIL NIL \"xxx\" \"live.hk\")) () NIL NIL NIL " + + "\"<20110429134718.70333732030A@mail2.milium.com.br>\"))"); + FetchResponse fr = new FetchResponse(response); + // no exception means it worked + } +}
diff --git a/mail/src/test/java/com/sun/mail/imap/protocol/IMAPProtocolTest.java b/mail/src/test/java/com/sun/mail/imap/protocol/IMAPProtocolTest.java new file mode 100644 index 0000000..e561388 --- /dev/null +++ b/mail/src/test/java/com/sun/mail/imap/protocol/IMAPProtocolTest.java
@@ -0,0 +1,101 @@ +/* + * Copyright (c) 2014, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import java.io.*; +import java.nio.charset.Charset; +import java.util.Properties; +import com.sun.mail.test.AsciiStringInputStream; +import static org.junit.Assert.assertEquals; +import org.junit.Test; + +/** + * Test the IMAPProtocol class. + */ +public class IMAPProtocolTest { + private static final boolean debug = false; + private static final String content = "aXQncyBteSB0ZXN0IG1haWwNCg0K\r\n"; + private static final String response = + "* 1 FETCH (UID 127 BODY[1.1.MIME] {82}\r\n" + + "Content-Type: text/plain;\r\n" + + "\tcharset=\"utf-8\"\r\n" + + "Content-Transfer-Encoding: base64\r\n" + + "\r\n" + + " ENVELOPE (\"Mon, 17 Mar 2014 14:03:08 +0100\" \"test invoice\"" + + " ((\"Joe User\" NIL \"joe.user\" \"example.com\"))" + + " ((\"Joe User\" NIL \"joe.user\" \"example.com\"))" + + " ((\"Joe User\" NIL \"joe.user\" \"example.com\"))" + + " ((\"Joe User\" NIL \"joe.user\" \"example.com\"))" + + " NIL NIL NIL \"<1234@example.com>\") BODY[1.1]<0> " + + "{" + content.length() + "}\r\n" + content + + ")\r\n" + + "A0 OK FETCH completed.\r\n"; + + /** + * Test that a response containing multiple BODY elements + * returns the correct one. Derived from a customer bug + * with Exchange 2003. Normally this would never happen, + * but it's a valid IMAP response and JavaMail needs to + * handle it properly. + */ + @Test + public void testMultipleBodyResponses() throws Exception { + Properties props = new Properties(); + props.setProperty("mail.imap.reusetagprefix", "true"); + IMAPProtocol p = new IMAPProtocol( + new AsciiStringInputStream(response), + new PrintStream(new ByteArrayOutputStream()), + props, + debug); + BODY b = p.fetchBody(1, "1.1"); + assertEquals("section number", "1.1", b.getSection()); + //System.out.println(b); + //System.out.write(b.getByteArray().getNewBytes()); + String result = new String(b.getByteArray().getNewBytes(), "us-ascii"); + assertEquals("getByteArray.getNewBytes", content, result); + InputStream is = b.getByteArrayInputStream(); + byte[] ba = new byte[is.available()]; + is.read(ba); + result = new String(ba, "us-ascii"); + assertEquals("getByteArrayInputStream", content, result); + } + + /** + * Same test as above, but using a different fetchBody method. + */ + @Test + public void testMultipleBodyResponses2() throws Exception { + Properties props = new Properties(); + props.setProperty("mail.imap.reusetagprefix", "true"); + IMAPProtocol p = new IMAPProtocol( + new AsciiStringInputStream(response), + new PrintStream(new ByteArrayOutputStream()), + props, + debug); + BODY b = p.fetchBody(1, "1.1", 0, content.length(), null); + assertEquals("section number", "1.1", b.getSection()); + //System.out.println(b); + //System.out.write(b.getByteArray().getNewBytes()); + String result = new String(b.getByteArray().getNewBytes(), "us-ascii"); + assertEquals("getByteArray.getNewBytes", content, result); + InputStream is = b.getByteArrayInputStream(); + byte[] ba = new byte[is.available()]; + is.read(ba); + result = new String(ba, "us-ascii"); + assertEquals("getByteArrayInputStream", content, result); + } +}
diff --git a/mail/src/test/java/com/sun/mail/imap/protocol/MODSEQTest.java b/mail/src/test/java/com/sun/mail/imap/protocol/MODSEQTest.java new file mode 100644 index 0000000..e3769fe --- /dev/null +++ b/mail/src/test/java/com/sun/mail/imap/protocol/MODSEQTest.java
@@ -0,0 +1,53 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import com.sun.mail.iap.ParsingException; + +import static org.junit.Assert.assertEquals; +import org.junit.Test; + +/** + * Test the MODSEQ class. + */ +public class MODSEQTest { + /** + * Test an example MODSEQ response. + */ + @Test + public void testAll() throws Exception { + IMAPResponse response = new IMAPResponse( + "* 1 FETCH (MODSEQ (624140003))"); + FetchResponse fr = new FetchResponse(response); + MODSEQ m = fr.getItem(MODSEQ.class); + assertEquals(1, m.seqnum); + assertEquals(624140003, m.modseq); + } + + /** + * Test an example MODSEQ response with unnecessary spaces. + */ + @Test + public void testSpaces() throws Exception { + IMAPResponse response = new IMAPResponse( + "* 1 FETCH ( MODSEQ ( 624140003 ) )"); + FetchResponse fr = new FetchResponse(response); + MODSEQ m = fr.getItem(MODSEQ.class); + assertEquals(1, m.seqnum); + assertEquals(624140003, m.modseq); + } +}
diff --git a/mail/src/test/java/com/sun/mail/imap/protocol/NamespacesTest.java b/mail/src/test/java/com/sun/mail/imap/protocol/NamespacesTest.java new file mode 100644 index 0000000..04e834e --- /dev/null +++ b/mail/src/test/java/com/sun/mail/imap/protocol/NamespacesTest.java
@@ -0,0 +1,152 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import com.sun.mail.iap.ParsingException; + +import static org.junit.Assert.assertEquals; +import org.junit.Test; + +/** + * Test the Namespaces class. + */ +public class NamespacesTest { + private static final String utf8Folder = "#public\u03b1/"; + private static final String utf7Folder = "#public&A7E-/"; + + /** + * Test an example NAMESPACE response. + */ + @Test + public void testAll() throws Exception { + IMAPResponse response = new IMAPResponse( + "* NAMESPACE ((\"\" \"/\")) " + // personal + "((\"~\" \"/\")) " + // other users + "((\"#shared/\" \"/\")" + // shared + "(\"#public/\" \"/\")" + + "(\"#ftp/\" \"/\")" + + "(\"#news.\" \".\"))"); + Namespaces ns = new Namespaces(response); + assertEquals(1, ns.personal.length); + assertEquals("", ns.personal[0].prefix); + assertEquals('/', ns.personal[0].delimiter); + assertEquals(1, ns.otherUsers.length); + assertEquals("~", ns.otherUsers[0].prefix); + assertEquals('/', ns.otherUsers[0].delimiter); + assertEquals(4, ns.shared.length); + assertEquals("#shared/", ns.shared[0].prefix); + assertEquals('/', ns.shared[0].delimiter); + assertEquals("#public/", ns.shared[1].prefix); + assertEquals('/', ns.shared[1].delimiter); + assertEquals("#ftp/", ns.shared[2].prefix); + assertEquals('/', ns.shared[2].delimiter); + assertEquals("#news.", ns.shared[3].prefix); + assertEquals('.', ns.shared[3].delimiter); + } + + /** + * Test an example NAMESPACE response with unnecessary spaces. + */ + @Test + public void testSpaces() throws Exception { + IMAPResponse response = new IMAPResponse( + "* NAMESPACE ((\"\" \"/\")) " + // personal + "( ( \"~\" \"/\" ) ) " + // other users + "(( \"#shared/\" \"/\" )" + // shared + "( \"#public/\" \"/\" )" + + "( \"#ftp/\" \"/\" )" + + " (\"#news.\" \".\" ))"); + Namespaces ns = new Namespaces(response); + assertEquals(1, ns.personal.length); + assertEquals("", ns.personal[0].prefix); + assertEquals('/', ns.personal[0].delimiter); + assertEquals(1, ns.otherUsers.length); + assertEquals("~", ns.otherUsers[0].prefix); + assertEquals('/', ns.otherUsers[0].delimiter); + assertEquals(4, ns.shared.length); + assertEquals("#shared/", ns.shared[0].prefix); + assertEquals('/', ns.shared[0].delimiter); + assertEquals("#public/", ns.shared[1].prefix); + assertEquals('/', ns.shared[1].delimiter); + assertEquals("#ftp/", ns.shared[2].prefix); + assertEquals('/', ns.shared[2].delimiter); + assertEquals("#news.", ns.shared[3].prefix); + assertEquals('.', ns.shared[3].delimiter); + } + + /** + * Test a NAMESPACE response with a UTF-7 folder name. + */ + @Test + public void testUtf7() throws Exception { + IMAPResponse response = new IMAPResponse( + "* NAMESPACE ((\"\" \"/\")) " + // personal + "((\"~\" \"/\")) " + // other users + "((\"#shared/\" \"/\")" + // shared + "(\"" + utf7Folder + "\" \"/\")" + + "(\"#ftp/\" \"/\")" + + "(\"#news.\" \".\"))", + false); + Namespaces ns = new Namespaces(response); + assertEquals(1, ns.personal.length); + assertEquals("", ns.personal[0].prefix); + assertEquals('/', ns.personal[0].delimiter); + assertEquals(1, ns.otherUsers.length); + assertEquals("~", ns.otherUsers[0].prefix); + assertEquals('/', ns.otherUsers[0].delimiter); + assertEquals(4, ns.shared.length); + assertEquals("#shared/", ns.shared[0].prefix); + assertEquals('/', ns.shared[0].delimiter); + assertEquals(utf8Folder, ns.shared[1].prefix); + assertEquals('/', ns.shared[1].delimiter); + assertEquals("#ftp/", ns.shared[2].prefix); + assertEquals('/', ns.shared[2].delimiter); + assertEquals("#news.", ns.shared[3].prefix); + assertEquals('.', ns.shared[3].delimiter); + } + + /** + * Test a NAMESPACE response with a UTF-8 folder name. + */ + @Test + public void testUtf8() throws Exception { + IMAPResponse response = new IMAPResponse( + "* NAMESPACE ((\"\" \"/\")) " + // personal + "((\"~\" \"/\")) " + // other users + "((\"#shared/\" \"/\")" + // shared + "(\"" + utf8Folder + "\" \"/\")" + + "(\"#ftp/\" \"/\")" + + "(\"#news.\" \".\"))", + true); + Namespaces ns = new Namespaces(response); + assertEquals(1, ns.personal.length); + assertEquals("", ns.personal[0].prefix); + assertEquals('/', ns.personal[0].delimiter); + assertEquals(1, ns.otherUsers.length); + assertEquals("~", ns.otherUsers[0].prefix); + assertEquals('/', ns.otherUsers[0].delimiter); + assertEquals(4, ns.shared.length); + assertEquals("#shared/", ns.shared[0].prefix); + assertEquals('/', ns.shared[0].delimiter); + assertEquals(utf8Folder, ns.shared[1].prefix); + assertEquals('/', ns.shared[1].delimiter); + assertEquals("#ftp/", ns.shared[2].prefix); + assertEquals('/', ns.shared[2].delimiter); + assertEquals("#news.", ns.shared[3].prefix); + assertEquals('.', ns.shared[3].delimiter); + } +}
diff --git a/mail/src/test/java/com/sun/mail/imap/protocol/StatusTest.java b/mail/src/test/java/com/sun/mail/imap/protocol/StatusTest.java new file mode 100644 index 0000000..1cc4478 --- /dev/null +++ b/mail/src/test/java/com/sun/mail/imap/protocol/StatusTest.java
@@ -0,0 +1,92 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import com.sun.mail.iap.ParsingException; + +import static org.junit.Assert.assertEquals; +import org.junit.Test; + +/** + * Test the Status class. + */ +public class StatusTest { + /** + * Test that the returned mailbox name is decoded. + */ + @Test + public void testMailboxDecode() throws Exception { + String mbox = "Entw\u00fcrfe"; + IMAPResponse response = new IMAPResponse( + "* STATUS " + + BASE64MailboxEncoder.encode(mbox) + + " (MESSAGES 231 UIDNEXT 44292)", false); + Status s = new Status(response); + assertEquals(mbox, s.mbox); + assertEquals(231, s.total); + assertEquals(44292, s.uidnext); + } + + /** + * Test that the returned mailbox name is correct when using UTF-8. + */ + @Test + public void testMailboxUtf8() throws Exception { + String mbox = "Entw\u00fcrfe"; + IMAPResponse response = new IMAPResponse( + "* STATUS " + + mbox + + " (MESSAGES 231 UIDNEXT 44292)", true); + Status s = new Status(response); + assertEquals(mbox, s.mbox); + assertEquals(231, s.total); + assertEquals(44292, s.uidnext); + } + + /** + * Test that spaces in the response don't confuse it. + */ + @Test + public void testSpaces() throws Exception { + IMAPResponse response = new IMAPResponse( + "* STATUS test ( MESSAGES 231 UIDNEXT 44292 )"); + Status s = new Status(response); + assertEquals("test", s.mbox); + assertEquals(231, s.total); + assertEquals(44292, s.uidnext); + } + + /** + * Test that a bad response throws a ParsingException + */ + @Test(expected = ParsingException.class) + public void testBadResponseNoAttrList() throws Exception { + String mbox = "test"; + IMAPResponse response = new IMAPResponse("* STATUS test "); + Status s = new Status(response); + } + + /** + * Test that a bad response throws a ParsingException + */ + @Test(expected = ParsingException.class) + public void testBadResponseNoAttrs() throws Exception { + String mbox = "test"; + IMAPResponse response = new IMAPResponse("* STATUS test ("); + Status s = new Status(response); + } +}
diff --git a/mail/src/test/java/com/sun/mail/imap/protocol/StratoImapBugfixTest.java b/mail/src/test/java/com/sun/mail/imap/protocol/StratoImapBugfixTest.java new file mode 100644 index 0000000..2f9dd97 --- /dev/null +++ b/mail/src/test/java/com/sun/mail/imap/protocol/StratoImapBugfixTest.java
@@ -0,0 +1,70 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import com.sun.mail.iap.ParsingException; +import com.sun.mail.iap.Response; +import com.sun.mail.imap.protocol.Status; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; +import org.junit.Test; + +/** + * @author tkrammer + */ +public class StratoImapBugfixTest { + @Test + public void testValidStatusResponseLeadingSpaces() throws Exception { + final Response response = + new Response("STATUS \" Sent Items \" (UIDNEXT 1)"); + final Status status = new Status(response); + + assertEquals(" Sent Items ", status.mbox); + assertEquals(1, status.uidnext); + } + + @Test + public void testValidStatusResponse() throws Exception { + final Response response = + new Response("STATUS \"Sent Items\" (UIDNEXT 1)"); + final Status status = new Status(response); + + assertEquals("Sent Items", status.mbox); + assertEquals(1, status.uidnext); + } + + @Test + public void testInvalidStatusResponse() throws Exception { + Response response = new Response("STATUS Sent Items (UIDNEXT 1)"); + final Status status = new Status(response); + + assertEquals("Sent Items", status.mbox); + assertEquals(1, status.uidnext); + } + + @Test + public void testMissingBracket() throws Exception { + final Response response = + new Response("STATUS \"Sent Items\" UIDNEXT 1)"); + + try { + new Status(response); + fail("Must throw exception"); + } catch(ParsingException e) { + } + } +}
diff --git a/mail/src/test/java/com/sun/mail/imap/protocol/UIDSetTest.java b/mail/src/test/java/com/sun/mail/imap/protocol/UIDSetTest.java new file mode 100644 index 0000000..26b1c84 --- /dev/null +++ b/mail/src/test/java/com/sun/mail/imap/protocol/UIDSetTest.java
@@ -0,0 +1,229 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import java.io.*; +import java.util.*; + +import org.junit.Test; +import org.junit.Assert; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +/** + * Test UIDSet. + * + * @author Bill Shannon + */ + +@RunWith(Parameterized.class) +public class UIDSetTest { + private TestData data; + + private static boolean gen_test_input = false; // output good + private static int errors = 0; // number of errors detected + + private static boolean junit; + + static class TestData { + public String name; + public String uids; + public long max; + public String maxuids; + public long[] expect; + } + + public UIDSetTest(TestData t) { + data = t; + } + + @Test + public void testData() { + test(data); + } + + @Parameters + public static Collection<TestData[]> data() throws Exception { + junit = true; + // XXX - gratuitous array requirement + List<TestData[]> testData = new ArrayList<>(); + BufferedReader in = new BufferedReader(new InputStreamReader( + UIDSetTest.class.getResourceAsStream("uiddata"))); + TestData t; + while ((t = parse(in)) != null) + testData.add(new TestData[] { t }); + return testData; + } + + public static void main(String argv[]) throws Exception { + int optind; + // XXX - all options currently ignored + for (optind = 0; optind < argv.length; optind++) { + if (argv[optind].equals("-")) { + // ignore + } else if (argv[optind].equals("-g")) { + gen_test_input = true; + } else if (argv[optind].equals("--")) { + optind++; + break; + } else if (argv[optind].startsWith("-")) { + System.out.println( + "Usage: uidtest [-g] [-]"); + System.exit(1); + } else { + break; + } + } + + // read from stdin + BufferedReader in = + new BufferedReader(new InputStreamReader(System.in)); + + TestData t; + while ((t = parse(in)) != null) + test(t); + System.exit(errors); + } + + /* + * Parse the input, returning a test case. + */ + public static TestData parse(BufferedReader in) throws Exception { + + String line = null; + for (;;) { + line = in.readLine(); + if (line == null) + return null; + if (line.length() == 0 || line.startsWith("#")) + continue; + + if (!line.startsWith("TEST")) + throw new Exception("Bad test data format"); + break; + } + + TestData t = new TestData(); + int i = line.indexOf(' '); // XXX - crude + t.name = line.substring(i + 1); + + line = in.readLine(); + StringTokenizer st = new StringTokenizer(line); + String tok = st.nextToken(); + if (!tok.equals("DATA")) + throw new Exception("Bad test data format: " + line); + tok = st.nextToken(); + if (tok.equals("NULL")) + t.uids = null; + else if (tok.equals("EMPTY")) + t.uids = ""; + else + t.uids = tok; + + line = in.readLine(); + st = new StringTokenizer(line); + tok = st.nextToken(); + if (tok.equals("MAX")) { + tok = st.nextToken(); + try { + t.max = Long.valueOf(tok); + } catch (NumberFormatException ex) { + throw new Exception("Bad MAX value in line: " + line); + } + if (st.hasMoreTokens()) + t.maxuids = st.nextToken(); + else + t.maxuids = t.uids; + line = in.readLine(); + st = new StringTokenizer(line); + tok = st.nextToken(); + } + List<Long> uids = new ArrayList<>(); + if (!tok.equals("EXPECT")) + throw new Exception("Bad test data format: " + line); + while (st.hasMoreTokens()) { + tok = st.nextToken(); + if (tok.equals("NULL")) + t.expect = null; + else if (tok.equals("EMPTY")) + t.expect = new long[0]; + else { + try { + uids.add(Long.valueOf(tok)); + } catch (NumberFormatException ex) { + throw new Exception("Bad DATA option in line: " + line); + } + } + } + if (uids.size() > 0) { + t.expect = new long[uids.size()]; + i = 0; + for (Long l : uids) + t.expect[i++] = l.longValue(); + } + + return t; + } + + /** + * Test the data in the test case. + */ + public static void test(TestData t) { + // XXX - handle nulls + + // first, test string to array + UIDSet[] uidset = UIDSet.parseUIDSets(t.uids); + long[] uids; + if (t.max > 0) + uids = UIDSet.toArray(uidset, t.max); + else + uids = UIDSet.toArray(uidset); + if (junit) + Assert.assertArrayEquals(t.expect, uids); + else if (!arrayEquals(t.expect, uids)) { + System.out.println("Test: " + t.name); + System.out.println("FAIL"); + errors++; + } + + // now, test the reverse + UIDSet[] uidset2 = UIDSet.createUIDSets(uids); + String suid = UIDSet.toString(uidset2); + String euid = t.max > 0 ? t.maxuids : t.uids; + if (junit) + Assert.assertEquals(euid, suid); + else if (!euid.equals(suid)) { + System.out.println("Test: " + t.name); + System.out.println("FAIL2"); + errors++; + } + } + + private static boolean arrayEquals(long[] a,long[] b) { + if (a == b) + return true; + if (a == null || b == null) + return false; + if (a.length != b.length) + return false; + for (int i = 0; i < a.length; i++) + if (a[i] != b[i]) + return false; + return true; + } +}
diff --git a/mail/src/test/java/com/sun/mail/pop3/POP3AuthDebugTest.java b/mail/src/test/java/com/sun/mail/pop3/POP3AuthDebugTest.java new file mode 100644 index 0000000..3ff667d --- /dev/null +++ b/mail/src/test/java/com/sun/mail/pop3/POP3AuthDebugTest.java
@@ -0,0 +1,133 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.pop3; + +import java.io.*; +import java.util.Properties; + +import javax.mail.Session; +import javax.mail.Store; + +import com.sun.mail.test.TestServer; + +import org.junit.Test; +import org.junit.Rule; +import org.junit.rules.Timeout; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.fail; + +/** + * Test that authentication information is only included in + * the debug output when explicitly requested by setting the + * property "mail.debug.auth" to "true". + * + * XXX - should test all authentication types, but that requires + * more work in the dummy test server. + */ +public final class POP3AuthDebugTest { + + // timeout the test in case of deadlock + @Rule + public Timeout deadlockTimeout = Timeout.seconds(20); + + /** + * Test that authentication information isn't included in the debug output. + */ + @Test + public void testNoAuthDefault() { + final Properties properties = new Properties(); + assertFalse("PASS in debug output", test(properties, "PASS")); + } + + @Test + public void testNoAuth() { + final Properties properties = new Properties(); + properties.setProperty("mail.debug.auth", "false"); + assertFalse("PASS in debug output", test(properties, "PASS")); + } + + /** + * Test that authentication information *is* included in the debug output. + */ + @Test + public void testAuth() { + final Properties properties = new Properties(); + properties.setProperty("mail.debug.auth", "true"); + assertTrue("PASS in debug output", test(properties, "PASS")); + } + + /** + * Create a test server, connect to it, and collect the debug output. + * Scan the debug output looking for "expect", return true if found. + */ + public boolean test(Properties properties, String expect) { + TestServer server = null; + try { + final POP3Handler handler = new POP3Handler(); + server = new TestServer(handler); + server.start(); + Thread.sleep(1000); + + properties.setProperty("mail.pop3.host", "localhost"); + properties.setProperty("mail.pop3.port", "" + server.getPort()); + final Session session = Session.getInstance(properties); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + PrintStream ps = new PrintStream(bos); + session.setDebugOut(ps); + session.setDebug(true); + + final Store store = session.getStore("pop3"); + try { + store.connect("test", "test"); + } catch (Exception ex) { + System.out.println(ex); + //ex.printStackTrace(); + fail(ex.toString()); + } finally { + store.close(); + } + + ps.close(); + bos.close(); + ByteArrayInputStream bis = + new ByteArrayInputStream(bos.toByteArray()); + BufferedReader r = new BufferedReader( + new InputStreamReader(bis, "us-ascii")); + String line; + boolean found = false; + while ((line = r.readLine()) != null) { + if (line.startsWith("DEBUG")) + continue; + if (line.startsWith("*")) + continue; + if (line.indexOf(expect) >= 0) + found = true; + } + r.close(); + return found; + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + return false; // XXX - doesn't matter + } finally { + if (server != null) { + server.quit(); + } + } + } +}
diff --git a/mail/src/test/java/com/sun/mail/pop3/POP3FolderClosedExceptionTest.java b/mail/src/test/java/com/sun/mail/pop3/POP3FolderClosedExceptionTest.java new file mode 100644 index 0000000..3269e9b --- /dev/null +++ b/mail/src/test/java/com/sun/mail/pop3/POP3FolderClosedExceptionTest.java
@@ -0,0 +1,173 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.pop3; + +import java.io.IOException; +import java.util.Properties; + +import javax.mail.Folder; +import javax.mail.Session; +import javax.mail.Store; +import javax.mail.Message; +import javax.mail.FolderClosedException; + +import com.sun.mail.test.TestServer; + +import org.junit.Test; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.fail; + +/** + * Test that FolderClosedException is thrown when server times out connection. + * This test is derived from real failures seen with Hotmail. + * + * @author sbo + * @author Bill Shannon + */ +public final class POP3FolderClosedExceptionTest { + + /** + * Test that FolderClosedException is thrown when the timeout occurs + * when reading the message body. + */ + @Test + public void testFolderClosedExceptionBody() { + TestServer server = null; + try { + final POP3Handler handler = new POP3HandlerTimeoutBody(); + server = new TestServer(handler); + server.start(); + Thread.sleep(1000); + + final Properties properties = new Properties(); + properties.setProperty("mail.pop3.host", "localhost"); + properties.setProperty("mail.pop3.port", "" + server.getPort()); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + final Store store = session.getStore("pop3"); + try { + store.connect("test", "test"); + final Folder folder = store.getFolder("INBOX"); + folder.open(Folder.READ_ONLY); + Message msg = folder.getMessage(1); + try { + msg.getContent(); + } catch (IOException ioex) { + // expected + // first attempt detects error return from server + } + // second attempt detects closed connection from server + msg.getContent(); + + // Check + assertFalse(folder.isOpen()); + } catch (FolderClosedException ex) { + // success! + } finally { + store.close(); + } + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + } + } + } + + /** + * Custom handler. Returns ERR for RETR the first time, + * then closes the connection the second time. + */ + private static final class POP3HandlerTimeoutBody extends POP3Handler { + + private boolean first = true; + + /** + * {@inheritDoc} + */ + @Override + public void retr(String arg) throws IOException { + if (first) { + println("-ERR Server timeout"); + first = false; + } else + exit(); + } + } + + /** + * Test that FolderClosedException is thrown when the timeout occurs + * when reading the headers. + */ + @Test + public void testFolderClosedExceptionHeaders() { + TestServer server = null; + try { + final POP3Handler handler = new POP3HandlerTimeoutHeader(); + server = new TestServer(handler); + server.start(); + Thread.sleep(1000); + + final Properties properties = new Properties(); + properties.setProperty("mail.pop3.host", "localhost"); + properties.setProperty("mail.pop3.port", "" + server.getPort()); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + final Store store = session.getStore("pop3"); + try { + store.connect("test", "test"); + final Folder folder = store.getFolder("INBOX"); + folder.open(Folder.READ_ONLY); + Message msg = folder.getMessage(1); + msg.getSubject(); + + // Check + assertFalse(folder.isOpen()); + } catch (FolderClosedException ex) { + // success! + } finally { + store.close(); + } + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + } + } + } + + /** + * Custom handler. Returns ERR for TOP, then closes connection. + */ + private static final class POP3HandlerTimeoutHeader extends POP3Handler { + + /** + * {@inheritDoc} + */ + @Override + public void top(String arg) throws IOException { + println("-ERR Server timeout"); + exit(); + } + } +}
diff --git a/mail/src/test/java/com/sun/mail/pop3/POP3Handler.java b/mail/src/test/java/com/sun/mail/pop3/POP3Handler.java new file mode 100644 index 0000000..0713ff1 --- /dev/null +++ b/mail/src/test/java/com/sun/mail/pop3/POP3Handler.java
@@ -0,0 +1,279 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.pop3; + +import java.io.IOException; +import java.util.StringTokenizer; +import java.util.logging.Logger; +import java.util.logging.Level; + +import com.sun.mail.test.ProtocolHandler; + +/** + * Handle connection. + * + * @author sbo + */ +public class POP3Handler extends ProtocolHandler { + + /** Current line. */ + private String currentLine; + + /** First test message. */ + private String top1 = + "Mime-Version: 1.0\r\n" + + "From: joe@example.com\r\n" + + "To: bob@example.com\r\n" + + "Subject: Example\r\n" + + "Content-Type: text/plain\r\n" + + "\r\n"; + private String msg1 = top1 + + "plain text\r\n"; + + /** Second test message. */ + private String top2 = + "Mime-Version: 1.0\r\n" + + "From: joe@example.com\r\n" + + "To: bob@example.com\r\n" + + "Subject: Multipart Example\r\n" + + "Content-Type: multipart/mixed; boundary=\"xxx\"\r\n" + + "\r\n"; + private String msg2 = top2 + + "preamble\r\n" + + "--xxx\r\n" + + "\r\n" + + "first part\r\n" + + "\r\n" + + "--xxx\r\n" + + "\r\n" + + "second part\r\n" + + "\r\n" + + "--xxx--\r\n"; + + /** + * Send greetings. + * + * @throws IOException + * unable to write to socket + */ + @Override + public void sendGreetings() throws IOException { + this.println("+OK POP3 CUSTOM"); + } + + /** + * Send String to socket. + * + * @param str + * String to send + * @throws IOException + * unable to write to socket + */ + public void println(final String str) throws IOException { + this.writer.print(str); + this.writer.print("\r\n"); + this.writer.flush(); + } + + /** + * Handle command. + * + * @throws IOException + * unable to read/write to socket + */ + @Override + public void handleCommand() throws IOException { + this.currentLine = readLine(); + + if (this.currentLine == null) { + // probably just EOF because the socket was closed + //LOGGER.severe("Current line is null!"); + this.exit(); + return; + } + + final StringTokenizer st = new StringTokenizer(this.currentLine, " "); + final String commandName = st.nextToken().toUpperCase(); + final String arg = st.hasMoreTokens() ? st.nextToken() : null; + if (commandName == null) { + LOGGER.severe("Command name is empty!"); + this.exit(); + return; + } + + if (commandName.equals("STAT")) { + this.stat(); + } else if (commandName.equals("LIST")) { + this.list(); + } else if (commandName.equals("RETR")) { + this.retr(arg); + } else if (commandName.equals("DELE")) { + this.dele(); + } else if (commandName.equals("NOOP")) { + this.noop(); + } else if (commandName.equals("RSET")) { + this.rset(); + } else if (commandName.equals("QUIT")) { + this.quit(); + } else if (commandName.equals("TOP")) { + this.top(arg); + } else if (commandName.equals("UIDL")) { + this.uidl(); + } else if (commandName.equals("USER")) { + this.user(); + } else if (commandName.equals("PASS")) { + this.pass(); + } else if (commandName.equals("CAPA")) { + this.println("-ERR CAPA not supported"); + } else { + LOGGER.log(Level.SEVERE, "ERROR command unknown: {0}", commandName); + this.println("-ERR unknown command"); + } + } + + /** + * STAT command. + * + * @throws IOException + * unable to read/write to socket + */ + public void stat() throws IOException { + this.println("+OK 2 " + (msg1.length() + msg2.length())); + } + + /** + * LIST command. + * + * @throws IOException + * unable to read/write to socket + */ + public void list() throws IOException { + this.writer.println("+OK"); + this.writer.println("1 " + msg1.length()); + this.writer.println("2 " + msg2.length()); + this.println("."); + } + + /** + * RETR command. + * + * @throws IOException + * unable to read/write to socket + */ + public void retr(String arg) throws IOException { + String msg; + if (arg.equals("1")) + msg = msg1; + else + msg = msg2; + this.println("+OK " + msg.length() + " octets"); + this.writer.write(msg); + this.println("."); + } + + /** + * DELE command. + * + * @throws IOException + * unable to read/write to socket + */ + public void dele() throws IOException { + this.println("-ERR DELE not supported"); + } + + /** + * NOOP command. + * + * @throws IOException + * unable to read/write to socket + */ + public void noop() throws IOException { + this.println("+OK"); + } + + /** + * RSET command. + * + * @throws IOException + * unable to read/write to socket + */ + public void rset() throws IOException { + this.println("+OK"); + } + + /** + * QUIT command. + * + * @throws IOException + * unable to read/write to socket + */ + public void quit() throws IOException { + this.println("+OK"); + this.exit(); + } + + /** + * TOP command. + * XXX - ignores number of lines argument + * + * @throws IOException + * unable to read/write to socket + */ + public void top(String arg) throws IOException { + String top; + if (arg.equals("1")) + top = top1; + else + top = top2; + this.println("+OK " + top.length() + " octets"); + this.writer.write(top); + this.println("."); + } + + /** + * UIDL command. + * + * @throws IOException + * unable to read/write to socket + */ + public void uidl() throws IOException { + this.writer.println("+OK"); + this.writer.println("1 1"); + this.writer.println("2 2"); + this.println("."); + } + + /** + * USER command. + * + * @throws IOException + * unable to read/write to socket + */ + public void user() throws IOException { + this.println("+OK"); + } + + /** + * PASS command. + * + * @throws IOException + * unable to read/write to socket + */ + public void pass() throws IOException { + this.println("+OK"); + } +}
diff --git a/mail/src/test/java/com/sun/mail/pop3/POP3MessageTest.java b/mail/src/test/java/com/sun/mail/pop3/POP3MessageTest.java new file mode 100644 index 0000000..2dc38b7 --- /dev/null +++ b/mail/src/test/java/com/sun/mail/pop3/POP3MessageTest.java
@@ -0,0 +1,131 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.pop3; + +import java.io.IOException; +import java.util.Properties; + +import javax.mail.Folder; +import javax.mail.Session; +import javax.mail.Store; +import javax.mail.Message; +import javax.mail.Multipart; +import javax.mail.Part; +import javax.mail.MessagingException; + +import com.sun.mail.test.TestServer; + +import org.junit.Test; +import org.junit.BeforeClass; +import org.junit.AfterClass; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.fail; + +/** + * Test that we can read POP3 messages. + * + * @author Bill Shannon + */ +public final class POP3MessageTest { + + private static TestServer server = null; + private static Store store; + private static Folder folder; + + private static void startServer(boolean cached) { + try { + final POP3Handler handler = new POP3Handler(); + server = new TestServer(handler); + server.start(); + Thread.sleep(1000); + + final Properties properties = new Properties(); + properties.setProperty("mail.pop3.host", "localhost"); + properties.setProperty("mail.pop3.port", "" + server.getPort()); + if (cached) + properties.setProperty("mail.pop3.filecache.enable", "true"); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + store = session.getStore("pop3"); + store.connect("test", "test"); + folder = store.getFolder("INBOX"); + folder.open(Folder.READ_ONLY); + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } + } + + private static void stopServer() { + try { + if (folder != null) + folder.close(false); + if (store != null) + store.close(); + } catch (MessagingException ex) { + // ignore it + } finally { + if (server != null) + server.quit(); + } + } + + /** + * Test that we can read the content of a message twice. + * A bug caused POP3Message to return the same stream the + * second time, instead of a new stream positioned at the + * beginning of the data. This caused multipart parsing + * to fail. + */ + @Test + public void testReadTwice() throws Exception { + readTwice(false); + } + + /** + * Now test it using the file cache. + */ + @Test + public void testReadTwiceCached() throws Exception { + readTwice(true); + } + + private void readTwice(boolean cached) throws Exception { + startServer(cached); + try { + Message[] msgs = folder.getMessages(); + for (int i = 0; i < msgs.length; i++) { + loadMail(msgs[i]); + loadMail(msgs[i]); + } + } finally { + stopServer(); + } + // no exception is success! + } + + private void loadMail(Part p) throws Exception { + Object content = p.getContent(); + if (content instanceof Multipart) { + Multipart mp = (Multipart)content; + int cnt = mp.getCount(); + for (int i = 0; i < cnt; i++) + loadMail(mp.getBodyPart(i)); + } + } +}
diff --git a/mail/src/test/java/com/sun/mail/pop3/POP3ReadableMimeTest.java b/mail/src/test/java/com/sun/mail/pop3/POP3ReadableMimeTest.java new file mode 100644 index 0000000..ad6dd71 --- /dev/null +++ b/mail/src/test/java/com/sun/mail/pop3/POP3ReadableMimeTest.java
@@ -0,0 +1,139 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.pop3; + +import java.io.IOException; +import java.io.InputStream; +import java.io.ByteArrayOutputStream; +import java.util.Properties; + +import javax.mail.Folder; +import javax.mail.Session; +import javax.mail.Store; +import javax.mail.Message; +import javax.mail.Part; +import javax.mail.MessagingException; + +import com.sun.mail.util.ReadableMime; +import com.sun.mail.test.TestServer; + +import org.junit.Test; +import org.junit.BeforeClass; +import org.junit.AfterClass; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * Test ReadableMime support for POP3. + * + * @author Bill Shannon + */ +public final class POP3ReadableMimeTest { + + private static TestServer server = null; + private static Store store; + private static Folder folder; + + private static void startServer(boolean cached) { + try { + final POP3Handler handler = new POP3Handler(); + server = new TestServer(handler); + server.start(); + Thread.sleep(1000); + + final Properties properties = new Properties(); + properties.setProperty("mail.pop3.host", "localhost"); + properties.setProperty("mail.pop3.port", "" + server.getPort()); + if (cached) + properties.setProperty("mail.pop3.filecache.enable", "true"); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + store = session.getStore("pop3"); + store.connect("test", "test"); + folder = store.getFolder("INBOX"); + folder.open(Folder.READ_ONLY); + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } + } + + private static void stopServer() { + try { + if (folder != null) + folder.close(false); + if (store != null) + store.close(); + } catch (MessagingException ex) { + // ignore it + } finally { + if (server != null) + server.quit(); + } + } + + /** + * Test that the data returned by the getMimeStream method + * is exactly the same data as produced by the writeTo method. + */ + @Test + public void testReadableMime() throws Exception { + test(false); + } + + /** + * Now test it using the file cache. + */ + @Test + public void testReadableMimeCached() throws Exception { + test(true); + } + + private void test(boolean cached) throws Exception { + startServer(cached); + try { + Message[] msgs = folder.getMessages(); + for (int i = 0; i < msgs.length; i++) + verifyData(msgs[i]); + } finally { + stopServer(); + } + // no exception is success! + } + + private void verifyData(Part p) throws MessagingException, IOException { + assertTrue("ReadableMime", p instanceof ReadableMime); + InputStream is = null; + try { + ReadableMime rp = (ReadableMime)p; + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + p.writeTo(bos); + bos.close(); + byte[] buf = bos.toByteArray(); + is = rp.getMimeStream(); + int i, b; + for (i = 0; (b = is.read()) != -1; i++) + assertTrue("message data", b == (buf[i] & 0xff)); + assertTrue("data size", i == buf.length); + } finally { + try { + is.close(); + } catch (IOException ex) { } + } + } +}
diff --git a/mail/src/test/java/com/sun/mail/pop3/POP3StoreTest.java b/mail/src/test/java/com/sun/mail/pop3/POP3StoreTest.java new file mode 100644 index 0000000..b4d1082 --- /dev/null +++ b/mail/src/test/java/com/sun/mail/pop3/POP3StoreTest.java
@@ -0,0 +1,145 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.pop3; + +import java.io.IOException; +import java.util.Properties; + +import javax.mail.Folder; +import javax.mail.Session; +import javax.mail.Store; + +import com.sun.mail.test.TestServer; + +import org.junit.Test; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.fail; + +/** + * Test POP3Store. + * + * @author sbo + * @author Bill Shannon + */ +public final class POP3StoreTest { + + /** + * Check is connected. + */ + @Test + public void testIsConnected() { + TestServer server = null; + try { + final POP3Handler handler = new POP3HandlerNoopErr(); + server = new TestServer(handler); + server.start(); + + final Properties properties = new Properties(); + properties.setProperty("mail.pop3.host", "localhost"); + properties.setProperty("mail.pop3.port", "" + server.getPort()); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + final Store store = session.getStore("pop3"); + try { + store.connect("test", "test"); + final Folder folder = store.getFolder("INBOX"); + folder.open(Folder.READ_ONLY); + + // Check + assertFalse(folder.isOpen()); + } finally { + store.close(); + } + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + } + } + } + + /** + * Check that enabling APOP with a server that doesn't support APOP + * (and doesn't return any information in the greeting) doesn't fail. + */ + @Test + public void testApopNotSupported() { + TestServer server = null; + try { + final POP3Handler handler = new POP3HandlerNoGreeting(); + server = new TestServer(handler); + server.start(); + + final Properties properties = new Properties(); + properties.setProperty("mail.pop3.host", "localhost"); + properties.setProperty("mail.pop3.port", "" + server.getPort()); + properties.setProperty("mail.pop3.apop.enable", "true"); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + final Store store = session.getStore("pop3"); + try { + store.connect("test", "test"); + // success! + } finally { + store.close(); + } + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + } + } + } + + /** + * Custom handler. Returns ERR for NOOP. + * + * @author sbo + */ + private static final class POP3HandlerNoopErr extends POP3Handler { + + /** + * {@inheritDoc} + */ + @Override + public void noop() throws IOException { + this.println("-ERR"); + } + } + + /** + * Custom handler. Don't include any extra information in the greeting. + * + * @author Bill Shannon + */ + private static final class POP3HandlerNoGreeting extends POP3Handler { + + /** + * {@inheritDoc} + */ + @Override + public void sendGreetings() throws IOException { + this.println("+OK"); + } + } +}
diff --git a/mail/src/test/java/com/sun/mail/smtp/NopServer.java b/mail/src/test/java/com/sun/mail/smtp/NopServer.java new file mode 100644 index 0000000..033deca --- /dev/null +++ b/mail/src/test/java/com/sun/mail/smtp/NopServer.java
@@ -0,0 +1,100 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.smtp; + +import java.io.IOException; +import java.io.InputStream; +import java.net.ServerSocket; +import java.net.Socket; + +/** + * A server that does nothing, but keeps track of + * whether the client socket was closed. + * + * Inspired by, and derived from, POP3Server by sbo. + * + * @author Bill Shannon + */ +public final class NopServer extends Thread { + + /** Server socket. */ + private ServerSocket serverSocket; + + /** Keep on? */ + private volatile boolean keepOn; + + /** Did we get EOF on the client socket? */ + private volatile boolean gotEOF = false; + + /** + * Nop server. + */ + public NopServer() throws IOException { + serverSocket = new ServerSocket(0); + } + + /** + * Return the port the server is listening on. + */ + public int getPort() { + return serverSocket.getLocalPort(); + } + + /** + * Exit Nop server. + */ + public void quit() { + try { + keepOn = false; + if (serverSocket != null && !serverSocket.isClosed()) { + serverSocket.close(); + serverSocket = null; + } + } catch (final IOException e) { + throw new RuntimeException(e); + } + } + + public boolean eof() { + return gotEOF; + } + + @Override + public void run() { + try { + keepOn = true; + + while (keepOn) { + try { + final Socket clientSocket = serverSocket.accept(); + /* + * Do nothing but consume any input and throw it away. + * When we see EOF, remember it. + */ + InputStream is = clientSocket.getInputStream(); + while (is.read() >= 0) + ; + gotEOF = true; + } catch (final IOException e) { + //e.printStackTrace(); + } + } + } finally { + quit(); + } + } +}
diff --git a/mail/src/test/java/com/sun/mail/smtp/SMTPAuthDebugTest.java b/mail/src/test/java/com/sun/mail/smtp/SMTPAuthDebugTest.java new file mode 100644 index 0000000..60340af --- /dev/null +++ b/mail/src/test/java/com/sun/mail/smtp/SMTPAuthDebugTest.java
@@ -0,0 +1,133 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.smtp; + +import java.io.*; +import java.util.Properties; + +import javax.mail.Session; +import javax.mail.Transport; + +import com.sun.mail.test.TestServer; + +import org.junit.Test; +import org.junit.Rule; +import org.junit.rules.Timeout; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.fail; + +/** + * Test that authentication information is only included in + * the debug output when explicitly requested by setting the + * property "mail.debug.auth" to "true". + * + * XXX - should test all authentication types, but that requires + * more work in the dummy test server. + */ +public final class SMTPAuthDebugTest { + + // timeout the test in case of deadlock + @Rule + public Timeout deadlockTimeout = Timeout.seconds(20); + + /** + * Test that authentication information isn't included in the debug output. + */ + @Test + public void testNoAuthDefault() { + final Properties properties = new Properties(); + assertFalse("AUTH in debug output", test(properties, "AUTH")); + } + + @Test + public void testNoAuth() { + final Properties properties = new Properties(); + properties.setProperty("mail.debug.auth", "false"); + assertFalse("AUTH in debug output", test(properties, "AUTH")); + } + + /** + * Test that authentication information *is* included in the debug output. + */ + @Test + public void testAuth() { + final Properties properties = new Properties(); + properties.setProperty("mail.debug.auth", "true"); + assertTrue("AUTH in debug output", test(properties, "AUTH")); + } + + /** + * Create a test server, connect to it, and collect the debug output. + * Scan the debug output looking for "expect", return true if found. + */ + public boolean test(Properties properties, String expect) { + TestServer server = null; + try { + final SMTPHandler handler = new SMTPHandler(); + server = new TestServer(handler); + server.start(); + Thread.sleep(1000); + + properties.setProperty("mail.smtp.host", "localhost"); + properties.setProperty("mail.smtp.port", "" + server.getPort()); + final Session session = Session.getInstance(properties); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + PrintStream ps = new PrintStream(bos); + session.setDebugOut(ps); + session.setDebug(true); + + final Transport t = session.getTransport("smtp"); + try { + t.connect("test", "test"); + } catch (Exception ex) { + System.out.println(ex); + //ex.printStackTrace(); + fail(ex.toString()); + } finally { + t.close(); + } + + ps.close(); + bos.close(); + ByteArrayInputStream bis = + new ByteArrayInputStream(bos.toByteArray()); + BufferedReader r = new BufferedReader( + new InputStreamReader(bis, "us-ascii")); + String line; + boolean found = false; + while ((line = r.readLine()) != null) { + if (line.startsWith("DEBUG")) + continue; + if (line.startsWith("*")) + continue; + if (line.startsWith(expect)) + found = true; + } + r.close(); + return found; + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + return false; // XXX - doesn't matter + } finally { + if (server != null) { + server.quit(); + } + } + } +}
diff --git a/mail/src/test/java/com/sun/mail/smtp/SMTPBdatTest.java b/mail/src/test/java/com/sun/mail/smtp/SMTPBdatTest.java new file mode 100644 index 0000000..64fb3f0 --- /dev/null +++ b/mail/src/test/java/com/sun/mail/smtp/SMTPBdatTest.java
@@ -0,0 +1,151 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.smtp; + +import java.io.IOException; +import java.io.ByteArrayOutputStream; +import java.util.Properties; + +import javax.mail.Session; +import javax.mail.Transport; +import javax.mail.Message; +import javax.mail.MessagingException; +import javax.mail.internet.MimeMessage; + +import com.sun.mail.test.TestServer; + +import org.junit.Test; +import org.junit.Rule; +import org.junit.rules.Timeout; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.fail; + +/** + * Test the BDAT command. + */ +public final class SMTPBdatTest { + + private byte[] message; + + @Test + public void testBdatSuccess() throws Exception { + TestServer server = null; + try { + SMTPHandler handler = new SMTPHandler() { + {{ extensions.add("CHUNKING"); }} + + @Override + public void setMessage(byte[] msg) { + message = msg; + } + }; + server = new TestServer(handler); + server.start(); + + final Properties properties = new Properties(); + properties.setProperty("mail.smtp.host", "localhost"); + properties.setProperty("mail.smtp.port", "" + server.getPort()); + properties.setProperty("mail.smtp.chunksize", "128"); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + final Transport t = session.getTransport("smtp"); + try { + MimeMessage msg = new MimeMessage(session); + msg.setRecipients(Message.RecipientType.TO, "joe@example.com"); + msg.setSubject("test"); + msg.setText("test\r\n"); + t.connect(); + t.sendMessage(msg, msg.getAllRecipients()); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + msg.writeTo(bos); + bos.close(); + byte[] orig = bos.toByteArray(); + assertArrayEquals(orig, message); + } catch (MessagingException ex) { + fail(ex.getMessage()); + } finally { + t.close(); + } + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + server.interrupt(); + // wait for handler to exit + server.join(); + } + } + } + + @Test + public void testBdatFailure() throws Exception { + TestServer server = null; + try { + SMTPHandler handler = new SMTPHandler() { + {{ extensions.add("CHUNKING"); }} + + @Override + public void bdat(String line) throws IOException { + String[] tok = line.split("\\s+"); + int bytes = Integer.parseInt(tok[1]); + boolean last = tok.length > 2 && + tok[2].equalsIgnoreCase("LAST"); + readBdatMessage(bytes, last); + println("444 failed"); + } + }; + server = new TestServer(handler); + server.start(); + + final Properties properties = new Properties(); + properties.setProperty("mail.smtp.host", "localhost"); + properties.setProperty("mail.smtp.port", "" + server.getPort()); + properties.setProperty("mail.smtp.chunksize", "128"); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + final Transport t = session.getTransport("smtp"); + try { + MimeMessage msg = new MimeMessage(session); + msg.setRecipients(Message.RecipientType.TO, "joe@example.com"); + msg.setSubject("test"); + msg.setText("test\r\n"); + t.connect(); + t.sendMessage(msg, msg.getAllRecipients()); + fail("no exception"); + } catch (MessagingException ex) { + // expect it to fail + } finally { + t.close(); + } + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + server.interrupt(); + // wait for handler to exit + server.join(); + } + } + } +}
diff --git a/mail/src/test/java/com/sun/mail/smtp/SMTPCloseTest.java b/mail/src/test/java/com/sun/mail/smtp/SMTPCloseTest.java new file mode 100644 index 0000000..2d26cb8 --- /dev/null +++ b/mail/src/test/java/com/sun/mail/smtp/SMTPCloseTest.java
@@ -0,0 +1,77 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.smtp; + +import java.io.IOException; +import java.util.Properties; + +import javax.mail.Session; +import javax.mail.Transport; + +import org.junit.Test; +import org.junit.Rule; +import org.junit.rules.Timeout; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * Test that the socket is closed when connect times out. + */ +public final class SMTPCloseTest { + + // timeout the test in case of failure + @Rule + public Timeout deadlockTimeout = Timeout.seconds(5); + + @Test + public void test() { + NopServer server = null; + try { + server = new NopServer(); + server.start(); + Thread.sleep(1000); + + final Properties properties = new Properties(); + properties.setProperty("mail.smtp.host", "localhost"); + properties.setProperty("mail.smtp.port", "" + server.getPort()); + properties.setProperty("mail.smtp.timeout", "100"); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + final Transport t = session.getTransport("smtp"); + try { + t.connect(); + } catch (Exception ex) { + // expect an exception when connect times out + } finally { + t.close(); + } + // give the server thread a chance to detect the close + for (int i = 0; i < 10 && !server.eof(); i++) + Thread.sleep(100); + assertTrue("socket closed", server.eof()); + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + server.interrupt(); + } + } + } +}
diff --git a/mail/src/test/java/com/sun/mail/smtp/SMTPConnectFailureTest.java b/mail/src/test/java/com/sun/mail/smtp/SMTPConnectFailureTest.java new file mode 100644 index 0000000..2f5b7d0 --- /dev/null +++ b/mail/src/test/java/com/sun/mail/smtp/SMTPConnectFailureTest.java
@@ -0,0 +1,76 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.smtp; + +import java.util.Properties; +import java.net.ServerSocket; + +import javax.mail.Session; +import javax.mail.Transport; + +import com.sun.mail.util.MailConnectException; + +import org.junit.Test; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +/** + * Test connect failures. + */ +public class SMTPConnectFailureTest { + + private static final String HOST = "localhost"; + private static final int CTO = 20; + + @Test + public void testNoServer() { + try { + // verify that port is not being used + ServerSocket ss = new ServerSocket(0); + int port = ss.getLocalPort(); + ss.close(); + Properties properties = new Properties(); + properties.setProperty("mail.smtp.host", HOST); + properties.setProperty("mail.smtp.port", "" + port); + properties.setProperty("mail.smtp.connectiontimeout", "" + CTO); + Session session = Session.getInstance(properties); + //session.setDebug(true); + + Transport t = session.getTransport("smtp"); + try { + t.connect("test", "test"); + fail("Connected!"); + // failure! + } catch (MailConnectException mcex) { + // success! + assertEquals(HOST, mcex.getHost()); + assertEquals(port, mcex.getPort()); + assertEquals(CTO, mcex.getConnectionTimeout()); + } catch (Exception ex) { + // expect an exception when connect times out + fail(ex.toString()); + } finally { + if (t.isConnected()) + t.close(); + } + } catch (Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } + } +}
diff --git a/mail/src/test/java/com/sun/mail/smtp/SMTPHandler.java b/mail/src/test/java/com/sun/mail/smtp/SMTPHandler.java new file mode 100644 index 0000000..3217b50 --- /dev/null +++ b/mail/src/test/java/com/sun/mail/smtp/SMTPHandler.java
@@ -0,0 +1,289 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.smtp; + +import java.io.*; +import java.util.StringTokenizer; +import java.util.Set; +import java.util.HashSet; +import java.util.logging.Logger; +import java.util.logging.Level; + +import com.sun.mail.test.ProtocolHandler; + +/** + * Handle connection. + * + * @author Bill Shannon + */ +public class SMTPHandler extends ProtocolHandler { + + /** Current line. */ + private String currentLine; + + /** A message being accumulated. */ + private ByteArrayOutputStream messageStream; + + /** SMTP extensions supported. */ + protected Set<String> extensions = new HashSet<String>(); + + /** + * Send greetings. + * + * @throws IOException + * unable to write to socket + */ + @Override + public void sendGreetings() throws IOException { + println("220 localhost dummy server ready"); + } + + /** + * Send String to socket. + * + * @param str + * String to send + * @throws IOException + * unable to write to socket + */ + public void println(final String str) throws IOException { + writer.print(str); + writer.print("\r\n"); + writer.flush(); + } + + /** + * Handle command. + * + * @throws IOException + * unable to read/write to socket + */ + @Override + public void handleCommand() throws IOException { + currentLine = readLine(); + + if (currentLine == null) + return; + + final StringTokenizer st = new StringTokenizer(currentLine, " "); + final String commandName = st.nextToken().toUpperCase(); + if (commandName == null) { + LOGGER.severe("Command name is empty!"); + exit(); + return; + } + + if (commandName.equals("HELO")) { + helo(); + } else if (commandName.equals("EHLO")) { + ehlo(); + } else if (commandName.equals("MAIL")) { + mail(currentLine); + } else if (commandName.equals("RCPT")) { + rcpt(currentLine); + } else if (commandName.equals("DATA")) { + data(); + } else if (commandName.equals("BDAT")) { + bdat(currentLine); + } else if (commandName.equals("NOOP")) { + noop(); + } else if (commandName.equals("RSET")) { + rset(); + } else if (commandName.equals("QUIT")) { + quit(); + } else if (commandName.equals("AUTH")) { + auth(currentLine); + } else { + LOGGER.log(Level.SEVERE, "ERROR command unknown: {0}", commandName); + println("-ERR unknown command"); + } + } + + protected String readLine() throws IOException { + currentLine = super.readLine(); + + if (currentLine == null) { + // XXX - often happens when shutting down + //LOGGER.severe("Current line is null!"); + exit(); + } + return currentLine; + } + + /** + * HELO command. + * + * @throws IOException + * unable to read/write to socket + */ + public void helo() throws IOException { + println("220 Ok"); + } + + /** + * EHLO command. + * + * @throws IOException + * unable to read/write to socket + */ + public void ehlo() throws IOException { + println("250-hello"); + for (String ext : extensions) + println("250-" + ext); + println("250 AUTH PLAIN"); // PLAIN is simplest to fake + } + + /** + * MAIL command. + * + * @throws IOException + * unable to read/write to socket + */ + public void mail(String line) throws IOException { + ok(); + } + + /** + * RCPT command. + * + * @throws IOException + * unable to read/write to socket + */ + public void rcpt(String line) throws IOException { + ok(); + } + + /** + * DATA command. + * + * @throws IOException + * unable to read/write to socket + */ + public void data() throws IOException { + println("354 go ahead"); + readMessage(); + ok(); + } + + /** + * BDAT command. + * + * @throws IOException + * unable to read/write to socket + */ + public void bdat(String line) throws IOException { + StringTokenizer st = new StringTokenizer(line, " "); + String commandName = st.nextToken(); + int bytes = Integer.parseInt(st.nextToken()); + boolean last = st.hasMoreTokens() && + st.nextToken().equalsIgnoreCase("LAST"); + readBdatMessage(bytes, last); + ok(); + } + + /** + * Allow subclasses to override to save the message. + */ + protected void setMessage(byte[] msg) { + } + + /** + * Consume the message and save it. + */ + protected void readMessage() throws IOException { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + PrintWriter pw = new PrintWriter(new OutputStreamWriter(bos, "utf-8")); + String line; + while ((line = super.readLine()) != null) { + if (line.equals(".")) + break; + if (line.startsWith(".")) + line = line.substring(1); + pw.print(line); + pw.print("\r\n"); + } + pw.close(); + setMessage(bos.toByteArray()); + } + + /** + * Consume a chunk of the message and save it. + * Save the entire message when the last chunk is received. + */ + protected void readBdatMessage(int bytes, boolean last) throws IOException { + byte[] data = new byte[bytes]; + int len = data.length; + int off = 0; + int n; + while (len > 0 && (n = in.read(data, off, len)) > 0) { + off += n; + len -= n; + } + if (messageStream == null) + messageStream = new ByteArrayOutputStream(); + messageStream.write(data); + if (last) { + setMessage(messageStream.toByteArray()); + messageStream = null; + } + } + + /** + * NOOP command. + * + * @throws IOException + * unable to read/write to socket + */ + public void noop() throws IOException { + ok(); + } + + /** + * RSET command. + * + * @throws IOException + * unable to read/write to socket + */ + public void rset() throws IOException { + ok(); + } + + /** + * QUIT command. + * + * @throws IOException + * unable to read/write to socket + */ + public void quit() throws IOException { + println("221 BYE"); + exit(); + } + + /** + * AUTH command. + * + * @throws IOException + * unable to read/write to socket + */ + public void auth(String line) throws IOException { + println("235 Authorized"); + } + + protected void ok() throws IOException { + println("250 OK"); + } +}
diff --git a/mail/src/test/java/com/sun/mail/smtp/SMTPIOExceptionTest.java b/mail/src/test/java/com/sun/mail/smtp/SMTPIOExceptionTest.java new file mode 100644 index 0000000..bc80496 --- /dev/null +++ b/mail/src/test/java/com/sun/mail/smtp/SMTPIOExceptionTest.java
@@ -0,0 +1,117 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.smtp; + +import java.io.IOException; +import java.util.Properties; +import java.util.concurrent.CountDownLatch; + +import javax.mail.Session; +import javax.mail.Transport; +import javax.mail.Message; +import javax.mail.MessagingException; +import javax.mail.internet.MimeMessage; +import javax.mail.event.ConnectionAdapter; +import javax.mail.event.ConnectionEvent; + +import com.sun.mail.test.TestServer; + +import org.junit.Test; +import org.junit.Rule; +import org.junit.rules.Timeout; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * Test that the connection is closed when an IOException is detected. + */ +public final class SMTPIOExceptionTest { + + // timeout the test in case of failure + @Rule + public Timeout deadlockTimeout = Timeout.seconds(5); + + private boolean closed = false; + + private static final int TIMEOUT = 200; // I/O timeout, in millis + + @Test + public void test() throws Exception { + TestServer server = null; + final CountDownLatch closedLatch = new CountDownLatch(1); + try { + SMTPHandler handler = new SMTPHandler() { + @Override + public void rcpt(String line) throws IOException { + try { + // delay long enough to cause timeout + Thread.sleep(2 * TIMEOUT); + } catch (Exception ex) { } + super.rcpt(line); + } + }; + server = new TestServer(handler); + server.start(); + + final Properties properties = new Properties(); + properties.setProperty("mail.smtp.host", "localhost"); + properties.setProperty("mail.smtp.port", "" + server.getPort()); + properties.setProperty("mail.smtp.timeout", "" + TIMEOUT); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + final Transport t = session.getTransport("smtp"); + /* + * Use a listener to detect the connection being closed + * because if we called isConnected() and the connection + * wasn't already closed, it will issue a command that + * might detect that the connection was closed, even + * though it wasn't closed already. + */ + t.addConnectionListener(new ConnectionAdapter() { + @Override + public void closed(ConnectionEvent e) { + closedLatch.countDown(); + } + }); + try { + MimeMessage msg = new MimeMessage(session); + msg.setRecipients(Message.RecipientType.TO, "joe@example.com"); + msg.setSubject("test"); + msg.setText("test"); + t.connect(); + t.sendMessage(msg, msg.getAllRecipients()); + } catch (MessagingException ex) { + // expect an exception from sendMessage + closedLatch.await(); // wait for the listener to run + // if we get here, the listener was called - SUCCESS + } finally { + t.close(); + } + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + server.interrupt(); + // wait for handler to exit + server.join(); + } + } + } +}
diff --git a/mail/src/test/java/com/sun/mail/smtp/SMTPLoginHandler.java b/mail/src/test/java/com/sun/mail/smtp/SMTPLoginHandler.java new file mode 100644 index 0000000..0b294ea --- /dev/null +++ b/mail/src/test/java/com/sun/mail/smtp/SMTPLoginHandler.java
@@ -0,0 +1,151 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.smtp; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.StringTokenizer; +import java.util.logging.Logger; +import java.util.logging.Level; + +import com.sun.mail.util.BASE64EncoderStream; +import com.sun.mail.util.BASE64DecoderStream; +import com.sun.mail.util.ASCIIUtility; + +/** + * Handle connection with LOGIN or PLAIN authentication. + * + * @author Bill Shannon + */ +public class SMTPLoginHandler extends SMTPHandler { + protected String username = "test"; + protected String password = "test"; + + /** + * EHLO command. + * + * @throws IOException + * unable to read/write to socket + */ + @Override + public void ehlo() throws IOException { + println("250-hello"); + println("250-SMTPUTF8"); + println("250-8BITMIME"); + println("250 AUTH PLAIN LOGIN"); + } + + /** + * AUTH command. + * + * @throws IOException + * unable to read/write to socket + */ + @Override + public void auth(String line) throws IOException { + StringTokenizer ct = new StringTokenizer(line, " "); + String commandName = ct.nextToken().toUpperCase(); + String mech = ct.nextToken().toUpperCase(); + String ir = ""; + if (ct.hasMoreTokens()) + ir = ct.nextToken(); + + if (LOGGER.isLoggable(Level.FINE)) + LOGGER.fine(line); + if (mech.equalsIgnoreCase("PLAIN")) + plain(ir); + else if (mech.equalsIgnoreCase("LOGIN")) + login(ir); + else + println("501 bad AUTH mechanism"); + } + + /** + * AUTH LOGIN + */ + private void login(String ir) throws IOException { + println("334"); + // read user name + String resp = readLine(); + if (!isBase64(resp)) { + println("501 response not base64"); + return; + } + byte[] response = resp.getBytes(StandardCharsets.US_ASCII); + response = BASE64DecoderStream.decode(response); + String u = new String(response, StandardCharsets.UTF_8); + if (LOGGER.isLoggable(Level.FINE)) + LOGGER.fine("USER: " + u); + println("334"); + + // read password + resp = readLine(); + if (!isBase64(resp)) { + println("501 response not base64"); + return; + } + response = resp.getBytes(StandardCharsets.US_ASCII); + response = BASE64DecoderStream.decode(response); + String p = new String(response, StandardCharsets.UTF_8); + if (LOGGER.isLoggable(Level.FINE)) + LOGGER.fine("PASSWORD: " + p); + + //System.out.printf("USER: %s, PASSWORD: %s%n", u, p); + if (!u.equals(username) || !p.equals(password)) { + println("535 authentication failed"); + return; + } + + println("235 Authenticated"); + } + + /** + * AUTH PLAIN + */ + private void plain(String ir) throws IOException { + String auth = new String(BASE64DecoderStream.decode( + ir.getBytes(StandardCharsets.US_ASCII)), + StandardCharsets.UTF_8); + String[] ap = auth.split("\000"); + String u = ap[1]; + String p = ap[2]; + //System.out.printf("USER: %s, PASSWORD: %s%n", u, p); + if (!u.equals(username) || !p.equals(password)) { + println("535 authentication failed"); + return; + } + println("235 Authenticated"); + } + + /** + * Is every character in the string a base64 character? + */ + private boolean isBase64(String s) { + int len = s.length(); + if (s.endsWith("==")) + len -= 2; + else if (s.endsWith("=")) + len--; + for (int i = 0; i < len; i++) { + char c = s.charAt(i); + if (!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || c == '+' || c == '/')) + return false; + } + return true; + } +}
diff --git a/mail/src/test/java/com/sun/mail/smtp/SMTPSaslHandler.java b/mail/src/test/java/com/sun/mail/smtp/SMTPSaslHandler.java new file mode 100644 index 0000000..2ab6a2b --- /dev/null +++ b/mail/src/test/java/com/sun/mail/smtp/SMTPSaslHandler.java
@@ -0,0 +1,192 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.smtp; + +import java.io.IOException; +import java.util.StringTokenizer; +import java.util.logging.Logger; +import java.util.logging.Level; + +import javax.security.sasl.*; +import javax.security.auth.callback.*; + +import com.sun.mail.util.BASE64EncoderStream; +import com.sun.mail.util.BASE64DecoderStream; +import com.sun.mail.util.ASCIIUtility; + +/** + * Handle connection with SASL authentication. + * + * @author Bill Shannon + */ +public class SMTPSaslHandler extends SMTPHandler { + + /** + * EHLO command. + * + * @throws IOException + * unable to read/write to socket + */ + @Override + public void ehlo() throws IOException { + println("250-hello"); + println("250 AUTH DIGEST-MD5"); + } + + /** + * AUTH command. + * + * @throws IOException + * unable to read/write to socket + */ + @Override + public void auth(String line) throws IOException { + StringTokenizer ct = new StringTokenizer(line, " "); + String commandName = ct.nextToken().toUpperCase(); + String mech = ct.nextToken().toUpperCase(); + String ir = ""; + if (ct.hasMoreTokens()) + ir = ct.nextToken(); + + final String u = "test"; + final String p = "test"; + final String realm = "test"; + + CallbackHandler cbh = new CallbackHandler() { + @Override + public void handle(Callback[] callbacks) { + if (LOGGER.isLoggable(Level.FINE)) + LOGGER.fine("SASL callback length: " + callbacks.length); + for (int i = 0; i < callbacks.length; i++) { + if (LOGGER.isLoggable(Level.FINE)) + LOGGER.fine("SASL callback " + i + ": " + callbacks[i]); + if (callbacks[i] instanceof NameCallback) { + NameCallback ncb = (NameCallback)callbacks[i]; + ncb.setName(u); + } else if (callbacks[i] instanceof PasswordCallback) { + PasswordCallback pcb = (PasswordCallback)callbacks[i]; + pcb.setPassword(p.toCharArray()); + } else if (callbacks[i] instanceof AuthorizeCallback) { + AuthorizeCallback ac = (AuthorizeCallback)callbacks[i]; + if (LOGGER.isLoggable(Level.FINE)) + LOGGER.fine("SASL authorize: " + + "authn: " + ac.getAuthenticationID() + ", " + + "authz: " + ac.getAuthorizationID() + ", " + + "authorized: " + ac.getAuthorizedID()); + ac.setAuthorized(true); + } else if (callbacks[i] instanceof RealmCallback) { + RealmCallback rcb = (RealmCallback)callbacks[i]; + rcb.setText(realm != null ? + realm : rcb.getDefaultText()); + } else if (callbacks[i] instanceof RealmChoiceCallback) { + RealmChoiceCallback rcb = + (RealmChoiceCallback)callbacks[i]; + if (realm == null) + rcb.setSelectedIndex(rcb.getDefaultChoice()); + else { + // need to find specified realm in list + String[] choices = rcb.getChoices(); + for (int k = 0; k < choices.length; k++) { + if (choices[k].equals(realm)) { + rcb.setSelectedIndex(k); + break; + } + } + } + } + } + } + }; + + SaslServer ss; + try { + ss = Sasl.createSaslServer(mech, "smtp", "localhost", null, cbh); + } catch (SaslException sex) { + LOGGER.log(Level.FINE, "Failed to create SASL server", sex); + println("501 Failed to create SASL server"); + return; + } + if (ss == null) { + LOGGER.fine("No SASL support"); + println("501 No SASL support"); + return; + } + if (LOGGER.isLoggable(Level.FINE)) + LOGGER.fine("SASL server " + ss.getMechanismName()); + + byte[] response = ir.getBytes(); + while (!ss.isComplete()) { + try { + byte[] chal = ss.evaluateResponse(response); + // send challenge + if (LOGGER.isLoggable(Level.FINE)) + LOGGER.fine("SASL challenge: " + + ASCIIUtility.toString(chal, 0, chal.length)); + byte[] ba = BASE64EncoderStream.encode(chal); + if (ba.length > 0) + println("334 " + + ASCIIUtility.toString(ba, 0, ba.length)); + else + println("334"); + // read response + String resp = readLine(); + if (!isBase64(resp)) { + println("501 response not base64"); + break; + } + response = resp.getBytes(); + response = BASE64DecoderStream.decode(response); + } catch (SaslException ex) { + println("501 " + ex.toString()); + break; + } + } + + if (ss.isComplete() /*&& status == SUCCESS*/) { + String qop = (String)ss.getNegotiatedProperty(Sasl.QOP); + if (qop != null && (qop.equalsIgnoreCase("auth-int") || + qop.equalsIgnoreCase("auth-conf"))) { + // XXX - NOT SUPPORTED!!! + LOGGER.fine( + "SASL Mechanism requires integrity or confidentiality"); + println("501 " + + "SASL Mechanism requires integrity or confidentiality"); + return; + } + } + + println("235 Authenticated"); + } + + /** + * Is every character in the string a base64 character? + */ + private boolean isBase64(String s) { + int len = s.length(); + if (s.endsWith("==")) + len -= 2; + else if (s.endsWith("=")) + len--; + for (int i = 0; i < len; i++) { + char c = s.charAt(i); + if (!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || c == '+' || c == '/')) + return false; + } + return true; + } +}
diff --git a/mail/src/test/java/com/sun/mail/smtp/SMTPSaslLoginTest.java b/mail/src/test/java/com/sun/mail/smtp/SMTPSaslLoginTest.java new file mode 100644 index 0000000..fe8b5ba --- /dev/null +++ b/mail/src/test/java/com/sun/mail/smtp/SMTPSaslLoginTest.java
@@ -0,0 +1,289 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.smtp; + +import java.io.IOException; +import java.util.Properties; + +import javax.mail.Session; +import javax.mail.Transport; +import javax.mail.AuthenticationFailedException; + +import com.sun.mail.test.TestServer; + +import org.junit.Test; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * Test login using a SASL mechanism on the server + * with SASL and non-SASL on the client. + */ +public class SMTPSaslLoginTest { + + /** + * Test using non-SASL DIGEST-MD5. + */ + @Test + public void testSuccess() { + TestServer server = null; + try { + server = new TestServer(new SMTPSaslHandler()); + server.start(); + + Properties properties = new Properties(); + properties.setProperty("mail.smtp.host", "localhost"); + properties.setProperty("mail.smtp.port", "" + server.getPort()); + //properties.setProperty("mail.debug.auth", "true"); + Session session = Session.getInstance(properties); + //session.setDebug(true); + + Transport t = session.getTransport("smtp"); + try { + t.connect("test", "test"); + // success! + } catch (Exception ex) { + fail(ex.toString()); + } finally { + t.close(); + } + } catch (Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + server.interrupt(); + } + } + } + + /** + * Test using non-SASL DIGEST-MD5 with incorrect password. + */ + @Test + public void testFailure() { + TestServer server = null; + try { + server = new TestServer(new SMTPSaslHandler()); + server.start(); + + Properties properties = new Properties(); + properties.setProperty("mail.smtp.host", "localhost"); + properties.setProperty("mail.smtp.port", "" + server.getPort()); + //properties.setProperty("mail.debug.auth", "true"); + Session session = Session.getInstance(properties); + //session.setDebug(true); + + Transport t = session.getTransport("smtp"); + try { + t.connect("test", "xtest"); + // should have failed + fail("wrong password succeeded"); + } catch (AuthenticationFailedException ex) { + // success! + } catch (Exception ex) { + fail(ex.toString()); + } finally { + t.close(); + } + } catch (Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + server.interrupt(); + } + } + } + + /** + * Test using SASL DIGEST-MD5. + */ + @Test + public void testSaslSuccess() { + TestServer server = null; + try { + server = new TestServer(new SMTPSaslHandler()); + server.start(); + + Properties properties = new Properties(); + properties.setProperty("mail.smtp.host", "localhost"); + properties.setProperty("mail.smtp.port", "" + server.getPort()); + properties.setProperty("mail.smtp.sasl.enable", "true"); + properties.setProperty("mail.smtp.sasl.mechanisms", "DIGEST-MD5"); + properties.setProperty("mail.smtp.auth.digest-md5.disable", "true"); + //properties.setProperty("mail.debug.auth", "true"); + Session session = Session.getInstance(properties); + //session.setDebug(true); + + Transport t = session.getTransport("smtp"); + try { + t.connect("test", "test"); + // success! + } catch (Exception ex) { + fail(ex.toString()); + } finally { + t.close(); + } + } catch (Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + server.interrupt(); + } + } + } + + /** + * Test using SASL DIGEST-MD5 with incorrect password. + */ + @Test + public void testSaslFailure() { + TestServer server = null; + try { + server = new TestServer(new SMTPSaslHandler()); + server.start(); + + Properties properties = new Properties(); + properties.setProperty("mail.smtp.host", "localhost"); + properties.setProperty("mail.smtp.port", "" + server.getPort()); + properties.setProperty("mail.smtp.sasl.enable", "true"); + properties.setProperty("mail.smtp.sasl.mechanisms", "DIGEST-MD5"); + properties.setProperty("mail.smtp.auth.digest-md5.disable", "true"); + //properties.setProperty("mail.debug.auth", "true"); + Session session = Session.getInstance(properties); + //session.setDebug(true); + + Transport t = session.getTransport("smtp"); + try { + t.connect("test", "xtest"); + // should have failed + fail("wrong password succeeded"); + } catch (AuthenticationFailedException ex) { + // success! + } catch (Exception ex) { + fail(ex.toString()); + } finally { + t.close(); + } + } catch (Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + server.interrupt(); + } + } + } + + /** + * Test that AUTH with no mechanisms fails. + */ + @Test + public void testAuthNoParam() { + TestServer server = null; + try { + server = new TestServer(new SMTPSaslHandler() { + @Override + public void ehlo() throws IOException { + println("250-hello"); + println("250 AUTH"); + } + }); + server.start(); + + Properties properties = new Properties(); + properties.setProperty("mail.smtp.host", "localhost"); + properties.setProperty("mail.smtp.port", "" + server.getPort()); + //properties.setProperty("mail.debug.auth", "true"); + Session session = Session.getInstance(properties); + //session.setDebug(true); + + Transport t = session.getTransport("smtp"); + try { + t.connect("test", "test"); + fail("Connect didn't fail"); + } catch (AuthenticationFailedException ex) { + // success + } catch (Exception ex) { + fail(ex.toString()); + } finally { + t.close(); + } + } catch (Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + server.interrupt(); + } + } + } + + /** + * Test that no AUTH succeeds by skipping authentication entirely. + */ + @Test + public void testNoAuth() { + TestServer server = null; + try { + server = new TestServer(new SMTPSaslHandler() { + @Override + public void ehlo() throws IOException { + println("250-hello"); + println("250 XXX"); + } + @Override + public void auth(String line) throws IOException { + println("501 Authentication failed"); + } + }); + server.start(); + + Properties properties = new Properties(); + properties.setProperty("mail.smtp.host", "localhost"); + properties.setProperty("mail.smtp.port", "" + server.getPort()); + //properties.setProperty("mail.debug.auth", "true"); + Session session = Session.getInstance(properties); + //session.setDebug(true); + + Transport t = session.getTransport("smtp"); + try { + t.connect("test", "test"); + // success + } catch (Exception ex) { + fail(ex.toString()); + } finally { + t.close(); + } + } catch (Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + server.interrupt(); + } + } + } +}
diff --git a/mail/src/test/java/com/sun/mail/smtp/SMTPUtf8Test.java b/mail/src/test/java/com/sun/mail/smtp/SMTPUtf8Test.java new file mode 100644 index 0000000..07d2386 --- /dev/null +++ b/mail/src/test/java/com/sun/mail/smtp/SMTPUtf8Test.java
@@ -0,0 +1,264 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.smtp; + +import java.io.IOException; +import java.util.Properties; +import java.util.StringTokenizer; + +import javax.mail.Session; +import javax.mail.Message; +import javax.mail.Transport; +import javax.mail.AuthenticationFailedException; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeMessage; + +import com.sun.mail.test.TestServer; + +import org.junit.Test; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +/** + * Test UTF-8 user names and recipients. + */ +public class SMTPUtf8Test { + + /** + * Test using UTF-8 user name. + */ + @Test + public void testUtf8UserName() { + TestServer server = null; + final String user = "test\u03b1"; + try { + server = new TestServer(new SMTPLoginHandler() { + @Override + public void auth(String line) throws IOException { + username = user; + password = user; + super.auth(line); + } + }); + server.start(); + + Properties properties = new Properties(); + properties.setProperty("mail.smtp.host", "localhost"); + properties.setProperty("mail.smtp.port", "" + server.getPort()); + properties.setProperty("mail.smtp.auth.mechanisms", "LOGIN"); + properties.setProperty("mail.mime.allowutf8", "true"); + //properties.setProperty("mail.debug.auth", "true"); + Session session = Session.getInstance(properties); + //session.setDebug(true); + + Transport t = session.getTransport("smtp"); + try { + t.connect(user, user); + // success! + } catch (Exception ex) { + fail(ex.toString()); + } finally { + t.close(); + } + } catch (Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + server.interrupt(); + } + } + } + + /** + * Test using UTF-8 user name but without mail.mime.allowutf8. + */ + @Test + public void testUtf8UserNameNoAllowUtf8() { + TestServer server = null; + final String user = "test\u03b1"; + try { + server = new TestServer(new SMTPLoginHandler() { + @Override + public void auth(String line) throws IOException { + username = user; + password = user; + super.auth(line); + } + }); + server.start(); + + Properties properties = new Properties(); + properties.setProperty("mail.smtp.host", "localhost"); + properties.setProperty("mail.smtp.port", "" + server.getPort()); + properties.setProperty("mail.smtp.auth.mechanisms", "LOGIN"); + //properties.setProperty("mail.debug.auth", "true"); + Session session = Session.getInstance(properties); + //session.setDebug(true); + + Transport t = session.getTransport("smtp"); + try { + t.connect(user, user); + // success! + } catch (Exception ex) { + fail(ex.toString()); + } finally { + t.close(); + } + } catch (Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + server.interrupt(); + } + } + } + + /** + * Test using UTF-8 user name and PLAIN but without mail.mime.allowutf8. + */ + @Test + public void testUtf8UserNamePlain() { + TestServer server = null; + final String user = "test\u03b1"; + try { + server = new TestServer(new SMTPLoginHandler() { + @Override + public void auth(String line) throws IOException { + username = user; + password = user; + super.auth(line); + } + }); + server.start(); + + Properties properties = new Properties(); + properties.setProperty("mail.smtp.host", "localhost"); + properties.setProperty("mail.smtp.port", "" + server.getPort()); + properties.setProperty("mail.smtp.auth.mechanisms", "PLAIN"); + //properties.setProperty("mail.debug.auth", "true"); + Session session = Session.getInstance(properties); + //session.setDebug(true); + + Transport t = session.getTransport("smtp"); + try { + t.connect(user, user); + // success! + } catch (Exception ex) { + fail(ex.toString()); + } finally { + t.close(); + } + } catch (Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + server.interrupt(); + } + } + } + + private static class Envelope { + public String from; + public String to; + } + + /** + * Test using UTF-8 From and To address. + */ + @Test + public void testUtf8From() { + TestServer server = null; + final String test = "test\u03b1"; + final String saddr = test + "@" + test + ".com"; + final Envelope env = new Envelope(); + try { + server = new TestServer(new SMTPHandler() { + @Override + public void ehlo() throws IOException { + println("250-hello"); + println("250-SMTPUTF8"); + println("250 AUTH PLAIN"); + } + + @Override + public void mail(String line) throws IOException { + StringTokenizer st = new StringTokenizer(line); + st.nextToken(); // skip "MAIL" + env.from = st.nextToken(). + replaceFirst("FROM:<(.*)>", "$1"); + if (!st.hasMoreTokens() || + !st.nextToken().equals("SMTPUTF8")) + println("500 fail"); + else + ok(); + } + + @Override + public void rcpt(String line) throws IOException { + StringTokenizer st = new StringTokenizer(line); + st.nextToken(); // skip "RCPT" + env.to = st.nextToken(). + replaceFirst("TO:<(.*)>", "$1"); + ok(); + } + }); + server.start(); + + Properties properties = new Properties(); + properties.setProperty("mail.smtp.host", "localhost"); + properties.setProperty("mail.smtp.port", "" + server.getPort()); + properties.setProperty("mail.mime.allowutf8", "true"); + //properties.setProperty("mail.debug.auth", "true"); + Session session = Session.getInstance(properties); + //session.setDebug(true); + + Transport t = session.getTransport("smtp"); + try { + MimeMessage msg = new MimeMessage(session); + InternetAddress addr = new InternetAddress(saddr, test); + msg.setFrom(addr); + msg.setRecipient(Message.RecipientType.TO, addr); + msg.setSubject(test); + msg.setText(test + "\n"); + t.connect("test", "test"); + t.sendMessage(msg, msg.getAllRecipients()); + } catch (Exception ex) { + fail(ex.toString()); + } finally { + t.close(); + } + } catch (Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + server.interrupt(); + } + } + // after we're sure the server is done + assertEquals(saddr, env.from); + assertEquals(saddr, env.to); + } +}
diff --git a/mail/src/test/java/com/sun/mail/smtp/SMTPWriteTimeoutTest.java b/mail/src/test/java/com/sun/mail/smtp/SMTPWriteTimeoutTest.java new file mode 100644 index 0000000..d9fd759 --- /dev/null +++ b/mail/src/test/java/com/sun/mail/smtp/SMTPWriteTimeoutTest.java
@@ -0,0 +1,102 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.smtp; + +import java.io.IOException; +import java.util.Properties; + +import javax.activation.DataHandler; +import javax.mail.Session; +import javax.mail.Transport; +import javax.mail.Message; +import javax.mail.MessagingException; +import javax.mail.internet.MimeMessage; +import javax.mail.util.ByteArrayDataSource; + +import com.sun.mail.test.TestServer; + +import org.junit.Test; +import org.junit.Rule; +import org.junit.rules.Timeout; +import static org.junit.Assert.fail; + +/** + * Test that write timeouts work. + */ +public final class SMTPWriteTimeoutTest { + + // timeout the test in case of failure + @Rule + public Timeout deadlockTimeout = Timeout.seconds(10); + + private static final int TIMEOUT = 200; // write timeout, in millis + + @Test + public void test() throws Exception { + TestServer server = null; + try { + SMTPHandler handler = new SMTPHandler() { + @Override + public void readMessage() throws IOException { + try { + // delay long enough to cause timeout + Thread.sleep(5 * TIMEOUT); + } catch (Exception ex) { } + super.readMessage(); + } + }; + server = new TestServer(handler); + server.start(); + Thread.sleep(1000); + + final Properties properties = new Properties(); + properties.setProperty("mail.smtp.host", "localhost"); + properties.setProperty("mail.smtp.port", "" + server.getPort()); + properties.setProperty("mail.smtp.writetimeout", "" + TIMEOUT); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + final Transport t = session.getTransport("smtp"); + try { + MimeMessage msg = new MimeMessage(session); + msg.setRecipients(Message.RecipientType.TO, "joe@example.com"); + msg.setSubject("test"); + byte[] bytes = new byte[16*1024*1024]; + msg.setDataHandler( + new DataHandler(new ByteArrayDataSource(bytes, + "application/octet-stream"))); + t.connect(); + t.sendMessage(msg, msg.getAllRecipients()); + fail("No exception"); + } catch (MessagingException ex) { + // expect an exception from sendMessage + } finally { + t.close(); + } + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + server.interrupt(); + // wait long enough for handler to exit + Thread.sleep(2 * TIMEOUT); + } + } + } +}
diff --git a/mail/src/test/java/com/sun/mail/test/AsciiStringInputStream.java b/mail/src/test/java/com/sun/mail/test/AsciiStringInputStream.java new file mode 100644 index 0000000..39caf7b --- /dev/null +++ b/mail/src/test/java/com/sun/mail/test/AsciiStringInputStream.java
@@ -0,0 +1,54 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.test; + +import java.io.InputStream; + +/** + * Replacement for deprecated java.io.StringBufferInputStream + */ +public class AsciiStringInputStream extends InputStream { + + private final String input; + private int position; + + public AsciiStringInputStream(String input) { + this(input, true); + } + + public AsciiStringInputStream(String input, boolean strict) { + if (strict) { + for (int i = 0; i < input.length(); i++) { + if (input.charAt(i) > 0x7F) { + throw new IllegalArgumentException("Not an ASCII string"); + } + } + } + + this.input = input; + } + + @Override + public int read() { + if (position < input.length()) { + return input.charAt(position++) & 0xFF; + } else { + return -1; + } + } + +}
diff --git a/mail/src/test/java/com/sun/mail/test/ClassLoaderSuite.java b/mail/src/test/java/com/sun/mail/test/ClassLoaderSuite.java new file mode 100644 index 0000000..ceb4813 --- /dev/null +++ b/mail/src/test/java/com/sun/mail/test/ClassLoaderSuite.java
@@ -0,0 +1,205 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.test; + +import java.lang.annotation.*; +import java.net.URL; +import java.net.MalformedURLException; +import java.net.URLClassLoader; +import java.util.List; +import java.util.ArrayList; + +import org.junit.runners.Suite; +import org.junit.runners.ParentRunner; +import org.junit.runners.model.InitializationError; +import org.junit.runners.model.RunnerBuilder; +import org.junit.runner.Runner; +import org.junit.runner.notification.RunNotifier; + +/** + * A special test suite that loads each of the test classes + * in a separate class loader, along with the class under test. + * This allows the tests to test methods whose behavior depends on + * the value of a System property that's read at class initialization + * time; each test can set a different value of the System property + * and the corresponding class under test will be loaded in a + * separate class loader. <p> + * + * To use this class, create a test suite class: + * + * <pre> + * @RunWith(ClassLoaderSuite.class) + * @SuiteClasses({ MyTest1.class, MyTest2.class }) + * @TestClass(ClassToTest.class) + * public class MyTestSuite { + * } + * </pre> + * + * The MyTest1 and MyTest2 classes are written as normal JUnit + * test classes. Set the System property to test in the @BeforeClass + * method of these classes. + * + * @author Bill Shannon + */ + +public class ClassLoaderSuite extends Suite { + /** + * An annotation to be used on the test suite class to indicate + * the class under test. The class is used to find the classpath + * to allow loading the class under test in a separate class loader. + * Note that other classes in the same classpath will also be loaded + * in the separate class loader, along with the test classes. + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + @Inherited + public @interface TestClass { + public Class<?> value(); + } + + /** + * A special class loader that loads classes from its own class path + * (specified via URLs) before delegating to the parent class loader. + * This is used to load the test classes in separate class loaders, + * even though those classes are also loaded in the parent class loader. + */ + static class TestClassLoader extends URLClassLoader { + public TestClassLoader(URL[] urls, ClassLoader parent) { + super(urls, parent); + } + + @Override + public Class<?> loadClass(String name, boolean resolve) + throws ClassNotFoundException { + Class<?> c = null; + try { + c = findLoadedClass(name); + if (c != null) + return c; + c = findClass(name); + if (resolve) + resolveClass(c); + } catch (ClassNotFoundException cex) { + c = super.loadClass(name, resolve); + } + return c; + } + } + + /** + * Constructor. + */ + public ClassLoaderSuite(Class<?> klass, RunnerBuilder builder) + throws InitializationError { + super(builder, klass, + reloadClasses(getTestClass(klass), getSuiteClasses(klass))); + } + + /** + * Set the thread's context class loader to the class loader + * for the test class. + */ + @Override + protected void runChild(Runner runner, RunNotifier notifier) { + // XXX - is it safe to assume it's always a ParentRunner? + ParentRunner<?> pr = (ParentRunner<?>)runner; + ClassLoader cl = null; + try { + cl = Thread.currentThread().getContextClassLoader(); + Thread.currentThread().setContextClassLoader( + pr.getTestClass().getJavaClass().getClassLoader()); + super.runChild(runner, notifier); + } finally { + Thread.currentThread().setContextClassLoader(cl); + } + } + + /** + * Get the value of the SuiteClasses annotation. + */ + private static Class<?>[] getSuiteClasses(Class<?> klass) + throws InitializationError { + SuiteClasses annotation = klass.getAnnotation(SuiteClasses.class); + if (annotation == null) + throw new InitializationError("class '" + klass.getName() + + "' must have a SuiteClasses annotation"); + return annotation.value(); + } + + /** + * Get the value of the TestClass annotation. + */ + private static Class<?> getTestClass(Class<?> klass) + throws InitializationError { + TestClass annotation = klass.getAnnotation(TestClass.class); + if (annotation == null) + throw new InitializationError("class '" + klass.getName() + + "' must have a TestClass annotation"); + return annotation.value(); + } + + /** + * Reload the classes in a separate class loader. + */ + private static Class<?>[] reloadClasses(Class<?> testClass, + Class<?>[] suiteClasses) throws InitializationError { + URL[] urls = new URL[] { + classpathOf(testClass), + classpathOf(ClassLoaderSuite.class) + }; + Class<?> sc = null; + try { + for (int i = 0; i < suiteClasses.length; i++) { + sc = suiteClasses[i]; + ClassLoader cl = new TestClassLoader(urls, + ClassLoaderSuite.class.getClassLoader()); + suiteClasses[i] = cl.loadClass(sc.getName()); + } + return suiteClasses; + } catch (ClassNotFoundException cex) { + throw new InitializationError("could not reload class: " + sc); + } + } + + /** + * Return the classpath entry used to load the named resource. + * XXX - Only handles file: and jar: URLs. + */ + private static URL classpathOf(Class<?> c) { + String name = "/" + c.getName().replace('.', '/') + ".class"; + try { + URL url = ClassLoaderSuite.class.getResource(name); + if (url.getProtocol().equals("file")) { + String file = url.getPath(); + if (file.endsWith(name)) // has to be true? + file = file.substring(0, file.length() - name.length() + 1); +//System.out.println("file URL " + url + " has CLASSPATH " + file); + return new URL("file", null, file); + } else if (url.getProtocol().equals("jar")) { + String file = url.getPath(); + int i = file.lastIndexOf('!'); + if (i >= 0) + file = file.substring(0, i); +//System.out.println("jar URL " + url + " has CLASSPATH " + file); + return new URL(file); + } else + return url; + } catch (MalformedURLException mex) { + return null; + } + } +}
diff --git a/mail/src/test/java/com/sun/mail/test/NullOutputStream.java b/mail/src/test/java/com/sun/mail/test/NullOutputStream.java new file mode 100644 index 0000000..d78fc87 --- /dev/null +++ b/mail/src/test/java/com/sun/mail/test/NullOutputStream.java
@@ -0,0 +1,37 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.test; + +import java.io.*; + +/** + * An OutputStream that throws away all data written to it. + */ +public class NullOutputStream extends OutputStream { + + @Override + public void write(int b) throws IOException { + } + + @Override + public void write(byte[] b) throws IOException { + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + } +}
diff --git a/mail/src/test/java/com/sun/mail/test/ProtocolHandler.java b/mail/src/test/java/com/sun/mail/test/ProtocolHandler.java new file mode 100644 index 0000000..434ff0a --- /dev/null +++ b/mail/src/test/java/com/sun/mail/test/ProtocolHandler.java
@@ -0,0 +1,176 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.test; + +import java.io.InputStream; +import java.io.BufferedInputStream; +import java.io.PushbackInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.net.Socket; +import java.net.SocketException; +import java.nio.charset.StandardCharsets; +import java.util.logging.Logger; +import java.util.logging.Level; +import javax.net.ssl.SSLException; + +/** + * Handle protocol connection. + * + * Inspired by, and derived from, POP3Handler by sbo. + * + * @author sbo + * @author Bill Shannon + */ +public abstract class ProtocolHandler implements Runnable, Cloneable { + + /** Logger for this class. */ + protected final Logger LOGGER = Logger.getLogger(this.getClass().getName()); + + /** Client socket. */ + protected Socket clientSocket; + + /** Quit? */ + protected boolean quit; + + /** Writer to socket. */ + protected PrintWriter writer; + + /** Input from socket. */ + protected InputStream in; + + /** + * Sets the client socket. + * + * @param clientSocket the client socket + */ + public final void setClientSocket(final Socket clientSocket) + throws IOException { + this.clientSocket = clientSocket; + writer = new PrintWriter(new OutputStreamWriter( + clientSocket.getOutputStream(), StandardCharsets.UTF_8)); + in = new BufferedInputStream(clientSocket.getInputStream()); + } + + /** + * Optionally send a greeting when first connected. + */ + public void sendGreetings() throws IOException { + } + + /** + * Read and process a single command. + */ + public abstract void handleCommand() throws IOException; + + /** + * Read a single line terminated by newline or CRLF. + * Convert the UTF-8 bytes in the line (minus the line terminator) + * to a String. + */ + protected String readLine() throws IOException { + byte[] buf = new byte[128]; + + int room = buf.length; + int offset = 0; + int c; + + while ((c = in.read()) != -1) { + if (c == '\n') { + break; + } else if (c == '\r') { + int c2 = in.read(); + if ((c2 != '\n') && (c2 != -1)) { + if (!(in instanceof PushbackInputStream)) + this.in = new PushbackInputStream(in); + ((PushbackInputStream)in).unread(c2); + } + break; + } else { + if (--room < 0) { + byte[] nbuf = new byte[offset + 128]; + room = nbuf.length - offset - 1; + System.arraycopy(buf, 0, nbuf, 0, offset); + buf = nbuf; + } + buf[offset++] = (byte)c; + } + } + if ((c == -1) && (offset == 0)) + return null; + return new String(buf, 0, offset, StandardCharsets.UTF_8); + } + + /** + * {@inheritDoc} + */ + @Override + public final void run() { + try { + + sendGreetings(); + + while (!quit) { + handleCommand(); + } + + //clientSocket.close(); + } catch (SocketException sex) { + // ignore it, often get "connection reset" when client closes + } catch (SSLException sex) { + // ignore it, often occurs when testing SSL + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Error", e); + } finally { + try { + if (clientSocket != null) + clientSocket.close(); + } catch (final IOException ioe) { + LOGGER.log(Level.SEVERE, "Error", ioe); + } + } + } + + /** + * Quit. + */ + public void exit() { + quit = true; + try { + if (clientSocket != null && !clientSocket.isClosed()) { + clientSocket.close(); + clientSocket = null; + } + } catch (final IOException e) { + LOGGER.log(Level.SEVERE, "Error", e); + } + } + + /** + * {@inheritDoc} + */ + @Override + public Object clone() { + try { + return super.clone(); + } catch (final CloneNotSupportedException e) { + throw new AssertionError(e); + } + } +}
diff --git a/mail/src/test/java/com/sun/mail/test/SavedSocketFactory.java b/mail/src/test/java/com/sun/mail/test/SavedSocketFactory.java new file mode 100644 index 0000000..095f2ce --- /dev/null +++ b/mail/src/test/java/com/sun/mail/test/SavedSocketFactory.java
@@ -0,0 +1,80 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.test; + +import java.io.IOException; +import java.net.Socket; +import java.net.InetAddress; +import javax.net.SocketFactory; + +/** + * A SocketFactory that saves the Socket it creates so that it can be + * accessed later. Useful for checking that sockets are closed properly. + */ +public class SavedSocketFactory extends SocketFactory { + private SocketFactory factory; + private Socket saved; + + public SavedSocketFactory() { + super(); + try { + factory = SocketFactory.getDefault(); + } catch(Exception ex) { + // ignore + } + } + + @Override + public Socket createSocket() throws IOException { + return save(factory.createSocket()); + } + + @Override + public Socket createSocket(InetAddress host, int port) + throws IOException { + return save(factory.createSocket(host, port)); + } + + @Override + public Socket createSocket(InetAddress address, int port, + InetAddress localAddress, int localPort) + throws IOException { + return save(factory.createSocket( + address, port, localAddress, localPort)); + } + + @Override + public Socket createSocket(String host, int port) throws IOException { + return save(factory.createSocket(host, port)); + } + + @Override + public Socket createSocket(String host, int port, + InetAddress localHost, int localPort) + throws IOException { + return save(factory.createSocket(host, port, localHost, localPort)); + } + + public Socket getSocket() { + return saved; + } + + private Socket save(Socket s) { + saved = s; + return saved; + } +}
diff --git a/mail/src/test/java/com/sun/mail/test/TestSSLSocketFactory.java b/mail/src/test/java/com/sun/mail/test/TestSSLSocketFactory.java new file mode 100644 index 0000000..de5a8b5 --- /dev/null +++ b/mail/src/test/java/com/sun/mail/test/TestSSLSocketFactory.java
@@ -0,0 +1,197 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.test; + +import java.io.IOException; +import java.net.*; +import java.security.GeneralSecurityException; + +import javax.net.ssl.*; + +/** + * An SSL socket factory for testing that tracks whether it's being used. + * <p> + * + * An instance of this factory can be set as the value of the + * <code>mail.<protocol>.ssl.socketFactory</code> property. + * + * @since JavaMail 1.5.3 + * @author Stephan Sann + * @author Bill Shannon + */ +public class TestSSLSocketFactory extends SSLSocketFactory { + + /** Holds a SSLSocketFactory to pass all API-method-calls to */ + private SSLSocketFactory defaultFactory = null; + + /** Was a socket created? */ + private boolean socketCreated; + + /** Was a socket wrapped? */ + private boolean socketWrapped; + + private String[] suites; + + /** + * Initializes a new TestSSLSocketFactory. + * + * @throws GeneralSecurityException for security errors + */ + public TestSSLSocketFactory() throws GeneralSecurityException { + this("TLS"); + } + + /** + * Initializes a new TestSSLSocketFactory with a given protocol. + * Normally the protocol will be specified as "TLS". + * + * @param protocol The protocol to use + * @throws NoSuchAlgorithmException if given protocol is not supported + * @throws GeneralSecurityException for security errors + */ + public TestSSLSocketFactory(String protocol) + throws GeneralSecurityException { + + // Get the default SSLSocketFactory to delegate all API-calls to. + defaultFactory = (SSLSocketFactory)SSLSocketFactory.getDefault(); + } + + /** + * Was a socket created using one of the createSocket methods? + */ + public boolean getSocketCreated() { + return socketCreated; + } + + /** + * Was a socket wrapped using the createSocket method that takes a Socket? + */ + public boolean getSocketWrapped() { + return socketWrapped; + } + + /** + * Set the default cipher suites to be applied to future sockets. + */ + public void setDefaultCipherSuites(String[] suites) { + this.suites = suites; + } + + /** + * Configure the socket to be returned. + */ + private Socket configure(Socket socket) { + if (socket instanceof SSLSocket) { // XXX - always true + SSLSocket s = (SSLSocket)socket; + if (suites != null) + s.setEnabledCipherSuites(suites); + } + return socket; + } + + + // SocketFactory methods + + /* (non-Javadoc) + * @see javax.net.ssl.SSLSocketFactory#createSocket(java.net.Socket, + * java.lang.String, int, boolean) + */ + @Override + public synchronized Socket createSocket(Socket socket, String s, int i, + boolean flag) throws IOException { + Socket wrappedSocket = defaultFactory.createSocket(socket, s, i, flag); + socketWrapped = true; + return configure(wrappedSocket); + } + + /* (non-Javadoc) + * @see javax.net.ssl.SSLSocketFactory#getDefaultCipherSuites() + */ + @Override + public synchronized String[] getDefaultCipherSuites() { + if (suites != null) + return suites.clone(); + else + return defaultFactory.getDefaultCipherSuites(); + } + + /* (non-Javadoc) + * @see javax.net.ssl.SSLSocketFactory#getSupportedCipherSuites() + */ + @Override + public synchronized String[] getSupportedCipherSuites() { + return defaultFactory.getSupportedCipherSuites(); + } + + /* (non-Javadoc) + * @see javax.net.SocketFactory#createSocket() + */ + @Override + public synchronized Socket createSocket() throws IOException { + Socket socket = defaultFactory.createSocket(); + socketCreated = true; + return configure(socket); + } + + /* (non-Javadoc) + * @see javax.net.SocketFactory#createSocket(java.net.InetAddress, int, + * java.net.InetAddress, int) + */ + @Override + public synchronized Socket createSocket(InetAddress inetaddress, int i, + InetAddress inetaddress1, int j) throws IOException { + Socket socket = + defaultFactory.createSocket(inetaddress, i, inetaddress1, j); + socketCreated = true; + return configure(socket); + } + + /* (non-Javadoc) + * @see javax.net.SocketFactory#createSocket(java.net.InetAddress, int) + */ + @Override + public synchronized Socket createSocket(InetAddress inetaddress, int i) + throws IOException { + Socket socket = defaultFactory.createSocket(inetaddress, i); + socketCreated = true; + return configure(socket); + } + + /* (non-Javadoc) + * @see javax.net.SocketFactory#createSocket(java.lang.String, int, + * java.net.InetAddress, int) + */ + @Override + public synchronized Socket createSocket(String s, int i, + InetAddress inetaddress, int j) + throws IOException, UnknownHostException { + Socket socket = defaultFactory.createSocket(s, i, inetaddress, j); + socketCreated = true; + return configure(socket); + } + + /* (non-Javadoc) + * @see javax.net.SocketFactory#createSocket(java.lang.String, int) + */ + @Override + public synchronized Socket createSocket(String s, int i) + throws IOException, UnknownHostException { + Socket socket = defaultFactory.createSocket(s, i); + socketCreated = true; + return configure(socket); + } +}
diff --git a/mail/src/test/java/com/sun/mail/test/TestServer.java b/mail/src/test/java/com/sun/mail/test/TestServer.java new file mode 100644 index 0000000..bf60350 --- /dev/null +++ b/mail/src/test/java/com/sun/mail/test/TestServer.java
@@ -0,0 +1,231 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.test; + +import java.util.List; +import java.util.ArrayList; +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.InetSocketAddress; +import javax.net.ssl.*; + +/** + * A simple server for testing. + * + * Inspired by, and derived from, POP3Server by sbo. + * + * @author sbo + * @author Bill Shannon + */ +public final class TestServer extends Thread { + + /** Server socket. */ + private ServerSocket serverSocket; + + /** Keep on? */ + private volatile boolean keepOn; + + /** Protocol handler. */ + private final ProtocolHandler handler; + + private List<Thread> clients = new ArrayList<Thread>(); + + /** + * Test server. + * + * @param handler the protocol handler + */ + public TestServer(final ProtocolHandler handler) throws IOException { + this(handler, false); + } + + /** + * Test server. + * + * @param handler the protocol handler + * @param isSSL create SSL sockets? + */ + public TestServer(final ProtocolHandler handler, final boolean isSSL) + throws IOException { + this.handler = handler; + + /* + * Allowing the JDK to pick a port number sometimes results in it + * picking a number that's already in use by another process, but + * no error is returned. Picking it ourself allows us to make sure + * that it's not used before we pick it. Hopefully the socket + * creation will fail if the port is already in use. + * + * XXX - perhaps we should use Random to choose a port number in + * the emphemeral range, in case a lot of low port numbers are + * already in use. + */ + for (int port = 49152; port < 50000 /*65535*/; port++) { + /* + if (isListening(port)) + continue; + */ + try { + serverSocket = createServerSocket(port, isSSL); + return; + } catch (IOException ex) { + // ignore + } + } + throw new RuntimeException("Can't find unused port"); + } + + private static ServerSocket createServerSocket(int port, boolean isSSL) + throws IOException { + ServerSocket ss; + if (isSSL) { + SSLServerSocketFactory sf = + (SSLServerSocketFactory)SSLServerSocketFactory.getDefault(); + ss = sf.createServerSocket(port); + // enable only the anonymous cipher suites so we don't have to + // create a server certificate + List<String> anon = new ArrayList<>(); + String[] suites = sf.getSupportedCipherSuites(); + for (int i = 0; i < suites.length; i++) { + if (suites[i].indexOf("_anon_") >= 0) { + anon.add(suites[i]); + } + } + ((SSLServerSocket)ss).setEnabledCipherSuites( + anon.toArray(new String[anon.size()])); + } else + ss = new ServerSocket(port); + return ss; + } + + /** + * Return the port the server is listening on. + */ + public int getPort() { + return serverSocket.getLocalPort(); + } + + /** + * Exit server. + */ + public void quit() { + try { + keepOn = false; + if (serverSocket != null && !serverSocket.isClosed()) { + serverSocket.close(); + serverSocket = null; + } + } catch (final IOException e) { + throw new RuntimeException(e); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void start() { + super.start(); + // don't return until server is really listening + // XXX - this might not be necessary + for (int tries = 0; tries < 10; tries++) { + if (isListening(getPort())) { + return; + } + try { + Thread.sleep(100); + } catch (InterruptedException ex) { } + } + throw new RuntimeException("Server isn't listening"); + } + + /** + * {@inheritDoc} + */ + @Override + public void run() { + try { + keepOn = true; + + while (keepOn) { + try { + final Socket clientSocket = serverSocket.accept(); + final ProtocolHandler pHandler = + (ProtocolHandler)handler.clone(); + pHandler.setClientSocket(clientSocket); + Thread t = new Thread(pHandler); + synchronized (clients) { + clients.add(t); + } + t.start(); + } catch (final IOException e) { + //e.printStackTrace(); + } catch (NullPointerException nex) { + // serverSocket can be set to null before we could check + } + } + } finally { + quit(); + } + } + + /** + * Return number of clients ever created. + */ + public int clientCount() { + synchronized (clients) { + // isListening creates a client that we don't count + return clients.size() - 1; + } + } + + /** + * Wait for at least n clients to terminate. + */ + public void waitForClients(int n) { + if (n > clientCount()) + throw new RuntimeException("not that many clients"); + for (;;) { + int num = -1; // ignore isListening client + synchronized (clients) { + for (Thread t : clients) { + if (!t.isAlive()) { + if (++num >= n) + return; + } + } + } + try { + Thread.sleep(100); + } catch (InterruptedException ex) { } + } + } + + private boolean isListening(int port) { + try { + Socket s = new Socket(); + s.connect(new InetSocketAddress("localhost", port), 100); + // it's listening! + s.close(); + return true; + } catch (Exception ex) { + //System.out.println(ex); + } + return false; + } +}
diff --git a/mail/src/test/java/com/sun/mail/test/TestSocketFactory.java b/mail/src/test/java/com/sun/mail/test/TestSocketFactory.java new file mode 100644 index 0000000..a0e751e --- /dev/null +++ b/mail/src/test/java/com/sun/mail/test/TestSocketFactory.java
@@ -0,0 +1,118 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.test; + +import java.io.IOException; +import java.net.*; + +import javax.net.SocketFactory; + +/** + * A socket factory for testing that tracks whether it's being used. + * <p> + * + * An instance of this factory can be set as the value of the + * <code>mail.<protocol>.socketFactory</code> property. + * + * @since JavaMail 1.5.3 + * @author Stephan Sann + * @author Bill Shannon + */ +public class TestSocketFactory extends SocketFactory { + + /** Holds a SocketFactory to pass all API-method-calls to */ + private SocketFactory defaultFactory = null; + + /** Was a socket created? */ + private boolean socketCreated; + + /** + * Initializes a new TestSocketFactory. + */ + public TestSocketFactory() { + // Get the default SocketFactory to delegate all API-calls to. + defaultFactory = SocketFactory.getDefault(); + } + + /** + * Was a socket created using one of the createSocket methods? + */ + public boolean getSocketCreated() { + return socketCreated; + } + + + // SocketFactory methods + + /* (non-Javadoc) + * @see javax.net.SocketFactory#createSocket() + */ + @Override + public synchronized Socket createSocket() throws IOException { + Socket socket = defaultFactory.createSocket(); + socketCreated = true; + return socket; + } + + /* (non-Javadoc) + * @see javax.net.SocketFactory#createSocket(java.net.InetAddress, int, + * java.net.InetAddress, int) + */ + @Override + public synchronized Socket createSocket(InetAddress inetaddress, int i, + InetAddress inetaddress1, int j) throws IOException { + Socket socket = + defaultFactory.createSocket(inetaddress, i, inetaddress1, j); + socketCreated = true; + return socket; + } + + /* (non-Javadoc) + * @see javax.net.SocketFactory#createSocket(java.net.InetAddress, int) + */ + @Override + public synchronized Socket createSocket(InetAddress inetaddress, int i) + throws IOException { + Socket socket = defaultFactory.createSocket(inetaddress, i); + socketCreated = true; + return socket; + } + + /* (non-Javadoc) + * @see javax.net.SocketFactory#createSocket(java.lang.String, int, + * java.net.InetAddress, int) + */ + @Override + public synchronized Socket createSocket(String s, int i, + InetAddress inetaddress, int j) + throws IOException, UnknownHostException { + Socket socket = defaultFactory.createSocket(s, i, inetaddress, j); + socketCreated = true; + return socket; + } + + /* (non-Javadoc) + * @see javax.net.SocketFactory#createSocket(java.lang.String, int) + */ + @Override + public synchronized Socket createSocket(String s, int i) + throws IOException, UnknownHostException { + Socket socket = defaultFactory.createSocket(s, i); + socketCreated = true; + return socket; + } +}
diff --git a/mail/src/test/java/com/sun/mail/util/BASE64Test.java b/mail/src/test/java/com/sun/mail/util/BASE64Test.java new file mode 100644 index 0000000..6a558b2 --- /dev/null +++ b/mail/src/test/java/com/sun/mail/util/BASE64Test.java
@@ -0,0 +1,303 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.*; +import java.util.*; +import javax.mail.*; + +import org.junit.Test; +import org.junit.Assert; + +/** + * Test base64 encoding/decoding. + * + * @author Bill Shannon + */ + +public class BASE64Test { + + @Test + public void test() throws IOException { + // test a range of buffer sizes + for (int bufsize = 1; bufsize < 100; bufsize++) { + //System.out.println("Buffer size: " + bufsize); + byte[] buf = new byte[bufsize]; + + // test a set of patterns + + // first, all zeroes + Arrays.fill(buf, (byte)0); + test("Zeroes", buf); + + // now, all ones + Arrays.fill(buf, (byte)0xff); + test("Ones", buf); + + // now, small integers + for (int i = 0; i < bufsize; i++) + buf[i] = (byte)i; + test("Ints", buf); + + // finally, random numbers + Random rnd = new Random(); + rnd.nextBytes(buf); + test("Random", buf); + } + } + + /** + * Encode and decode the buffer and check that we get back the + * same data. Encoding is done both with the static encode + * method and using the encoding stream. Likewise, decoding + * is done both with the static decode method and using the + * decoding stream. Check all combinations. + */ + private static void test(String name, byte[] buf) throws IOException { + // first encode and decode with method + byte[] encoded = BASE64EncoderStream.encode(buf); + byte[] nbuf = BASE64DecoderStream.decode(encoded); + compare(name, "method", buf, nbuf); + + // encode with stream, compare with method encoded version + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + BASE64EncoderStream os = + new BASE64EncoderStream(bos, Integer.MAX_VALUE); + os.write(buf); + os.flush(); + os.close(); + byte[] sbuf = bos.toByteArray(); + compare(name, "encoded", encoded, sbuf); + + // encode with stream, decode with method + nbuf = BASE64DecoderStream.decode(sbuf); + compare(name, "stream->method", buf, nbuf); + + // encode with stream, decode with stream + ByteArrayInputStream bin = new ByteArrayInputStream(sbuf); + BASE64DecoderStream in = new BASE64DecoderStream(bin); + readAll(in, nbuf, nbuf.length); + compare(name, "stream", buf, nbuf); + + // encode with method, decode with stream + for (int i = 1; i <= nbuf.length; i++) { + bin = new ByteArrayInputStream(encoded); + in = new BASE64DecoderStream(bin); + readAll(in, nbuf, i); + compare(name, "method->stream " + i, buf, nbuf); + } + + // encode with stream, decode with stream, many buffers + + // first, fill the output with multiple buffers, up to the limit + int limit = 10000; // more than 8K + bos = new ByteArrayOutputStream(); + os = new BASE64EncoderStream(bos); + for (int size = 0, blen = buf.length; size < limit; size += blen) { + if (size + blen > limit) { + blen = limit - size; + // write out partial buffer, starting at non-zero offset + os.write(buf, buf.length - blen, blen); + } else + os.write(buf); + } + os.flush(); + os.close(); + + // read the encoded output and check the line length + String type = "big stream"; // for error messages below + sbuf = bos.toByteArray(); + bin = new ByteArrayInputStream(sbuf); + byte[] inbuf = new byte[78]; + for (int size = 0, blen = 76; size < limit; size += blen) { + if (size + blen > limit) { + blen = limit - size; + int n = bin.read(inbuf, 0, blen); + Assert.assertEquals(name + ": " + type + + " read wrong size at offset " + (size + blen), blen, n); + } else { + int n = bin.read(inbuf, 0, blen + 2); + Assert.assertEquals(name + ": " + type + + " read wrong size at offset " + (size + blen), blen + 2, n); + Assert.assertTrue(name + ": " + type + + " no CRLF: at offset " + (size + blen), + inbuf[blen] == (byte)'\r' && inbuf[blen+1] == (byte)'\n'); + } + } + + // decode the output and check the data + bin = new ByteArrayInputStream(sbuf); + in = new BASE64DecoderStream(bin); + inbuf = new byte[buf.length]; + for (int size = 0, blen = buf.length; size < limit; size += blen) { + if (size + blen > limit) + blen = limit - size; + int n = in.read(nbuf, 0, blen); + Assert.assertEquals(name + ": " + type + + " read decoded wrong size at offset " + (size + blen), blen, n); + if (blen != buf.length) { + // have to compare with end of original buffer + byte[] cbuf = new byte[blen]; + System.arraycopy(buf, buf.length - blen, cbuf, 0, blen); + // need a version of the read buffer that's the right size + byte[] cnbuf = new byte[blen]; + System.arraycopy(nbuf, 0, cnbuf, 0, blen); + compare(name, type, cbuf, cnbuf); + } else { + compare(name, type, buf, nbuf); + } + } + } + + private static byte[] origLine; + private static byte[] encodedLine; + static { + try { + origLine = + "000000000000000000000000000000000000000000000000000000000". + getBytes("us-ascii"); + encodedLine = + ("MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw" + + "MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw" + "\r\n"). + getBytes("us-ascii"); + } catch (UnsupportedEncodingException uex) { + // should never happen; + } + } + + /** + * Test that CRLF is inserted at the right place. + * Test combinations of array writes of different sizes + * and single byte writes. + */ + @Test + public void testLineLength() throws Exception { + for (int i = 0; i < origLine.length; i++) { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + + OutputStream os = new BASE64EncoderStream(bos); + os.write(origLine, 0, i); + os.write(origLine, i, origLine.length - i); + os.write((byte)'0'); + os.flush(); + os.close(); + + byte[] line = new byte[encodedLine.length]; + System.arraycopy(bos.toByteArray(), 0, line, 0, line.length); + Assert.assertArrayEquals("encoded line " + i, encodedLine, line); + } + + for (int i = 0; i < origLine.length; i++) { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + + OutputStream os = new BASE64EncoderStream(bos); + os.write(origLine, 0, i); + os.write(origLine, i, origLine.length - i); + os.write(origLine); + os.flush(); + os.close(); + + byte[] line = new byte[encodedLine.length]; + System.arraycopy(bos.toByteArray(), 0, line, 0, line.length); + Assert.assertArrayEquals("all arrays, encoded line " + i, + encodedLine, line); + } + + for (int i = 1; i < 5; i++) { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + + OutputStream os = new BASE64EncoderStream(bos); + for (int j = 0; j < i; j++) + os.write((byte)'0'); + os.write(origLine, i, origLine.length - i); + os.write((byte)'0'); + os.flush(); + os.close(); + + byte[] line = new byte[encodedLine.length]; + System.arraycopy(bos.toByteArray(), 0, line, 0, line.length); + Assert.assertArrayEquals("single byte first encoded line " + i, + encodedLine, line); + } + for (int i = origLine.length - 5; i < origLine.length; i++) { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + + OutputStream os = new BASE64EncoderStream(bos); + os.write(origLine, 0, i); + for (int j = 0; j < origLine.length - i; j++) + os.write((byte)'0'); + os.write((byte)'0'); + os.flush(); + os.close(); + + byte[] line = new byte[encodedLine.length]; + System.arraycopy(bos.toByteArray(), 0, line, 0, line.length); + Assert.assertArrayEquals("single byte last encoded line " + i, + encodedLine, line); + } + } + + /** + * Fill the buffer from the stream. + */ + private static void readAll(InputStream in, byte[] buf, int readsize) + throws IOException { + int need = buf.length; + int off = 0; + int got; + while (need > 0) { + got = in.read(buf, off, need > readsize ? readsize : need); + if (got <= 0) + break; + off += got; + need -= got; + } + if (need != 0) + System.out.println("couldn't read all bytes"); + } + + /** + * Compare the two buffers. + */ + private static void compare(String name, String type, + byte[] buf, byte[] nbuf) { + /* + if (nbuf.length != buf.length) { + System.out.println(name + ": " + type + + " decoded array size wrong: " + + "got " + nbuf.length + ", expected " + buf.length); + dump(name + " buf", buf); + dump(name + " nbuf", nbuf); + } + */ + Assert.assertEquals(name + ": " + type + " decoded array size wrong", + buf.length, nbuf.length); + for (int i = 0; i < buf.length; i++) { + Assert.assertEquals(name + ": " + type + " data wrong: index " + i, + buf[i], nbuf[i]); + } + } + + /** + * Dump the contents of the buffer. + */ + private static void dump(String name, byte[] buf) { + System.out.println(name); + for (int i = 0; i < buf.length; i++) + System.out.println(buf[i]); + } +}
diff --git a/mail/src/test/java/com/sun/mail/util/ContentTypeCleaner.java b/mail/src/test/java/com/sun/mail/util/ContentTypeCleaner.java new file mode 100644 index 0000000..98b4581 --- /dev/null +++ b/mail/src/test/java/com/sun/mail/util/ContentTypeCleaner.java
@@ -0,0 +1,108 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.ByteArrayInputStream; +import java.nio.charset.Charset; +import java.util.Properties; + +import javax.mail.Session; +import javax.mail.MessagingException; +import javax.mail.BodyPart; +import com.sun.mail.test.AsciiStringInputStream; +import javax.mail.internet.MimePart; +import javax.mail.internet.MimeMessage; +import javax.mail.internet.MimeMultipart; + +import org.junit.*; +import static org.junit.Assert.assertEquals; + +/** + * Test the "mail.mime.contenttypehandler" property. + */ +public class ContentTypeCleaner { + + private static Session s = Session.getInstance(new Properties()); + + @BeforeClass + public static void before() { + System.out.println("ContentTypeCleaner"); + System.setProperty("mail.mime.contenttypehandler", + ContentTypeCleaner.class.getName()); + } + + @Test + public void testGarbage() throws Exception { + MimeMessage m = createMessage(); + MimeMultipart mp = (MimeMultipart)m.getContent(); + BodyPart bp = mp.getBodyPart(0); + assertEquals("text/plain", bp.getContentType()); + assertEquals("first part\n", bp.getContent()); + } + + @Test + public void testValid() throws Exception { + MimeMessage m = createMessage(); + MimeMultipart mp = (MimeMultipart)m.getContent(); + BodyPart bp = mp.getBodyPart(1); + assertEquals("text/plain; charset=iso-8859-1", bp.getContentType()); + assertEquals("second part\n", bp.getContent()); + } + + @Test + public void testEmpty() throws Exception { + MimeMessage m = createMessage(); + MimeMultipart mp = (MimeMultipart)m.getContent(); + BodyPart bp = mp.getBodyPart(2); + assertEquals("text/plain", bp.getContentType()); + assertEquals("third part\n", bp.getContent()); + } + + public static String cleanContentType(MimePart mp, String contentType) { + if (contentType == null) + return null; + if (contentType.equals("complete garbage")) + return "text/plain"; + return contentType; + } + + private static MimeMessage createMessage() throws MessagingException { + String content = + "Mime-Version: 1.0\n" + + "Subject: Example\n" + + "Content-Type: multipart/mixed; boundary=\"-\"\n" + + "\n" + + "preamble\n" + + "---\n" + + "Content-Type: complete garbage\n" + + "\n" + + "first part\n" + + "\n" + + "---\n" + + "Content-Type: text/plain; charset=iso-8859-1\n" + + "\n" + + "second part\n" + + "\n" + + "---\n" + + "\n" + + "third part\n" + + "\n" + + "-----\n"; + + return new MimeMessage(s, new AsciiStringInputStream(content)); + } +}
diff --git a/mail/src/test/java/com/sun/mail/util/LineInputStreamTest.java b/mail/src/test/java/com/sun/mail/util/LineInputStreamTest.java new file mode 100644 index 0000000..2255f0a --- /dev/null +++ b/mail/src/test/java/com/sun/mail/util/LineInputStreamTest.java
@@ -0,0 +1,99 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.*; +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * Test handling of line terminators. + * LineInputStream handles these different line terminators: + * + * NL - Unix + * CR LF - Windows, MIME + * CR - old MacOS + * CR CR LF - broken internet servers + * + * @author Bill Shannon + */ + +public class LineInputStreamTest { + private static final String[] lines = { + "line1\nline2\nline3\n", + "line1\r\nline2\r\nline3\r\n", + "line1\rline2\rline3\r", + "line1\r\r\nline2\r\r\nline3\r\r\n" + }; + + private static final String[] empty = { + "\n\n\n", + "\r\n\r\n\r\n", + "\r\r\r", + "\r\r\n\r\r\n\r\r\n" + }; + + private static final String[] mixed = { + "line1\n\nline3\n", + "line1\r\n\r\nline3\r\n", + "line1\r\rline3\r", + "line1\r\r\n\r\r\nline3\r\r\n" + }; + + @Test + public void testLines() throws IOException { + for (String s : lines) { + LineInputStream is = createStream(s); + assertEquals("line1", is.readLine()); + assertEquals("line2", is.readLine()); + assertEquals("line3", is.readLine()); + assertEquals(null, is.readLine()); + } + } + + @Test + public void testEmpty() throws IOException { + for (String s : empty) { + LineInputStream is = createStream(s); + assertEquals("", is.readLine()); + assertEquals("", is.readLine()); + assertEquals("", is.readLine()); + assertEquals(null, is.readLine()); + } + } + + @Test + public void testMixed() throws IOException { + for (String s : mixed) { + LineInputStream is = createStream(s); + assertEquals("line1", is.readLine()); + assertEquals("", is.readLine()); + assertEquals("line3", is.readLine()); + assertEquals(null, is.readLine()); + } + } + + private LineInputStream createStream(String s) { + try { + return new LineInputStream( + new ByteArrayInputStream(s.getBytes("us-ascii"))); + } catch (UnsupportedEncodingException ex) { + return null; // should never happen + } + } +}
diff --git a/mail/src/test/java/com/sun/mail/util/MimeUtilTestSuite.java b/mail/src/test/java/com/sun/mail/util/MimeUtilTestSuite.java new file mode 100644 index 0000000..06131fe --- /dev/null +++ b/mail/src/test/java/com/sun/mail/util/MimeUtilTestSuite.java
@@ -0,0 +1,34 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import org.junit.runner.RunWith; +import org.junit.runners.Suite.SuiteClasses; + +import com.sun.mail.test.ClassLoaderSuite; +import com.sun.mail.test.ClassLoaderSuite.TestClass; + +/** + * Suite of MimeUtil tests that need to be run in a separate class loader. + */ +@RunWith(ClassLoaderSuite.class) +@TestClass(MimeUtil.class) +@SuiteClasses( { + ContentTypeCleaner.class +}) +public class MimeUtilTestSuite { +}
diff --git a/mail/src/test/java/com/sun/mail/util/PropUtilTest.java b/mail/src/test/java/com/sun/mail/util/PropUtilTest.java new file mode 100644 index 0000000..3d6695e --- /dev/null +++ b/mail/src/test/java/com/sun/mail/util/PropUtilTest.java
@@ -0,0 +1,181 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.util.Properties; +import javax.mail.Session; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import org.junit.Test; + +/** + * Test that the PropUtil methods return the correct values, + * especially when defaults and non-String values are considered. + */ +public class PropUtilTest { + @Test + public void testInt() throws Exception { + Properties props = new Properties(); + props.setProperty("test", "2"); + assertEquals(PropUtil.getIntProperty(props, "test", 1), 2); + } + + @Test + public void testIntDef() throws Exception { + Properties props = new Properties(); + assertEquals(PropUtil.getIntProperty(props, "test", 1), 1); + } + + @Test + public void testIntDefProp() throws Exception { + Properties defprops = new Properties(); + defprops.setProperty("test", "2"); + Properties props = new Properties(defprops); + assertEquals(PropUtil.getIntProperty(props, "test", 1), 2); + } + + @Test + public void testInteger() throws Exception { + Properties props = new Properties(); + props.put("test", 2); + assertEquals(PropUtil.getIntProperty(props, "test", 1), 2); + } + + @Test + public void testBool() throws Exception { + Properties props = new Properties(); + props.setProperty("test", "true"); + assertTrue(PropUtil.getBooleanProperty(props, "test", false)); + } + + @Test + public void testBoolDef() throws Exception { + Properties props = new Properties(); + assertTrue(PropUtil.getBooleanProperty(props, "test", true)); + } + + @Test + public void testBoolDefProp() throws Exception { + Properties defprops = new Properties(); + defprops.setProperty("test", "true"); + Properties props = new Properties(defprops); + assertTrue(PropUtil.getBooleanProperty(props, "test", false)); + } + + @Test + public void testBoolean() throws Exception { + Properties props = new Properties(); + props.put("test", true); + assertTrue(PropUtil.getBooleanProperty(props, "test", false)); + } + + + // the Session variants... + + @Test + @SuppressWarnings("deprecation") + public void testSessionInt() throws Exception { + Properties props = new Properties(); + props.setProperty("test", "2"); + Session sess = Session.getInstance(props, null); + assertEquals(PropUtil.getIntSessionProperty(sess, "test", 1), 2); + } + + @Test + @SuppressWarnings("deprecation") + public void testSessionIntDef() throws Exception { + Properties props = new Properties(); + Session sess = Session.getInstance(props, null); + assertEquals(PropUtil.getIntSessionProperty(sess, "test", 1), 1); + } + + @Test + @SuppressWarnings("deprecation") + public void testSessionIntDefProp() throws Exception { + Properties defprops = new Properties(); + defprops.setProperty("test", "2"); + Properties props = new Properties(defprops); + Session sess = Session.getInstance(props, null); + assertEquals(PropUtil.getIntSessionProperty(sess, "test", 1), 2); + } + + @Test + @SuppressWarnings("deprecation") + public void testSessionInteger() throws Exception { + Properties props = new Properties(); + props.put("test", 2); + Session sess = Session.getInstance(props, null); + assertEquals(PropUtil.getIntSessionProperty(sess, "test", 1), 2); + } + + @Test + @SuppressWarnings("deprecation") + public void testSessionBool() throws Exception { + Properties props = new Properties(); + props.setProperty("test", "true"); + Session sess = Session.getInstance(props, null); + assertTrue(PropUtil.getBooleanSessionProperty(sess, "test", false)); + } + + @Test + @SuppressWarnings("deprecation") + public void testSessionBoolDef() throws Exception { + Properties props = new Properties(); + Session sess = Session.getInstance(props, null); + assertTrue(PropUtil.getBooleanSessionProperty(sess, "test", true)); + } + + @Test + @SuppressWarnings("deprecation") + public void testSessionBoolDefProp() throws Exception { + Properties defprops = new Properties(); + defprops.setProperty("test", "true"); + Properties props = new Properties(defprops); + Session sess = Session.getInstance(props, null); + assertTrue(PropUtil.getBooleanSessionProperty(sess, "test", false)); + } + + @Test + @SuppressWarnings("deprecation") + public void testSessionBoolean() throws Exception { + Properties props = new Properties(); + props.put("test", true); + Session sess = Session.getInstance(props, null); + assertTrue(PropUtil.getBooleanSessionProperty(sess, "test", false)); + } + + + // the System variants... + + @Test + public void testSystemBool() throws Exception { + System.setProperty("test", "true"); + assertTrue(PropUtil.getBooleanSystemProperty("test", false)); + } + + @Test + public void testSystemBoolDef() throws Exception { + assertTrue(PropUtil.getBooleanSystemProperty("testnotset", true)); + } + + @Test + public void testSystemBoolean() throws Exception { + System.getProperties().put("testboolean", true); + assertTrue(PropUtil.getBooleanSystemProperty("testboolean", false)); + } +}
diff --git a/mail/src/test/java/com/sun/mail/util/QPEncoderStreamTest.java b/mail/src/test/java/com/sun/mail/util/QPEncoderStreamTest.java new file mode 100644 index 0000000..245a768 --- /dev/null +++ b/mail/src/test/java/com/sun/mail/util/QPEncoderStreamTest.java
@@ -0,0 +1,44 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.ByteArrayOutputStream; +import com.sun.mail.util.QPEncoderStream; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; + +/** + * Test quoted-printable encoder. + * + * @author Bill Shannon + */ + +public class QPEncoderStreamTest { + /** + * Test that a trailing space is encoded in the output stream. + */ + @Test + public void testTrailingSpace() throws Exception { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + QPEncoderStream qs = new QPEncoderStream(bos); + qs.write("test ".getBytes("us-ascii")); + qs.flush(); + String result = new String(bos.toByteArray(), "us-ascii"); + assertEquals("test=20", result); + } +}
diff --git a/mail/src/test/java/com/sun/mail/util/SocketFetcherTest.java b/mail/src/test/java/com/sun/mail/util/SocketFetcherTest.java new file mode 100644 index 0000000..991d525 --- /dev/null +++ b/mail/src/test/java/com/sun/mail/util/SocketFetcherTest.java
@@ -0,0 +1,206 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.IOException; +import java.net.Socket; +import java.util.Properties; +import java.nio.charset.StandardCharsets; + +import com.sun.mail.test.TestServer; +import com.sun.mail.test.ProtocolHandler; + +import org.junit.Test; +import org.junit.Rule; +import org.junit.rules.Timeout; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.fail; + +/** + * Test SocketFetcher. + */ +public final class SocketFetcherTest { + + // timeout the test in case of deadlock + @Rule + public Timeout deadlockTimeout = Timeout.seconds(20); + + /** + * Test connecting with proxy host and port. + */ + @Test + public void testProxyHostPort() { + assertTrue("proxy host, port", testProxy("proxy", "localhost", "PPPP")); + } + + /** + * Test connecting with proxy host and port and user name and password. + */ + @Test + public void testProxyHostPortUserPassword() { + assertTrue("proxy host, port, user, password", + testProxyUserPassword("proxy", "localhost", "PPPP", "user", "pwd")); + } + + /** + * Test connecting with proxy host:port. + */ + @Test + public void testProxyHostColonPort() { + assertTrue("proxy host:port", testProxy("proxy", "localhost:PPPP", null)); + } + + /** + * Test connecting with socks host and port. + */ + @Test + public void testSocksHostPort() { + assertTrue("socks host, port", testProxy("socks", "localhost", "PPPP")); + } + + /** + * Test connecting with socks host:port. + */ + @Test + public void testSocksHostColonPort() { + assertTrue("socks host:port", testProxy("socks", "localhost:PPPP", null)); + } + + /** + * Test connecting with no proxy. + */ + @Test + public void testNoProxy() { + assertFalse("no proxy", testProxy("none", "localhost", null)); + } + + /** + */ + public boolean testProxy(String type, String host, String port) { + return testProxyUserPassword(type, host, port, null, null); + } + + /** + */ + public boolean testProxyUserPassword(String type, String host, String port, + String user, String pwd) { + TestServer server = null; + try { + ProxyHandler handler = new ProxyHandler(type.equals("proxy")); + server = new TestServer(handler); + server.start(); + String sport = "" + server.getPort(); + + //System.setProperty("mail.socket.debug", "true"); + Properties properties = new Properties(); + properties.setProperty("mail.test.host", "localhost"); + properties.setProperty("mail.test.port", "2"); + properties.setProperty("mail.test." + type + ".host", + host.replace("PPPP", sport)); + if (port != null) + properties.setProperty("mail.test." + type + ".port", + port.replace("PPPP", sport)); + if (user != null) + properties.setProperty("mail.test." + type + ".user", user); + if (pwd != null) + properties.setProperty("mail.test." + type + ".password", pwd); + + Socket s = null; + try { + s = SocketFetcher.getSocket("localhost", 2, + properties, "mail.test", false); + } catch (Exception ex) { + // ignore failure, which is expected + //System.out.println(ex); + //ex.printStackTrace(); + } finally { + if (s != null) + s.close(); + } + if (!handler.getConnected()) + return false; + if (user != null && pwd != null) + return (user + ":" + pwd).equals(handler.getUserPassword()); + else + return true; + + } catch (final Exception e) { + //e.printStackTrace(); + fail(e.getMessage()); + return false; // XXX - doesn't matter + } finally { + if (server != null) { + server.quit(); + } + } + } + + /** + * Custom handler. Remember whether any data was sent + * and save user/password string; + */ + private static class ProxyHandler extends ProtocolHandler { + private boolean http; + + // must be static because handler is cloned for each connection + private static volatile boolean connected; + private static volatile String userPassword; + + public ProxyHandler(boolean http) { + this.http = http; + connected = false; + } + + @Override + public void handleCommand() throws IOException { + if (!http) { + int c = in.read(); + if (c >= 0) { + // any data means a real client connected + connected = true; + } + exit(); + } + + // else, http... + String line; + while ((line = readLine()) != null) { + // any data means a real client connected + connected = true; + if (line.length() == 0) + break; + if (line.startsWith("Proxy-Authorization:")) { + int i = line.indexOf("Basic ") + 6; + String up = line.substring(i); + userPassword = new String(BASE64DecoderStream.decode( + up.getBytes(StandardCharsets.US_ASCII)), + StandardCharsets.UTF_8); + } + } + exit(); + } + + public boolean getConnected() { + return connected; + } + + public String getUserPassword() { + return userPassword; + } + } +}
diff --git a/mail/src/test/java/com/sun/mail/util/UUDecoderStreamTest.java b/mail/src/test/java/com/sun/mail/util/UUDecoderStreamTest.java new file mode 100644 index 0000000..10c7942 --- /dev/null +++ b/mail/src/test/java/com/sun/mail/util/UUDecoderStreamTest.java
@@ -0,0 +1,271 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.*; +import java.util.*; +import com.sun.mail.util.UUDecoderStream; + +import org.junit.Test; +import org.junit.Assert; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +/** + * Test uudecoder. + * + * @author Bill Shannon + */ + +@RunWith(Parameterized.class) +public class UUDecoderStreamTest { + private TestData data; + + private static boolean gen_test_input = false; // output good + private static int errors = 0; // number of errors detected + + private static boolean junit; + + static class TestData { + public String name; + public boolean ignoreErrors; + public boolean ignoreMissingBeginEnd; + public byte[] input; + public byte[] expectedOutput; + public String expectedException; + } + + public UUDecoderStreamTest(TestData t) { + data = t; + } + + @Test + public void testData() { + test(data); + } + + @Parameters + public static Collection<TestData[]> data() throws Exception { + junit = true; + // XXX - gratuitous array requirement + List<TestData[]> testData = new ArrayList<>(); + BufferedReader in = new BufferedReader(new InputStreamReader( + UUDecoderStreamTest.class.getResourceAsStream("uudata"))); + TestData t; + while ((t = parse(in)) != null) + testData.add(new TestData[] { t }); + return testData; + } + + public static void main(String argv[]) throws Exception { + int optind; + // XXX - all options currently ignored + for (optind = 0; optind < argv.length; optind++) { + if (argv[optind].equals("-")) { + // ignore + } else if (argv[optind].equals("-g")) { + gen_test_input = true; + } else if (argv[optind].equals("--")) { + optind++; + break; + } else if (argv[optind].startsWith("-")) { + System.out.println( + "Usage: uutest [-g] [-]"); + System.exit(1); + } else { + break; + } + } + + // read from stdin + BufferedReader in = + new BufferedReader(new InputStreamReader(System.in)); + + TestData t; + while ((t = parse(in)) != null) + test(t); + System.exit(errors); + } + + /* + * Parse the input, returning a test case. + */ + public static TestData parse(BufferedReader in) throws Exception { + + String line = null; + for (;;) { + line = in.readLine(); + if (line == null) + return null; + if (line.length() == 0 || line.startsWith("#")) + continue; + + if (!line.startsWith("TEST")) + throw new Exception("Bad test data format"); + break; + } + + TestData t = new TestData(); + int i = line.indexOf(' '); // XXX - crude + t.name = line.substring(i + 1); + + line = in.readLine(); + StringTokenizer st = new StringTokenizer(line); + String tok = st.nextToken(); + if (!tok.equals("DATA")) + throw new Exception("Bad test data format: " + line); + while (st.hasMoreTokens()) { + tok = st.nextToken(); + if (tok.equals("ignoreErrors")) + t.ignoreErrors = true; + else if (tok.equals("ignoreMissingBeginEnd")) + t.ignoreMissingBeginEnd = true; + else + throw new Exception("Bad DATA option in line: " + line); + } + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + Writer os = new OutputStreamWriter(bos, "us-ascii"); + for (;;) { + line = in.readLine(); + if (line.equals("EXPECT")) + break; + os.write(line); + os.write("\n"); + } + os.close(); + t.input = bos.toByteArray(); + + bos = new ByteArrayOutputStream(); + os = new OutputStreamWriter(bos, "us-ascii"); + for (;;) { + line = in.readLine(); + if (line.startsWith("EXCEPTION")) { + i = line.indexOf(' '); // XXX - crude + t.expectedException = line.substring(i + 1); + } else if (line.equals("END")) + break; + os.write(line); + os.write("\n"); + } + os.close(); + if (t.expectedException == null) + t.expectedOutput = bos.toByteArray(); + + return t; + } + + /** + * Test the data in the test case. + */ + public static void test(TestData t) { + InputStream in = + new UUDecoderStream(new ByteArrayInputStream(t.input), + t.ignoreErrors, t.ignoreMissingBeginEnd); + + // two cases - either we're expecting an exception or we're not + if (t.expectedException != null) { + try { + int c; + while ((c = in.read()) >= 0) + ; // throw it away + // read all the data with no exception - fail + if (junit) + Assert.fail("Didn't get expected exception: " + + t.expectedException); + System.out.println("Test: " + t.name); + System.out.println("Got no Exception"); + System.out.println("Expected Exception: " + + t.expectedException); + errors++; + } catch (Exception ex) { + if (junit) + Assert.assertEquals("For expected exception", + ex.getClass().getName(), t.expectedException); + if (!ex.getClass().getName().equals(t.expectedException)) { + System.out.println("Test: " + t.name); + System.out.println("Got Exception: " + ex); + System.out.println("Expected Exception: " + + t.expectedException); + errors++; + } + } finally { + try { + in.close(); + } catch (IOException ioex) { } + } + } else { + InputStream ein = new ByteArrayInputStream(t.expectedOutput); + try { + int c, ec; + boolean gotError = false; + while ((c = in.read()) >= 0) { + ec = ein.read(); + if (junit) + Assert.assertFalse("For expected EOF, got char " + c, + ec < 0); + if (ec < 0) { + System.out.println("Test: " + t.name); + System.out.println("Got char: " + c); + System.out.println("Expected EOF"); + errors++; + gotError = true; + break; + } + if (junit) + Assert.assertEquals("For expected char " + ec + + ", got char " + c, ec, c); + if (c != ec) { + System.out.println("Test: " + t.name); + System.out.println("Got char: " + c); + System.out.println("Expected char: " + ec); + errors++; + gotError = true; + break; + } + } + if (!gotError) { + ec = ein.read(); + if (junit) + Assert.assertFalse("For expected char " + ec + + ", got EOF", ec >= 0); + if (ec >= 0) { + System.out.println("Test: " + t.name); + System.out.println("Got EOF"); + System.out.println("Expected char: " + ec); + errors++; + } + } + } catch (Exception ex) { + if (junit) + Assert.fail("Got exception: " + ex); + System.out.println("Test: " + t.name); + System.out.println("Got Exception: " + ex); + System.out.println("Expected no Exception"); + errors++; + } finally { + try { + in.close(); + } catch (IOException ioex) { } + try { + ein.close(); + } catch (IOException ioex) { } + } + } + } +}
diff --git a/mail/src/test/java/com/sun/mail/util/WriteTimeoutSocketTest.java b/mail/src/test/java/com/sun/mail/util/WriteTimeoutSocketTest.java new file mode 100644 index 0000000..f688f50 --- /dev/null +++ b/mail/src/test/java/com/sun/mail/util/WriteTimeoutSocketTest.java
@@ -0,0 +1,258 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.lang.reflect.*; + +import java.io.IOException; +import java.io.InterruptedIOException; +import java.util.Properties; +import java.util.List; +import java.util.ArrayList; +import java.util.Set; +import java.util.HashSet; + +import javax.mail.*; +import javax.mail.internet.MimeMessage; +import javax.mail.util.ByteArrayDataSource; +import javax.activation.DataHandler; +import javax.net.ssl.*; + +import com.sun.mail.imap.IMAPHandler; +import com.sun.mail.test.TestServer; +import com.sun.mail.test.TestSocketFactory; +import com.sun.mail.test.TestSSLSocketFactory; + +import org.junit.Test; +import org.junit.Rule; +import org.junit.rules.Timeout; +import static org.junit.Assert.fail; +import static org.junit.Assert.assertTrue; + +/** + * Test that write timeouts work. + */ +public final class WriteTimeoutSocketTest { + + // timeout the test in case of deadlock + @Rule + public Timeout deadlockTimeout = Timeout.seconds(20); + + private static final int TIMEOUT = 200; // ms + private static final String data = + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + + /** + * Test write timeouts with plain sockets. + */ + @Test + public void test() { + final Properties properties = new Properties(); + properties.setProperty("mail.imap.host", "localhost"); + properties.setProperty("mail.imap.writetimeout", "" + TIMEOUT); + test(properties, false); + } + + /** + * Test write timeouts with custom socket factory. + */ + @Test + public void testSocketFactory() { + final Properties properties = new Properties(); + properties.setProperty("mail.imap.host", "localhost"); + properties.setProperty("mail.imap.writetimeout", "" + TIMEOUT); + TestSocketFactory sf = new TestSocketFactory(); + properties.put("mail.imap.socketFactory", sf); + properties.setProperty("mail.imap.socketFactory.fallback", "false"); + test(properties, false); + // make sure our socket factory was actually used + assertTrue(sf.getSocketCreated()); + } + + /** + * Test write timeouts with SSL sockets. + */ + @Test + public void testSSL() { + final Properties properties = new Properties(); + properties.setProperty("mail.imap.host", "localhost"); + properties.setProperty("mail.imap.writetimeout", "" + TIMEOUT); + properties.setProperty("mail.imap.ssl.enable", "true"); + // enable only the anonymous cipher suites since there's no + // server certificate + properties.setProperty("mail.imap.ssl.ciphersuites", + getAnonCipherSuites()); + test(properties, true); + } + + /** + * Test write timeouts with a custom SSL socket factory. + */ + @Test + public void testSSLSocketFactory() throws Exception { + final Properties properties = new Properties(); + properties.setProperty("mail.imap.host", "localhost"); + properties.setProperty("mail.imap.writetimeout", "" + TIMEOUT); + properties.setProperty("mail.imap.ssl.enable", "true"); + TestSSLSocketFactory sf = new TestSSLSocketFactory(); + sf.setDefaultCipherSuites(getAnonCipherSuitesArray()); + properties.put("mail.imap.ssl.socketFactory", sf); + // don't fall back to non-SSL + properties.setProperty("mail.imap.socketFactory.fallback", "false"); + // enable only the anonymous cipher suites since there's no + // server certificate + properties.setProperty("mail.imap.ssl.ciphersuites", + getAnonCipherSuites()); + test(properties, true); + // make sure our socket factory was actually used + assertTrue(sf.getSocketWrapped() || sf.getSocketCreated()); + } + + /** + * Test that WriteTimeoutSocket overrides all methods from Socket. + * XXX - this is kind of hacky since it depends on Method.toString + */ + @Test + public void testOverrides() throws Exception { + Set<String> socketMethods = new HashSet<>(); + Method[] m = java.net.Socket.class.getDeclaredMethods(); + String className = java.net.Socket.class.getName() + "."; + for (int i = 0; i < m.length; i++) { + if (Modifier.isPublic(m[i].getModifiers()) && + !Modifier.isStatic(m[i].getModifiers())) { + String name = m[i].toString(). + replace("synchronized ", ""). + replace(className, ""); + socketMethods.add(name); + } + } + Set<String> wtsocketMethods = new HashSet<>(); + m = WriteTimeoutSocket.class.getDeclaredMethods(); + className = WriteTimeoutSocket.class.getName() + "."; + for (int i = 0; i < m.length; i++) { + if (Modifier.isPublic(m[i].getModifiers())) { + String name = m[i].toString(). + replace("synchronized ", ""). + replace(className, ""); + socketMethods.remove(name); + } + } + for (String s : socketMethods) + System.out.println("WriteTimeoutSocket did not override: " + s); + assertTrue(socketMethods.isEmpty()); + } + + private static String[] getAnonCipherSuitesArray() { + SSLSocketFactory sf = (SSLSocketFactory)SSLSocketFactory.getDefault(); + List<String> anon = new ArrayList<>(); + String[] suites = sf.getSupportedCipherSuites(); + for (int i = 0; i < suites.length; i++) { + if (suites[i].indexOf("_anon_") >= 0) { + anon.add(suites[i]); + } + } + return anon.toArray(new String[anon.size()]); + } + + private static String getAnonCipherSuites() { + SSLSocketFactory sf = (SSLSocketFactory)SSLSocketFactory.getDefault(); + StringBuilder anon = new StringBuilder(); + String[] suites = sf.getSupportedCipherSuites(); + for (int i = 0; i < suites.length; i++) { + if (suites[i].indexOf("_anon_") >= 0) { + if (anon.length() > 0) + anon.append(" "); + anon.append(suites[i]); + } + } + return anon.toString(); + } + + private void test(Properties properties, boolean isSSL) { + TestServer server = null; + try { + final TimeoutHandler handler = new TimeoutHandler(); + server = new TestServer(handler, isSSL); + server.start(); + + properties.setProperty("mail.imap.port", "" + server.getPort()); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + MimeMessage msg = new MimeMessage(session); + msg.setFrom("test@example.com"); + msg.setSubject("test"); + final int size = 8192000; // enough data to fill network buffers + byte[] part = new byte[size]; + for (int i = 0; i < size; i++) { + int j = i % 64; + if (j == 62) + part[i] = (byte)'\r'; + else if (j == 63) + part[i] = (byte)'\n'; + else + part[i] = (byte)data.charAt((j + i / 64) % 62); + } + msg.setDataHandler(new DataHandler( + new ByteArrayDataSource(part, "text/plain"))); + msg.saveChanges(); + + final Store store = session.getStore("imap"); + try { + store.connect("test", "test"); + final Folder f = store.getFolder("test"); + f.appendMessages(new Message[] { msg }); + fail("No timeout"); + } catch (StoreClosedException scex) { + // success! + } catch (Exception ex) { + System.out.println(ex); + //ex.printStackTrace(); + fail(ex.toString()); + } finally { + store.close(); + } + } catch (final Exception e) { + //e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + } + } + } + + /** + * Custom handler. + */ + private static final class TimeoutHandler extends IMAPHandler { + @Override + protected void collectMessage(int bytes) throws IOException { + try { + // allow plenty of time for even slow machines to time out + Thread.sleep(TIMEOUT*20); + } catch (InterruptedException ex) { } + super.collectMessage(bytes); + } + + @Override + public void list(String line) throws IOException { + untagged("LIST () \"/\" test"); + ok(); + } + } +}
diff --git a/mail/src/test/java/com/sun/mail/util/logging/AbstractLogging.java b/mail/src/test/java/com/sun/mail/util/logging/AbstractLogging.java new file mode 100644 index 0000000..0ef4d95 --- /dev/null +++ b/mail/src/test/java/com/sun/mail/util/logging/AbstractLogging.java
@@ -0,0 +1,393 @@ +/* + * Copyright (c) 2016, 2018 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2016, 2018 Jason Mehrens. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package com.sun.mail.util.logging; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.lang.reflect.AccessibleObject; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Properties; +import java.util.logging.Level; +import java.util.logging.LogManager; +import java.util.logging.LogRecord; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * Common super class for the logging test suite. + * + * @author Jason Mehrens + * @since JavaMail 1.5.6 + */ +abstract class AbstractLogging { + + /** + * Used to print debug information about the given throwable. + * + * @param t the throwable. + * @throws NullPointerException if given throwable is null. + */ + @SuppressWarnings({"CallToThreadDumpStack", "CallToPrintStackTrace"}) + static void dump(final Throwable t) { + t.printStackTrace(); + } + + /** + * Gets all of the predefined Levels. + * + * @return an array of log levels. + */ + static Level[] getAllLevels() { + final Field[] fields = Level.class.getFields(); + List<Level> a = new ArrayList<>(fields.length); + for (Field field : fields) { + if (Modifier.isStatic(field.getModifiers()) + && Level.class.isAssignableFrom(field.getType())) { + try { + a.add((Level) field.get((Object) null)); + } catch (IllegalArgumentException | IllegalAccessException ex) { + fail(ex.toString()); + } + } + } + return a.toArray(new Level[a.size()]); + } + + /** + * Determines if the given class is from the JavaMail API Reference + * Implementation {@code com.sun.mail} package. + * + * @param k the type to test. + * @return true if this is part of reference implementation but not part of + * the official API spec. + * @throws Exception if there is a problem. + */ + final boolean isPrivateSpec(final Class<?> k) throws Exception { + return isFromJavaMail(k, false); + } + + /** + * Reinitialize the logging properties using the given properties. + * + * @param manager the log manager. + * @param props the properties. + * @throws IOException if there is a problem. + * @throws NullPointerException if either argument is null. + */ + final void read(LogManager manager, Properties props) throws IOException { + //JDK-4810637 + ByteArrayOutputStream out = new ByteArrayOutputStream(512); + props.store(out, getClass().getName()); + manager.readConfiguration(new ByteArrayInputStream(out.toByteArray())); + } + + /** + * Sets the log record time using milliseconds from the epoch of + * 1970-01-01T00:00:00Z. Any nanosecond information is set to zero. This + * method is used to support JDK8 when running on JDK9 or newer. + * + * @param record the log record to adjust. + * @param epochMilli the time in milliseconds from epoch. + * @throws NullPointerException if the given record is null. + */ + @SuppressWarnings("deprecation") //See JDK-8144262 and K7091. + static void setEpochMilli(final LogRecord record, final long epochMilli) { + record.setMillis(epochMilli); + } + + /** + * Sets the log record time using the seconds and nanoseconds of the epoch + * from 1970-01-01T00:00:00Z. + * + * @param record the log record. + * @param epochSecond the seconds. + * @param nanoAdjustment the nano seconds. + * @throws ClassNotFoundException if running on pre JDK 8. + * @throws NoSuchMethodException if running on JDK 8. + * @throws Exception if there is a problem. + */ + static void setEpochSecond(final LogRecord record, final long epochSecond, + final long nanoAdjustment) throws Exception { + final Class<?> k = Class.forName("java.time.Instant"); + Method instant = k.getMethod("ofEpochSecond", long.class, long.class); + Method set = LogRecord.class.getMethod("setInstant", k); + set.invoke(record, instant.invoke(null, epochSecond, nanoAdjustment)); + } + + /** + * Determines if the {@code java.time} APIs are available for this JVM. + * + * @return true if the time classes can be loaded. + */ + static boolean hasJavaTimeModule() { + try { + Class.forName("java.time.Duration"); + Class.forName("java.time.Instant"); + Class.forName("java.time.ZonedDateTime"); + Class.forName("java.time.ZoneId"); + return true; + } catch (final ClassNotFoundException | LinkageError notSupported) { + } + return false; + } + + /** + * Fails if any declared types are outside of the logging-mailhandler.jar. + * This includes classes from the JavaMail spec. + * + * @param k the type to check for dependencies. + * @throws Exception if there is a problem. + */ + final void testJavaMailLinkage(final Class<?> k) throws Exception { + testJavaMailLinkage(k, true); + } + + /** + * Fails if any declared types are outside of the logging-mailhandler.jar. + * + * @param k the type to check for dependencies. + * @param includeSpec if true this includes official JavaMail spec classes. + * @throws Exception if there is a problem. + */ + final void testJavaMailLinkage(final Class<?> k, final boolean includeSpec) + throws Exception { + assertFalse(k.getName(), isFromJavaMail(k, includeSpec)); + for (Annotation an : k.getDeclaredAnnotations()) { + assertFalse(an.toString(), + isFromJavaMail(an.annotationType(), includeSpec)); + } + + for (Method m : k.getDeclaredMethods()) { + assertFalse(m.getReturnType().getName(), + isFromJavaMail(m.getReturnType(), includeSpec)); + for (Class<?> p : m.getParameterTypes()) { + assertFalse(p.getName(), isFromJavaMail(p, includeSpec)); + } + + for (Class<?> e : m.getExceptionTypes()) { + assertFalse(e.getName(), isFromJavaMail(e, includeSpec)); + } + + for (Annotation an : m.getDeclaredAnnotations()) { + assertFalse(an.toString(), + isFromJavaMail(an.annotationType(), includeSpec)); + } + } + + for (Constructor<?> c : k.getDeclaredConstructors()) { + for (Class<?> p : c.getParameterTypes()) { + assertFalse(p.getName(), isFromJavaMail(p, includeSpec)); + } + + for (Class<?> e : c.getExceptionTypes()) { + assertFalse(e.getName(), isFromJavaMail(e, includeSpec)); + } + + for (Annotation an : c.getDeclaredAnnotations()) { + assertFalse(an.toString(), + isFromJavaMail(an.annotationType(), includeSpec)); + } + } + + for (Field f : k.getDeclaredFields()) { + for (Annotation an : k.getDeclaredAnnotations()) { + assertFalse(an.toString(), + isFromJavaMail(an.annotationType(), includeSpec)); + } + assertFalse(f.getName(), isFromJavaMail(f.getType(), includeSpec)); + } + } + + /** + * Tests that the private static loadDeclaredClasses method of the given + * type. Objects used by the MailHandler during a push might require + * declaring classes to be loaded on create since a push may happen after a + * class loader is shutdown. + * + * @param k the type to check never null. + * @throws Exception if there is a problem. + */ + final void testLoadDeclaredClasses(Class<?> k) throws Exception { + Method m = k.getDeclaredMethod("loadDeclaredClasses"); + assertTrue(Modifier.isStatic(m.getModifiers())); + assertTrue(Modifier.isPrivate(m.getModifiers())); + assertEquals(Class[].class, m.getReturnType()); + m.setAccessible(true); + Class<?>[] named = (Class<?>[]) m.invoke((Object) null); + assertTrue(named.length != 0); + HashSet<Class<?>> declared = new HashSet<>( + Arrays.<Class<?>>asList(k.getDeclaredClasses())); + for (Class<?> c : named) { + assertEquals(c.toString(), k, c.getEnclosingClass()); + assertTrue(c.getDeclaredClasses().length == 0); + declared.remove(c); + } + assertTrue(declared.toString(), declared.isEmpty()); + } + + /** + * Checks that the given class is visible to the LogManager. + * + * @param c the class to check. + * @throws Exception if there is a problem. + */ + final void testLogManagerModifiers(final Class<?> c) throws Exception { + assertTrue(Modifier.isPublic(c.getModifiers())); + assertTrue(Modifier.isPublic(c.getConstructor().getModifiers())); + } + + /** + * Checks that the given class is not dependent on the + * {@code javax.annotation} classes as they are not present in all + * environments. + * + * @param k the class to inspect. + * @throws Exception if there is a problem. + */ + final void testNoDependencyOnJavaxAnnotations(Class<?> k) throws Exception { + for (Method m : k.getDeclaredMethods()) { + testNoJavaxAnnotation(m); + } + + for (Field f : k.getDeclaredFields()) { + testNoJavaxAnnotation(f); + } + + for (Constructor<?> c : k.getDeclaredConstructors()) { + testNoJavaxAnnotation(c); + } + + for (Class<?> i : k.getInterfaces()) { + testNoJavaxAnnotation(i); + } + + for (Class<?> d : k.getDeclaredClasses()) { + testNoDependencyOnJavaxAnnotations(d); + } + } + + /** + * WebappClassLoader.clearReferencesStaticFinal() method will ignore fields + * that have type names that start with 'java.' or 'javax.'. This test + * checks that the given class conforms to this rule so it doesn't become a + * target that will be nullified by the WebappClassLoader. + * + * @param c the class to check. + * @throws Exception if there is a problem. + */ + final void testWebappClassLoaderFieldNames(Class<?> c) throws Exception { + for (Field f : c.getDeclaredFields()) { + Class<?> k = f.getType(); + while (k.isArray()) { + k = k.getComponentType(); + } + + /** + * The WebappClassLoader ignores primitives, non-static, and + * synthetic fields. For the logging API, this test is stricter than + * what the WebappClassLoader actually clears. This restricts the + * logging API to standard field types for both static and + * non-static fields. The idea is to try to stay forward compatible + * with WebappClassLoader. + */ + if (f.getName().indexOf('$') < 0 && !k.isPrimitive() + && !k.getName().startsWith("java.") + && !k.getName().startsWith("javax.")) { + fail(f.toString()); + } + } + + for (Class<?> ic : c.getDeclaredClasses()) { + testWebappClassLoaderFieldNames(ic); + } + } + + /** + * Blocks the current thread until current time has elapsed by one + * millisecond. + * + * @throws InterruptedException if the current thread is interrupted. + */ + static void tickMilli() throws InterruptedException { + tickMilli(1L); + } + + /** + * Blocks the current thread until current time has elapsed by the given + * delay in milliseconds. + * + * @param delay the number of milliseconds that have to elapse. + * @throws IllegalArgumentException if the given delay is zero or less. + * @throws InterruptedException if the current thread is interrupted. + */ + @SuppressWarnings("SleepWhileInLoop") + static void tickMilli(long delay) throws InterruptedException { + if (delay <= 0L) { + throw new IllegalArgumentException(Long.toString(delay)); + } + long then = System.currentTimeMillis(); + for (int i = 0; i < Short.MAX_VALUE; i++) { + long now = System.currentTimeMillis(); + long delta = (now - then); + if (delta >= delay) { + return; + } + Thread.sleep(delay - delta); + } + throw new AssertionError(then + " " + System.currentTimeMillis()); + } + + private boolean isFromJavaMail(Class<?> k, boolean include) throws Exception { + for (Class<?> t = k; t != null; t = t.getSuperclass()) { + final String n = t.getName(); + if (n.startsWith("javax.mail.")) { + return include; + } + + //Not included with logging-mailhandler.jar. + if (n.startsWith("com.sun.mail.") + && !n.startsWith("com.sun.mail.util.logging.")) { + return true; + } + } + return false; + } + + private void testNoJavaxAnnotation(AccessibleObject fm) throws Exception { + for (Annotation a : fm.getAnnotations()) { + testNoJavaxAnnotation(a.annotationType()); + } + } + + private void testNoJavaxAnnotation(Class<?> k) throws Exception { + for (; k != null; k = k.getSuperclass()) { + assertFalse(k.toString(), + k.getName().startsWith("javax.annotation")); + } + } +}
diff --git a/mail/src/test/java/com/sun/mail/util/logging/CollectorFormatterTest.java b/mail/src/test/java/com/sun/mail/util/logging/CollectorFormatterTest.java new file mode 100644 index 0000000..f608345 --- /dev/null +++ b/mail/src/test/java/com/sun/mail/util/logging/CollectorFormatterTest.java
@@ -0,0 +1,1089 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2018 Jason Mehrens. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package com.sun.mail.util.logging; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.text.DateFormat; +import java.text.MessageFormat; +import java.text.NumberFormat; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.logging.*; +import java.util.logging.Formatter; +import org.junit.*; +import static org.junit.Assert.*; + +/** + * The collector formatter tests. + * + * @author Jason Mehrens + * @since JavaMail 1.5.2 + */ +public class CollectorFormatterTest extends AbstractLogging { + + /** + * See LogManager. + */ + private static final String LOG_CFG_KEY = "java.util.logging.config.file"; + /** + * Date and time simple format pattern. + */ + private static final String DATE_TIME_FMT = "EEE, MMM dd HH:mm:ss:S ZZZ yyyy"; + + /** + * The line separator. + */ + private static final String LINE_SEP = System.lineSeparator(); + + private static void checkJVMOptions() throws Exception { + assertTrue(CollectorFormatterTest.class.desiredAssertionStatus()); + assertNull(System.getProperty("java.util.logging.manager")); + assertNull(System.getProperty("java.util.logging.config.class")); + assertNull(System.getProperty(LOG_CFG_KEY)); + assertEquals(LogManager.class, LogManager.getLogManager().getClass()); + } + + private static void fullFence() { + LogManager.getLogManager().getProperty(""); + } + + @BeforeClass + public static void setUpClass() throws Exception { + checkJVMOptions(); + } + + @AfterClass + public static void tearDownClass() throws Exception { + checkJVMOptions(); + } + + @Before + public void setUp() { + fullFence(); + } + + @After + public void tearDown() { + fullFence(); + } + + @Test + public void testDeclaredClasses() throws Exception { + Class<?>[] declared = CollectorFormatter.class.getDeclaredClasses(); + assertEquals(Arrays.toString(declared), 0, declared.length); + } + + @Test + public void testFormatHead() { + String msg = "message"; + XMLFormatter xml = new XMLFormatter(); + CollectorFormatter f = new CollectorFormatter("{0}", xml, + (Comparator<LogRecord>) null); + assertEquals("", f.getHead((Handler) null)); + f.format(new LogRecord(Level.SEVERE, msg)); + + String result = f.getTail((Handler) null); + String expect = f.finish(xml.getHead((Handler) null)); + assertEquals(result, expect); + assertEquals("", f.getHead((Handler) null)); + } + + @Test(expected = NullPointerException.class) + public void testFormatApplyReturnsNull() { + CollectorFormatter f = new ApplyReturnsNull(); + for (int i = 0; i < 10; i++) { + String o = f.format(new LogRecord(Level.INFO, "")); + assertNotNull(o); + } + } + + @Test(expected = NullPointerException.class) + public void testFormatNull() { + CollectorFormatter f = new CollectorFormatter("{1}", (Formatter) null, + (Comparator<LogRecord>) null); + f.format((LogRecord) null); + } + + private static final class TestFormatterAccept extends LogRecord { + + private static final long serialVersionUID = 1L; + + int inferred; + @SuppressWarnings("FieldMayBeFinal") + private transient Formatter f; + + TestFormatterAccept(Level level, CollectorFormatter f) { + super(level, ""); + this.f = f; + } + + @Override + public Throwable getThrown() { + if (inferred == 0) { + assertEquals(Level.INFO.getLocalizedName().concat("11111"), + f.getTail((Handler) null)); + } + return super.getThrown(); + } + + @Override + public String getSourceMethodName() { + ++inferred; + return super.getSourceMethodName(); + } + } + + @Test(timeout = 60000) + public void testFormatAccept() throws Exception { + final CollectorFormatter f = new CollectorFormatter( + "{1}{3}{5}{7}{8}{13}", + new CompactFormatter("%4$s"), + new SeverityComparator()); + LogRecord first = new LogRecord(Level.INFO, ""); + first.setThrown(new Throwable()); + setEpochMilli(first, 1L); + f.format(first); + + TestFormatterAccept r = new TestFormatterAccept(Level.FINE, f); + setEpochMilli(r, 2L); + r.setThrown(new Throwable()); + f.format(r); + assertEquals(1, r.inferred); + assertEquals(Level.FINE.getLocalizedName().concat("11222"), + f.getTail((Handler) null)); + } + + private static final class TestFormatAcceptAndUpdate extends LogRecord { + + private static final long serialVersionUID = 1L; + int inferred; + @SuppressWarnings("FieldMayBeFinal") + private transient Formatter f; + + public TestFormatAcceptAndUpdate(Level level, CollectorFormatter f) { + super(level, ""); + this.f = f; + } + + @Override + public String getSourceMethodName() { + if (++inferred == 1) { + LogRecord r = new LogRecord(Level.INFO, ""); + setEpochMilli(r, 1L); + f.format(r); + } + return super.getSourceMethodName(); + } + } + + @Test(timeout = 60000) + public void testFormatAcceptAndUpdate() throws Exception { + final CollectorFormatter f = new CollectorFormatter( + "{1}{3}{5}{7}{8}{13}", + new CompactFormatter("%4$s"), + new SeverityComparator()); + + TestFormatAcceptAndUpdate r + = new TestFormatAcceptAndUpdate(Level.SEVERE, f); + setEpochMilli(r, 2L); + r.setThrown(new Throwable()); + f.format(r); + assertEquals(2, r.inferred); + assertEquals(Level.SEVERE.getLocalizedName().concat("21121"), + f.getTail((Handler) null)); + } + + @Test + public void testFormatFormat() { + String msg = "message"; + XMLFormatter xml = new XMLFormatter(); + CollectorFormatter f = new CollectorFormatter("{1}", xml, + (Comparator<LogRecord>) null); + assertEquals("", f.getTail((Handler) null)); + LogRecord r; + r = new LogRecord(Level.SEVERE, msg); + f.format(r); + + String result = f.getTail((Handler) null); + String expect = f.finish(xml.format(r)); + assertEquals(result, expect); + assertEquals("", f.getTail((Handler) null)); + } + + @Test + public void testFormatFormatLocale() throws Exception { + String msg = "message"; + XMLFormatter xml = new XMLFormatter(); + CollectorFormatter f = new CollectorFormatter("{1}", xml, + (Comparator<LogRecord>) null); + + assertEquals("", f.getTail((Handler) null)); + LogRecord r; + r = new LogRecord(Level.SEVERE, LOG_CFG_KEY); + Properties props = new Properties(); + props.put(LOG_CFG_KEY, msg); + + r.setResourceBundle(new LocaleResource(props, Locale.US)); + assertNotNull(r.getResourceBundle().getLocale()); + f.format(r); + + String result = f.getTail((Handler) null); + String expect = f.finish(xml.format(r)); + assertEquals(result, expect); + assertEquals(msg, f.formatMessage(r)); + assertEquals(msg, xml.formatMessage(r)); + assertEquals("", f.getTail((Handler) null)); + } + + @Test + public void testFormatNoRecords() { + XMLFormatter xml = new XMLFormatter(); + CollectorFormatter f = new CollectorFormatter((String) null, xml, + (Comparator<LogRecord>) null); + + String result = f.getTail((Handler) null); + String expect = f.finish(xml.getHead((Handler) null)) + + f.finish(xml.getTail((Handler) null)) + '\n'; + assertEquals(result, expect); + } + + @Test + public void testFormatOneRecord() { + XMLFormatter xml = new XMLFormatter(); + CollectorFormatter f = new CollectorFormatter((String) null, xml, + (Comparator<LogRecord>) null); + LogRecord record = new LogRecord(Level.SEVERE, "message"); + assertEquals("", f.format(record)); + String result = f.getTail((Handler) null); + String expect = f.finish(xml.getHead((Handler) null)) + + f.finish(xml.format(record)) + + f.finish(xml.getTail((Handler) null)) + '\n'; + assertEquals(result, expect); + } + + @Test + public void testFormatTwoRecords() { + XMLFormatter xml = new XMLFormatter(); + CollectorFormatter f = new CollectorFormatter((String) null, xml, + (Comparator<LogRecord>) null); + LogRecord record = new LogRecord(Level.SEVERE, "first"); + assertEquals("", f.format(record)); + + record = new LogRecord(Level.SEVERE, "second"); + assertEquals("", f.format(record)); + String result = f.getTail((Handler) null); + String expect = f.finish(xml.getHead((Handler) null)) + + f.finish(xml.format(record)) + + f.finish(xml.getTail((Handler) null)) + "... 1 more\n"; + assertEquals(result, expect); + } + + @Test(expected = NullPointerException.class) + public void testFinishNull() { + CollectorFormatter f = new CollectorFormatter(); + String result = f.finish((String) null); + fail(result); + } + + @Test + public void testFinish() { + CollectorFormatter f = new CollectorFormatter(); + String format = LINE_SEP + f.getClass().getName() + LINE_SEP; + assertFalse(f.getClass().getName().equals(format)); + assertEquals(f.getClass().getName(), f.finish(format)); + } + + @Test + public void testFormatTail() { + String msg = "message"; + XMLFormatter xml = new XMLFormatter(); + CollectorFormatter f = new CollectorFormatter("{2}", xml, + (Comparator<LogRecord>) null); + f.format(new LogRecord(Level.SEVERE, msg)); + + String result = f.getTail((Handler) null); + String expect = f.finish(xml.getTail((Handler) null)); + assertEquals(result, expect); + assertEquals(expect, f.getTail((Handler) null)); + } + + @Test + public void testFormatCount() { + String msg = "message"; + CollectorFormatter f = new CollectorFormatter("{3}", (Formatter) null, + (Comparator<LogRecord>) null); + assertEquals("0", f.getTail((Handler) null)); + f.format(new LogRecord(Level.SEVERE, msg)); + f.format(new LogRecord(Level.SEVERE, msg)); + f.format(new LogRecord(Level.SEVERE, msg)); + f.format(new LogRecord(Level.SEVERE, msg)); + + String result = f.getTail((Handler) null); + assertEquals(result, "4"); + assertEquals("0", f.getTail((Handler) null)); + } + + @Test + public void testFormatRemaing() { + String msg = "message"; + CollectorFormatter f = new CollectorFormatter("{4}", (Formatter) null, + (Comparator<LogRecord>) null); + assertEquals("-1", f.getTail((Handler) null)); + f.format(new LogRecord(Level.SEVERE, msg)); + f.format(new LogRecord(Level.SEVERE, msg)); + f.format(new LogRecord(Level.SEVERE, msg)); + f.format(new LogRecord(Level.SEVERE, msg)); + + String result = f.getTail((Handler) null); + assertEquals(result, "3"); + assertEquals("-1", f.getTail((Handler) null)); + } + + @Test + public void testFormatThrown() { + String msg = "message"; + CollectorFormatter f = new CollectorFormatter("{5}", (Formatter) null, + (Comparator<LogRecord>) null); + assertEquals("0", f.getTail((Handler) null)); + f.format(new LogRecord(Level.SEVERE, msg)); + f.format(new LogRecord(Level.SEVERE, msg)); + + LogRecord r = new LogRecord(Level.SEVERE, msg); + r.setThrown(new Exception()); + f.format(r); + + r = new LogRecord(Level.SEVERE, msg); + r.setThrown(new Exception()); + f.format(r); + + r = new LogRecord(Level.SEVERE, msg); + r.setThrown(new Exception()); + f.format(r); + + String result = f.getTail((Handler) null); + assertEquals(result, "3"); + assertEquals("0", f.getTail((Handler) null)); + } + + @Test + public void testFormatNormal() { + String msg = "message"; + CollectorFormatter f = new CollectorFormatter("{6}", (Formatter) null, + (Comparator<LogRecord>) null); + + assertEquals("0", f.getTail((Handler) null)); + f.format(new LogRecord(Level.SEVERE, msg)); + f.format(new LogRecord(Level.SEVERE, msg)); + + LogRecord r = new LogRecord(Level.SEVERE, msg); + r.setThrown(new Exception()); + f.format(r); + + r = new LogRecord(Level.SEVERE, msg); + r.setThrown(new Exception()); + f.format(r); + + r = new LogRecord(Level.SEVERE, msg); + r.setThrown(new Exception()); + f.format(r); + + String result = f.getTail((Handler) null); + assertEquals(result, "2"); + assertEquals("0", f.getTail((Handler) null)); + } + + @Test + public void testFormatNextMin() throws Exception { + CollectorFormatter minF = new CollectorFormatter("{7}", + (Formatter) null, + (Comparator<LogRecord>) null); + + tickMilli(); //Make sure the max not equal to the start time. + + final String min = minF.getTail((Handler) null); + NumberFormat.getIntegerInstance().parse(min); + tickMilli(); + + //Next min is not old min. + String next = minF.getTail((Handler) null); + assertFalse(min + ' ' + next, min.equals(next)); + + //All mins start at the init time. + CollectorFormatter initF = new CollectorFormatter("{10}", + (Formatter) null, + (Comparator<LogRecord>) null); + + next = initF.getTail((Handler) null); + assertEquals(min, next); + } + + @Test + public void testFormatMinDateTime() { + String msg = "message"; + CollectorFormatter f = new CollectorFormatter("{7,date,short} {7,time}", + (Formatter) null, + (Comparator<LogRecord>) null); + + long min = 100L; + + LogRecord r = new LogRecord(Level.SEVERE, msg); + setEpochMilli(r, min + 1000L); + f.format(r); + + r = new LogRecord(Level.SEVERE, msg); + setEpochMilli(r, min + 2000L); + f.format(r); + + r = new LogRecord(Level.SEVERE, msg); + setEpochMilli(r, min); + f.format(r); + + r = new LogRecord(Level.SEVERE, msg); + setEpochMilli(r, min + 3000L); + f.format(r); + + r = new LogRecord(Level.SEVERE, msg); + setEpochMilli(r, min + 4000L); + f.format(r); + + String result = f.getTail((Handler) null); + assertEquals(result, MessageFormat.format("{0,date,short} {0,time}", min)); + } + + @Test + public void testFormatNextMax() throws Exception { + CollectorFormatter f = new CollectorFormatter("{8}", (Formatter) null, + (Comparator<LogRecord>) null); + + String now = f.getTail((Handler) null); + Number num = NumberFormat.getIntegerInstance().parse(now); + assertFalse(Long.MIN_VALUE == num.longValue()); + tickMilli(); + String next = f.getTail((Handler) null); + assertFalse(NumberFormat.getIntegerInstance().parse(now).longValue() + == Long.MIN_VALUE); + assertFalse(now.equals(next)); + } + + @Test + public void testFormatMaxDateTime() { + String msg = "message"; + CollectorFormatter f = new CollectorFormatter("{8,date,short} {8,time}", + (Formatter) null, + (Comparator<LogRecord>) null); + + long min = 100L; + long high = 4000L; + + LogRecord r = new LogRecord(Level.SEVERE, msg); + setEpochMilli(r, min + 1000L); + f.format(r); + + r = new LogRecord(Level.SEVERE, msg); + setEpochMilli(r, min + high); + f.format(r); + + r = new LogRecord(Level.SEVERE, msg); + setEpochMilli(r, min + 2000L); + f.format(r); + + r = new LogRecord(Level.SEVERE, msg); + setEpochMilli(r, min); + f.format(r); + + r = new LogRecord(Level.SEVERE, msg); + setEpochMilli(r, min + 3000L); + f.format(r); + + String result = f.getTail((Handler) null); + assertEquals(result, MessageFormat.format("{0,date,short} {0,time}", min + high)); + } + + @Test + public void testFormatWindowToInstant() throws Exception { + CollectorFormatter f = new CollectorFormatter("{7}_{8}", + (Formatter) null, + (Comparator<LogRecord>) null); + + LogRecord r = new LogRecord(Level.SEVERE, ""); + setEpochMilli(r, 100); + f.format(r); + + r = new LogRecord(Level.SEVERE, ""); + setEpochMilli(r, 200); + f.format(r); + + //Check that the min and max are different. + String output = f.getTail((Handler) null); + int fence = output.indexOf('_'); + assertFalse(output.regionMatches(0, output, fence + 1, + (output.length() - fence) - 1)); + + r = new LogRecord(Level.SEVERE, ""); + setEpochMilli(r, 400); + f.format(r); + + //Previous max is 200 so at this point the min and max better be 400. + output = f.getTail((Handler) null); + fence = output.indexOf('_'); + assertTrue(output.regionMatches(0, output, fence + 1, + (output.length() - fence) - 1)); + assertEquals("400_400", output); + } + + @Test + public void testGetTail() { + CollectorFormatter f = new CollectorFormatter( + "{0}{1}{2}{3}{4}{5}{6}{7}{8}{9}{10}{11}{12}{13}", + (Formatter) null, + (Comparator<LogRecord>) null); + assertTrue(f.getTail((Handler) null).length() != 0); + assertTrue(f.getTail((Handler) null).length() != 0); + } + + @Test + public void testGetTailExample1() { + String p = "{0}{1}{2}{4,choice,-1#|0#|0<... {4,number,integer} more}\n"; + CollectorFormatter cf = new CollectorFormatter(p); + LogRecord r = new LogRecord(Level.WARNING, "warning message"); + cf.format(r); + + r = new LogRecord(Level.SEVERE, "Encoding failed."); + RuntimeException npe = new NullPointerException(); + StackTraceElement frame = new StackTraceElement("java.lang.String", + "getBytes", "String.java", 913); + npe.setStackTrace(new StackTraceElement[]{frame}); + r.setThrown(npe); + cf.format(r); + + cf.format(new LogRecord(Level.INFO, "info")); + cf.format(new LogRecord(Level.INFO, "info")); + String output = cf.getTail((Handler) null); + assertNotNull(output); + } + + @Test + public void testGetTailExample2() { + String p = "These {3} messages occurred between\n" + + "{7,date,EEE, MMM dd HH:mm:ss:S ZZZ yyyy} and " + + "{8,time,EEE, MMM dd HH:mm:ss:S ZZZ yyyy}\n"; + CollectorFormatter cf = new CollectorFormatter(p); + LogRecord min = new LogRecord(Level.SEVERE, ""); + setEpochMilli(min, 1248203502449L); + cf.format(min); + + int count = 290; + for (int i = 0; i < count; ++i) { + LogRecord mid = new LogRecord(Level.SEVERE, ""); + setEpochMilli(mid, min.getMillis()); + cf.format(mid); + } + + LogRecord max = new LogRecord(Level.SEVERE, ""); + setEpochMilli(max, 1258723764000L); + cf.format(max); + Object[] args = new Object[9]; + args[3] = count + 2L; + args[7] = min.getMillis(); + args[8] = max.getMillis(); + assertEquals(MessageFormat.format(p, args), cf.toString()); + String output = cf.getTail((Handler) null); + assertNotNull(output); + } + + @Test + public void testGetTailExample3a() { + String p = "These {3} messages occurred between\n" + + "{7,date,EEE, MMM dd HH:mm:ss:S ZZZ yyyy}" + + " and {8,time,EEE, MMM dd HH:mm:ss:S ZZZ yyyy}\n"; + CollectorFormatter cf = new CollectorFormatter(p); + LogRecord min = new LogRecord(Level.SEVERE, ""); + setEpochMilli(min, 1248203502449L); + cf.format(min); + + for (int i = 0; i < 71; ++i) { + LogRecord mid = new LogRecord(Level.SEVERE, ""); + setEpochMilli(mid, min.getMillis()); + cf.format(mid); + } + + LogRecord max = new LogRecord(Level.SEVERE, ""); + setEpochMilli(max, min.getMillis() + 110500); + cf.format(max); + + String output = cf.getTail((Handler) null); + assertNotNull(output); + } + + @Test + public void testGetTailExample3b() { + String p = "These {3} messages occurred between " + + "{9,choice,86400000#{7,date} {7,time} and {8,time}" + + "|86400000<{7,date} and {8,date}}\n"; + CollectorFormatter cf = new CollectorFormatter(p); + LogRecord min = new LogRecord(Level.SEVERE, ""); + setEpochMilli(min, 1248203502449L); + + cf.format(min); + for (int i = 0; i < 114; ++i) { + LogRecord mid = new LogRecord(Level.SEVERE, ""); + setEpochMilli(mid, min.getMillis()); + cf.format(mid); + } + + LogRecord max = new LogRecord(Level.SEVERE, ""); + setEpochMilli(max, min.getMillis() + 2591000000L); + cf.format(max); + + String output = cf.getTail((Handler) null); + assertNotNull(output); + } + + @Test + public void testGetTailExample4() throws Exception { + String p = "{13} alert reports since {10,date}.\n"; + CollectorFormatter cf = new CollectorFormatter(p); + + int count = 4320; + for (int i = 1; i < count; ++i) { + LogRecord mid = new LogRecord(Level.SEVERE, ""); + cf.format(mid); + cf.getTail((Handler) null); + } + + String output = cf.getTail((Handler) null); + assertNotNull(output); + String jd73 = NumberFormat.getIntegerInstance().format(count); + assertTrue(output.startsWith(jd73)); + } + + @Test + public void testNewDefaultFormatter() { + String msg = ""; + CollectorFormatter f = new CollectorFormatter(); + f.format(new LogRecord(Level.SEVERE, msg)); + f.format(new LogRecord(Level.SEVERE, msg)); + String result = f.getTail((Handler) null); + assertTrue(result, result.length() != 0); + assertTrue(result, result.contains("...")); + assertTrue(result, result.contains("1")); + assertTrue(result, result.contains("more")); + } + + @Test + public void testNewFormatterWithString() { + CollectorFormatter f = new CollectorFormatter("{3}"); + f.format(new LogRecord(Level.SEVERE, "")); + String result = f.getTail((Handler) null); + assertEquals(result, "1"); + } + + @Test + public void testNewFormatterNullString() { + CollectorFormatter f = new CollectorFormatter((String) null); + assertEquals(CollectorFormatter.class, f.getClass()); + } + + @Test + public void testNewFormatterNullNonNullNonNull() { + CollectorFormatter f = new CollectorFormatter((String) null, + new XMLFormatter(), SeverityComparator.getInstance()); + assertEquals(CollectorFormatter.class, f.getClass()); + } + + @Test + public void testNewFormatterNullNullNull() { + CollectorFormatter f = new CollectorFormatter((String) null, + (Formatter) null, (Comparator<LogRecord>) null); + assertEquals(CollectorFormatter.class, f.getClass()); + } + + @Test + public void testNewFormatterNonNullNullNull() { + CollectorFormatter f = new CollectorFormatter("Test {0}", + (Formatter) null, (Comparator<LogRecord>) null); + assertEquals(CollectorFormatter.class, f.getClass()); + } + + @Test + public void testToString() { + String msg = "message"; + CollectorFormatter f = new CollectorFormatter("{3}", (Formatter) null, + (Comparator<LogRecord>) null); + + f.format(new LogRecord(Level.SEVERE, msg)); + f.format(new LogRecord(Level.SEVERE, msg)); + f.format(new LogRecord(Level.SEVERE, msg)); + f.format(new LogRecord(Level.SEVERE, msg)); + + String result = f.toString(); + assertEquals(result, f.toString()); + assertEquals(result, f.getTail((Handler) null)); + assertFalse(result.equals(f.toString())); + } + + @Test(expected = NullPointerException.class) + public void testApplyNullAndNull() { + CollectorFormatter f = new CollectorFormatter("{3}", (Formatter) null, + (Comparator<LogRecord>) null); + f.apply((LogRecord) null, (LogRecord) null); + } + + @Test(expected = NullPointerException.class) + public void testApplyNullAndLogRecord() { + CollectorFormatter f = new CollectorFormatter("{3}", (Formatter) null, + (Comparator<LogRecord>) null); + f.apply((LogRecord) null, new LogRecord(Level.SEVERE, "")); + } + + @Test(expected = NullPointerException.class) + public void testApplyLogRecordAndNull() { + CollectorFormatter f = new CollectorFormatter("{3}", (Formatter) null, + (Comparator<LogRecord>) null); + f.apply(new LogRecord(Level.SEVERE, ""), (LogRecord) null); + } + + @Test + public void testApplyWithoutComparator() { + CollectorFormatter f = new CollectorFormatter("{3}", (Formatter) null, + (Comparator<LogRecord>) null); + LogRecord first = new LogRecord(Level.SEVERE, ""); + LogRecord second = new LogRecord(Level.WARNING, ""); + assertSame(second, f.apply(first, second)); + } + + @Test + public void testApplyWithComparator() { + CollectorFormatter f = new CollectorFormatter("{3}", (Formatter) null, + SeverityComparator.getInstance()); + LogRecord first = new LogRecord(Level.SEVERE, ""); + LogRecord second = new LogRecord(Level.WARNING, ""); + assertSame(first, f.apply(first, second)); + } + + @Test + public void testComparator() throws Exception { + final String p = CollectorFormatter.class.getName(); + Properties props = new Properties(); + props.put(p.concat(".comparator"), SeverityComparator.class.getName()); + props.put(p.concat(".comparator.reverse"), "false"); + LogManager manager = LogManager.getLogManager(); + try { + read(manager, props); + CollectorFormatter cf = new CollectorFormatter(); + LogRecord first = new LogRecord(Level.SEVERE, Level.SEVERE.getName()); + LogRecord second = new LogRecord(Level.WARNING, Level.WARNING.getName()); + cf.format(second); + cf.format(first); + String result = cf.getTail((Handler) null); + assertTrue(result, result.startsWith(Level.SEVERE.getName())); + } finally { + manager.reset(); + } + } + + @Test + public void testComparatorReverse() throws Exception { + final String p = CollectorFormatter.class.getName(); + Properties props = new Properties(); + props.put(p.concat(".comparator"), SeverityComparator.class.getName()); + props.put(p.concat(".comparator.reverse"), "true"); + LogManager manager = LogManager.getLogManager(); + try { + read(manager, props); + CollectorFormatter cf = new CollectorFormatter(); + LogRecord first = new LogRecord(Level.SEVERE, Level.SEVERE.getName()); + LogRecord second = new LogRecord(Level.WARNING, Level.WARNING.getName()); + cf.format(second); + cf.format(first); + String result = cf.getTail((Handler) null); + assertTrue(result, result.startsWith(Level.WARNING.getName())); + } finally { + manager.reset(); + } + } + + @Test + public void testFormat() throws Exception { + final String p = CollectorFormatter.class.getName(); + Properties props = new Properties(); + final String expect = CollectorFormatterTest.class.getName(); + props.put(p.concat(".format"), expect); + LogManager manager = LogManager.getLogManager(); + try { + read(manager, props); + CollectorFormatter cf = new CollectorFormatter(); + LogRecord first = new LogRecord(Level.SEVERE, Level.SEVERE.getName()); + assertEquals("", cf.format(first)); + String result = cf.getTail((Handler) null); + assertEquals(expect, result); + } finally { + manager.reset(); + } + } + + @Test + public void testFormatter() throws Exception { + final String p = CollectorFormatter.class.getName(); + Properties props = new Properties(); + props.put(p.concat(".formatter"), XMLFormatter.class.getName()); + LogManager manager = LogManager.getLogManager(); + try { + read(manager, props); + XMLFormatter xml = new XMLFormatter(); + CollectorFormatter cf = new CollectorFormatter(); + LogRecord first = new LogRecord(Level.SEVERE, Level.SEVERE.getName()); + assertEquals("", cf.format(first)); + String result = cf.getTail((Handler) null); + assertEquals(result, cf.finish(xml.getHead((Handler) null)) + + cf.finish(xml.format(first)) + + cf.finish(xml.getTail((Handler) null)) + '\n'); + } finally { + manager.reset(); + } + } + + @Test + public void testFormatElapsedTime() throws Exception { + CollectorFormatter f = new CollectorFormatter("{9}", (Formatter) null, + (Comparator<LogRecord>) null); + LogRecord r = new LogRecord(Level.SEVERE, ""); + setEpochMilli(r, 25L); + f.format(r); + + r = new LogRecord(Level.SEVERE, ""); + setEpochMilli(r, 100L); + f.format(r); + + String init = f.getTail((Handler) null); + Number n = NumberFormat.getIntegerInstance().parse(init); + assertEquals(75, n.longValue()); + } + + @Test + public void testFormatInitTimeMillis() throws Exception { + CollectorFormatter f = new CollectorFormatter("{10}", (Formatter) null, + (Comparator<LogRecord>) null); + + String init = f.getTail((Handler) null); + NumberFormat.getIntegerInstance().parse(init); + tickMilli(); + + assertTrue(init.equals(f.getTail((Handler) null))); + } + + @Test + public void testFormatInitTimeDateTime() throws Exception { + CollectorFormatter f = new CollectorFormatter( + "{10,date," + DATE_TIME_FMT + "}", + (Formatter) null, + (Comparator<LogRecord>) null); + + String init = f.getTail((Handler) null); + DateFormat df = new SimpleDateFormat(DATE_TIME_FMT); + Date dt = df.parse(init); + tickMilli(); + + assertTrue(init.equals(f.getTail((Handler) null))); + assertTrue(dt.equals(df.parse(f.getTail((Handler) null)))); + } + + @Test + public void testFormatCurrentTimeMillis() throws Exception { + CollectorFormatter f = new CollectorFormatter("{11}", (Formatter) null, + (Comparator<LogRecord>) null); + + String now = f.getTail((Handler) null); + NumberFormat.getIntegerInstance().parse(now); + tickMilli(); + + assertFalse(now.equals(f.getTail((Handler) null))); + } + + @Test + public void testFormatCurrentTimeDateTime() throws Exception { + CollectorFormatter f = new CollectorFormatter( + "{11,date," + DATE_TIME_FMT + "}", + (Formatter) null, + (Comparator<LogRecord>) null); + + String init = f.getTail((Handler) null); + DateFormat df = new SimpleDateFormat(DATE_TIME_FMT); + Date dt = df.parse(init); + tickMilli(); + + assertFalse(init.equals(f.getTail((Handler) null))); + assertFalse(dt.equals(df.parse(f.getTail((Handler) null)))); + } + + @Test + public void testFormatUpTime() throws Exception { + CollectorFormatter f = new CollectorFormatter("{12}", (Formatter) null, + (Comparator<LogRecord>) null); + + String up = f.getTail((Handler) null); + NumberFormat.getIntegerInstance().parse(up); + tickMilli(); + + assertFalse(up.equals(f.getTail((Handler) null))); + } + + @Test + public void testFormatGeneration() { + String msg = "message"; + CollectorFormatter f = new CollectorFormatter("{13}", (Formatter) null, + (Comparator<LogRecord>) null); + + assertEquals("1", f.getTail((Handler) null)); + assertEquals("1", f.toString()); + assertEquals("1", f.getTail((Handler) null)); + assertEquals("1", f.toString()); + + f.format(new LogRecord(Level.SEVERE, msg)); + f.format(new LogRecord(Level.SEVERE, msg)); + f.format(new LogRecord(Level.SEVERE, msg)); + f.format(new LogRecord(Level.SEVERE, msg)); + + assertEquals("1", f.getTail((Handler) null)); //reset + + assertEquals("2", f.getTail((Handler) null)); + assertEquals("2", f.toString()); + assertEquals("2", f.getTail((Handler) null)); + assertEquals("2", f.toString()); + + f.format(new LogRecord(Level.SEVERE, msg)); + f.format(new LogRecord(Level.SEVERE, msg)); + f.format(new LogRecord(Level.SEVERE, msg)); + f.format(new LogRecord(Level.SEVERE, msg)); + + assertEquals("2", f.getTail((Handler) null)); //reset + assertEquals("3", f.getTail((Handler) null)); + assertEquals("3", f.toString()); + assertEquals("3", f.toString()); + assertEquals("3", f.getTail((Handler) null)); + } + + @Test + public void testFormatNullFormatter() { + CollectorFormatter f = new CollectorFormatter("{1}", (Formatter) null, + (Comparator<LogRecord>) null); + LogRecord r = new LogRecord(Level.SEVERE, "message {0}"); + r.setParameters(new Object[]{1}); + f.format(r); + String output = f.getTail((Handler) null); + assertEquals(f.formatMessage(r), output); + } + + @Test + public void testFormatIllegalPattern() { + CollectorFormatter f = new CollectorFormatter("{9"); + f.format(new LogRecord(Level.SEVERE, "")); + try { + f.getTail((Handler) null); + fail("Expected format exception."); + } catch (IllegalArgumentException expect) { + } + } + + @Test + public void testFormatIllegalTargetPattern() { + CollectorFormatter f = new CollectorFormatter("{1}", + new CompactFormatter("%1$#tc"), (Comparator<LogRecord>) null); + f.format(new LogRecord(Level.SEVERE, "")); + try { + f.getTail((Handler) null); + fail("Expected format exception."); + } catch (java.util.IllegalFormatException expect) { + } + } + + @Test + public void testJavaMailLinkage() throws Exception { + testJavaMailLinkage(CollectorFormatter.class); + } + + @Test + public void testLogManagerModifiers() throws Exception { + testLogManagerModifiers(CollectorFormatter.class); + } + + @Test + public void testWebappClassLoaderFieldNames() throws Exception { + testWebappClassLoaderFieldNames(CollectorFormatter.class); + } + + /** + * An example of a broken implementation of apply. + */ + private static class ApplyReturnsNull extends CollectorFormatter { + + /** + * The number of records. + */ + private int count; + + /** + * Promote access level. + */ + ApplyReturnsNull() { + } + + @Override + protected LogRecord apply(LogRecord t, LogRecord u) { + assertNotNull(t); + assertNotNull(u); + return (++count & 1) == 1 ? u : null; + } + } + + /** + * A properties resource bundle with locale. + */ + private static class LocaleResource extends PropertyResourceBundle { + + /** + * The locale + */ + private final Locale locale; + + /** + * Creates the locale resource. + * + * @param p the properties. + * @param l the locale. + * @throws IOException if there is a problem. + */ + LocaleResource(Properties p, Locale l) throws IOException { + super(toStream(p)); + locale = l; + } + + private static InputStream toStream(Properties p) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + p.store(out, LocaleResource.class.getName()); + return new ByteArrayInputStream(out.toByteArray()); + } + + @Override + public Locale getLocale() { + return locale; + } + } +}
diff --git a/mail/src/test/java/com/sun/mail/util/logging/CompactFormatterTest.java b/mail/src/test/java/com/sun/mail/util/logging/CompactFormatterTest.java new file mode 100644 index 0000000..c79fe7e --- /dev/null +++ b/mail/src/test/java/com/sun/mail/util/logging/CompactFormatterTest.java
@@ -0,0 +1,1680 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2018 Jason Mehrens. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package com.sun.mail.util.logging; + +import java.io.*; +import java.lang.reflect.Method; +import java.net.SocketException; +import java.util.*; +import java.util.logging.Level; +import java.util.logging.LogManager; +import java.util.logging.LogRecord; +import javax.mail.MessagingException; +import javax.mail.internet.MimeUtility; +import org.junit.*; +import static org.junit.Assert.*; + +/** + * Compact formatter tests. + * + * @author Jason Mehrens + * @since JavaMail 1.5.2 + */ +public class CompactFormatterTest extends AbstractLogging { + + private static final String UNKNOWN_CLASS_NAME + = CompactFormatterTest.class.getName().concat("Foo"); + + /** + * The line separator. + */ + private static final String LINE_SEP = System.lineSeparator(); + + /** + * The max width. + */ + private static final int MAX_PRE = 160; + /** + * The default left to right pattern. + */ + private static final String LEFT_TO_RIGHT = "%7$#." + MAX_PRE + "s%n"; + + /** + * See LogManager. + */ + private static final String LOG_CFG_KEY = "java.util.logging.config.file"; + + private static void checkJVMOptions() throws Exception { + assertTrue(CollectorFormatterTest.class.desiredAssertionStatus()); + assertNull(System.getProperty("java.util.logging.manager")); + assertNull(System.getProperty("java.util.logging.config.class")); + assertNull(System.getProperty(LOG_CFG_KEY)); + assertEquals(LogManager.class, LogManager.getLogManager().getClass()); + } + + private static void fullFence() { + LogManager.getLogManager().getProperty(""); + } + + @BeforeClass + public static void setUpClass() throws Exception { + checkJVMOptions(); + assertTrue("Recompile tests to include source line numbers.", + new Throwable().getStackTrace()[0].getLineNumber() >= 0); + try { + throw new AssertionError(Class.forName(UNKNOWN_CLASS_NAME)); + } catch (ClassNotFoundException expect) { + } + } + + @AfterClass + public static void tearDownClass() throws Exception { + checkJVMOptions(); + } + + @Before + public void setUp() { + fullFence(); + } + + @After + public void tearDown() { + fullFence(); + } + + @Test + public void testDeclaredClasses() throws Exception { + testLoadDeclaredClasses(CompactFormatter.class); + } + + @Test + public void testFormat() throws Exception { + final String p = CompactFormatter.class.getName(); + Properties props = new Properties(); + props.put(p.concat(".format"), "%9$s"); + LogManager manager = LogManager.getLogManager(); + try { + read(manager, props); + CompactFormatter cf = new CompactFormatter(); + LogRecord first = new LogRecord(Level.SEVERE, Level.INFO.getName()); + first.setSequenceNumber(Short.MAX_VALUE); + String result = cf.format(first); + assertEquals(String.valueOf((int) Short.MAX_VALUE), result); + } finally { + manager.reset(); + } + } + + @Test + public void testNewFormatterWithPattern() { + CompactFormatter cf = new CompactFormatter("%4$s"); + String result = cf.format(new LogRecord(Level.SEVERE, "")); + assertEquals(Level.SEVERE.getLocalizedName(), result); + } + + @Test + public void testNewFormatterNullPattern() { + CompactFormatter cf = new CompactFormatter((String) null); + assertEquals(CompactFormatter.class, cf.getClass()); + } + + @Test + public void testGetHeadAndGetTail() { + CompactFormatter cf = new CompactFormatter(); + assertEquals("", cf.getHead(null)); + assertEquals("", cf.getTail(null)); + } + + @Test + public void testFormatWithMessage() { + LogRecord record = new LogRecord(Level.SEVERE, "message"); + CompactFormatter cf = new CompactFormatter(); + String result = cf.format(record); + assertTrue(result, result.startsWith(record.getMessage())); + assertTrue(result, result.endsWith(LINE_SEP)); + } + + @Test + public void testFormatWithMessagePrecisionOverWidth() { + LogRecord record = new LogRecord(Level.SEVERE, "message"); + record.setThrown(new Throwable("thrown")); + CompactFormatter cf = new CompactFormatter("%7$#6.12s"); + String result = cf.format(record); + assertEquals("mes|Throwable", result); + } + + @Test + public void testFormatWithMessageWidthOverPrecision() { + LogRecord record = new LogRecord(Level.SEVERE, "message"); + record.setThrown(new Throwable("thrown")); + CompactFormatter cf = new CompactFormatter("%7$#12.6s"); + String result = cf.format(record); + assertEquals("mes\u0020\u0020\u0020|Thr\u0020\u0020\u0020", result); + } + + @Test + public void testFormatWithMessageEmpty() { + LogRecord record = new LogRecord(Level.SEVERE, ""); + CompactFormatter cf = new CompactFormatter(); + String result = cf.format(record); + assertEquals(result, LINE_SEP); + } + + @Test + public void testFormatMessageSurrogate() { + LogRecord record = new LogRecord(Level.SEVERE, + "a\ud801\udc00\ud801\udc00\ud801\udc00\ud801\udc00"); + record.setThrown(new Throwable("thrown")); + CompactFormatter cf = new CompactFormatter("%7$#.6s%n"); + String result = cf.format(record); + assertTrue(result, result.startsWith("a\ud801\udc00")); + assertTrue(result, result.endsWith("|Thr" + LINE_SEP)); + } + + @Test + public void testFormatWithMessageAndThrownLeftToRight() { + LogRecord record = new LogRecord(Level.SEVERE, "message"); + record.setThrown(new Throwable("thrown")); + CompactFormatter cf = new CompactFormatter(); + String result = cf.format(record); + assertTrue(result, result.startsWith(record.getMessage())); + assertTrue(result, result.contains("|")); + assertTrue(result, result.contains(Throwable.class.getSimpleName())); + assertTrue(result, result.contains(CompactFormatterTest.class.getSimpleName())); + assertTrue(result, result.contains("testFormatWithMessageAndThrown")); + assertTrue(result, result.contains(String.valueOf( + record.getThrown().getStackTrace()[0].getLineNumber()))); + assertTrue(result, result.endsWith(LINE_SEP)); + } + + @Test + public void testFormatWithThrownLeftToRight() { + testFormatWithThrown(LEFT_TO_RIGHT); + } + + private void testFormatWithThrown(String fmt) { + LogRecord record = new LogRecord(Level.SEVERE, ""); + record.setThrown(new Throwable("thrown")); + CompactFormatter cf = new CompactFormatter(fmt); + String result = cf.format(record); + assertFalse(result, result.startsWith("|")); + assertTrue(result, result.contains(record.getThrown().getMessage())); + assertTrue(result, result.endsWith(LINE_SEP)); + } + + @Test(expected = NullPointerException.class) + public void testFormatMessageNull() { + CompactFormatter cf = new CompactFormatter(); + cf.formatMessage((LogRecord) null); + fail(cf.toString()); + } + + @Test + public void testFormatMessageLocale() throws Exception { + String msg = "message"; + CompactFormatter cf = new CompactFormatter("%5$s"); + Properties props = new Properties(); + props.put(LOG_CFG_KEY, msg); + + LogRecord r = new LogRecord(Level.SEVERE, LOG_CFG_KEY); + r.setResourceBundle(new LocaleResource(props, Locale.US)); + assertNotNull(r.getResourceBundle().getLocale()); + String result = cf.format(r); + assertEquals(msg, result); + assertEquals(msg, cf.formatMessage(r)); + } + + @Test + public void testFormatMessage_LogRecordLeftToRight() { + testFormatMessage_LogRecord(LEFT_TO_RIGHT); + } + + private void testFormatMessage_LogRecord(String fmt) { + Exception e = new IOException(); + assertNull(e.getMessage(), e.getMessage()); + + e = new Exception(e.toString(), e); + assertNotNull(e.getMessage(), e.getMessage()); + + LogRecord record = new LogRecord(Level.SEVERE, e.toString()); + record.setThrown(e); + CompactFormatter cf = new CompactFormatter(fmt); + String result = cf.format(record); + + assertTrue(result, result.contains("|")); + assertTrue(e.toString(), e.toString().contains(Exception.class.getPackage().getName())); + assertTrue(e.toString(), e.toString().contains(IOException.class.getPackage().getName())); + int idx; + idx = result.indexOf(Exception.class.getSimpleName()); + assertTrue(result, idx >= 0); + + idx = result.indexOf(IOException.class.getSimpleName(), idx); + assertTrue(result, idx >= 0); + + assertTrue(result, result.contains(record.getThrown().getClass().getSimpleName())); + assertTrue(result, result.contains(record.getThrown().getCause().getClass().getSimpleName())); + + assertFalse(result, result.contains(Exception.class.getPackage().getName())); + assertFalse(result, result.contains(IOException.class.getPackage().getName())); + + assertFalse(result, result.contains(Exception.class.getName())); + assertFalse(result, result.contains(IOException.class.getName())); + + assertTrue(result, result.endsWith(LINE_SEP)); + } + + @Test(timeout = 30000) + public void testFormatMessage_LogRecordEvil() { + LogRecord record = new LogRecord(Level.SEVERE, ""); + record.setThrown(createEvilThrowable()); + CompactFormatter cf = new CompactFormatter(); + cf.formatMessage(record); + } + + @Test + public void testFormatMaxMessageWidthLeftToRight() { + testFormatMaxMessageWidth(LEFT_TO_RIGHT, MAX_PRE); + } + + private void testFormatMaxMessageWidth(String fmt, int width) { + assertTrue(fmt, fmt.contains(Integer.toString(width))); + assertTrue(String.valueOf(width), width < Integer.MAX_VALUE / 4); + + CompactFormatter cf = new CompactFormatter(fmt); + LogRecord record = new LogRecord(Level.SEVERE, ""); + int padding = LINE_SEP.length(); + for (int i = 1; i < width; i++) { + record.setMessage(rpad("", i, "A")); + String result = cf.format(record); + assertTrue(result, result.length() == i + padding); + assertTrue(result, result.endsWith(LINE_SEP)); + } + + for (int i = width; i <= (width * 4); i++) { + record.setMessage(rpad("", i, "A")); + String result = cf.format(record); + assertTrue(result, result.length() == width + padding); + assertTrue(result, result.endsWith(LINE_SEP)); + } + } + + @Test + public void testFormatMaxThrowableWidthLeftToRight() { + String fmt = LEFT_TO_RIGHT; + int width = MAX_PRE; + assertTrue(fmt, fmt.contains(Integer.toString(width))); + assertTrue(String.valueOf(width), width < Integer.MAX_VALUE / 4); + + CompactFormatter cf = new CompactFormatter(fmt); + LogRecord record = new LogRecord(Level.SEVERE, ""); + int padding = LINE_SEP.length(); + for (int i = 0; i < width; i++) { + record.setThrown(new Throwable(rpad("", i, "A"))); + String result = cf.format(record); + assertTrue(result, result.length() <= width + padding); + + assertTrue(result, result.endsWith(LINE_SEP)); + } + + for (int i = width; i <= (width * 4); i++) { + record.setThrown(new Throwable(rpad("", i, "A"))); + String result = cf.format(record); + assertTrue(result.length() + ", " + (width + padding), + result.length() == width + padding); + assertTrue(result, result.endsWith(LINE_SEP)); + } + } + + @Test + public void testFormatThrownTrailingDot() { + testFormatThrownIllegalClassName("Hello."); + } + + @Test + public void testFormatThrownClassDotSpace() { + String msg = "test"; + String prefix = IllegalStateException.class.getName() + ". "; + Throwable t = new PrefixException(prefix, null, null); + assertEquals(prefix, t.toString()); + LogRecord record = new LogRecord(Level.SEVERE, ""); + record.setThrown(t); + CompactFormatter cf = new CompactFormatter("%6$s"); + String result = cf.format(record); + StackTraceElement[] ste = t.getStackTrace(); + String frame = CompactFormatterTest.class.getSimpleName() + + '.' + ste[0].getMethodName() + "(:" + + ste[0].getLineNumber() + ")"; + + String cns = t.getClass().getSimpleName() + + ": " + IllegalStateException.class.getSimpleName(); + assertTrue(result, result.startsWith(cns)); + assertTrue(result, result.indexOf(cns) == result.lastIndexOf(cns)); + assertTrue(result, result.contains(msg)); + assertTrue(result, result.indexOf(msg) == result.lastIndexOf(msg)); + assertTrue(result, result.endsWith(frame)); + + cf = new CompactFormatter("%11$s %14$s"); + assertEquals(result, cf.format(record)); + } + + @Test + public void testFormatThrownLeadingDot() { + testFormatThrownIllegalClassName(".Hello"); + } + + @Test + public void testFormatThrownDotDot() { + testFormatThrownIllegalClassName("Hello..World"); + testFormatThrownIllegalClassName("..HelloWorld"); + testFormatThrownIllegalClassName("HelloWorld.."); + } + + @Test + public void testFormatThrownColonSpace() { + testFormatThrownIllegalClassName("Hello: World"); + } + + @Test + public void testFormatThrownEndSign() { + //Some of these are legal but not worth considering legal. + testFormatThrownIllegalClassName("HelloWorld$"); + testFormatThrownIllegalClassName("HelloWorld.$"); + testFormatThrownIllegalClassName("Hello.World$"); + } + + @Test + public void testFormatThrownStartSign() { + testFormatThrownIllegalClassName("$HelloWorld"); + testFormatThrownIllegalClassName("$.HelloWorld"); + } + + @Test + public void testFormatThrownDotDotSign() { + testFormatThrownIllegalClassName("Hello..$World"); + testFormatThrownIllegalClassName("..$HelloWorld"); + testFormatThrownIllegalClassName("HelloWorld..$"); + } + + @Test + public void testFormatThrownSignDot() { + testFormatThrownIllegalClassName("$.HelloWorld"); + testFormatThrownIllegalClassName("HelloWorld$."); + } + + @Test + public void testFormatThrownSignDotDot() { + testFormatThrownIllegalClassName("Hello$..World"); + testFormatThrownIllegalClassName("$..HelloWorld"); + testFormatThrownIllegalClassName("HelloWorld$.."); + } + + @Test + public void testFormatThrownDotSignDot() { + testFormatThrownIllegalClassName("Hello.$.World"); + testFormatThrownIllegalClassName(".$.HelloWorld"); + testFormatThrownIllegalClassName("HelloWorld.$."); + } + + private void testFormatThrownIllegalClassName(String prefix) { + Throwable t = new PrefixException(prefix, null, null); + assertEquals(prefix, t.toString()); + LogRecord record = new LogRecord(Level.SEVERE, ""); + record.setThrown(t); + CompactFormatter cf = new CompactFormatter("%6$s"); + String result = cf.format(record); + StackTraceElement[] ste = t.getStackTrace(); + String frame = CompactFormatterTest.class.getSimpleName() + + '.' + ste[0].getMethodName() + "(:" + + ste[0].getLineNumber() + ")"; + + String cn = t.getClass().getSimpleName(); + assertTrue(result, result.startsWith(cn)); + assertTrue(result, result.indexOf(cn) == result.lastIndexOf(cn)); + assertTrue(result, result.contains(prefix)); + assertTrue(result, result.indexOf(prefix) == result.lastIndexOf(prefix)); + assertTrue(result, result.endsWith(frame)); + + cf = new CompactFormatter("%11$s %14$s"); + assertEquals(result, cf.format(record)); + } + + @Test + public void testFormatThrownSimpleClassNameNullMessage() { + //javax.management.BadStringOperationException + String op = "some op"; + Throwable t = new PrefixException(PrefixException.class.getSimpleName() + + ": " + op, (String) null, null); + assertNull(t.getMessage()); + assertNotNull(t.toString()); + assertTrue(t.toString().startsWith(t.getClass().getSimpleName())); + + LogRecord record = new LogRecord(Level.SEVERE, ""); + record.setThrown(t); + CompactFormatter cf = new CompactFormatter("%6$s"); + String result = cf.format(record); + StackTraceElement[] ste = t.getStackTrace(); + String frame = CompactFormatterTest.class.getSimpleName() + + '.' + ste[0].getMethodName() + "(:" + + ste[0].getLineNumber() + ")"; + + String sn = t.getClass().getSimpleName(); + assertTrue(result, result.startsWith(sn)); + assertTrue(result, result.indexOf(sn) == result.lastIndexOf(sn)); + assertTrue(result, result.contains(op)); + assertTrue(result, result.indexOf(op) == result.lastIndexOf(op)); + assertTrue(result, result.endsWith(frame)); + + cf = new CompactFormatter("%11$s %14$s"); + assertEquals(result, cf.format(record)); + } + + @Test + public void testFormatServerSidetMetroException() { + //com.sun.xml.ws.developer.ServerSideException + String msg = "server error"; + NullPointerException npe = new NullPointerException(msg); + Throwable t = new PrefixException(npe.getClass().getName(), msg, null); + assertEquals(msg, npe.getMessage()); + assertEquals(msg, t.getMessage()); + assertEquals(npe.toString(), t.toString()); + + LogRecord record = new LogRecord(Level.SEVERE, ""); + record.setThrown(t); + CompactFormatter cf = new CompactFormatter("%6$s"); + String result = cf.format(record); + StackTraceElement[] ste = t.getStackTrace(); + String frame = CompactFormatterTest.class.getSimpleName() + + '.' + ste[0].getMethodName() + "(:" + + ste[0].getLineNumber() + ")"; + + String cns = t.getClass().getSimpleName() + + ": " + npe.getClass().getSimpleName(); + assertTrue(result, result.startsWith(cns)); + assertTrue(result, result.indexOf(cns) == result.lastIndexOf(cns)); + assertTrue(result, result.contains(msg)); + assertTrue(result, result.endsWith(frame)); + + cf = new CompactFormatter("%11$s %14$s"); + assertEquals(result, cf.format(record)); + } + + @Test + public void testFormatThrownPrefixMessageRetainsFqn() { + String msg = "java.io.tmpdir"; + NullPointerException npe = new NullPointerException(msg); + Throwable t = new PrefixException(npe.getClass().getName(), msg, null); + assertEquals(msg, npe.getMessage()); + assertEquals(msg, t.getMessage()); + assertEquals(npe.toString(), t.toString()); + + LogRecord record = new LogRecord(Level.SEVERE, ""); + record.setThrown(t); + CompactFormatter cf = new CompactFormatter("%6$s"); + String result = cf.format(record); + StackTraceElement[] ste = t.getStackTrace(); + String frame = CompactFormatterTest.class.getSimpleName() + + '.' + ste[0].getMethodName() + "(:" + + ste[0].getLineNumber() + ")"; + + String cns = t.getClass().getSimpleName() + + ": " + npe.getClass().getSimpleName(); + assertTrue(result, result.startsWith(cns)); + assertTrue(result, result.indexOf(cns) == result.lastIndexOf(cns)); + assertTrue(result, result.contains(msg)); + assertTrue(result, result.endsWith(frame)); + + cf = new CompactFormatter("%11$s %14$s"); + assertEquals(result, cf.format(record)); + } + + @Test + public void testFormatThrownHiddenMessageRetainsFqn() { + String msg = "java.io.tmpdir"; + NullPointerException npe = new NullPointerException(msg); + Throwable t = new ToStringException(npe.getClass().getName(), msg); + assertEquals(msg, npe.getMessage()); + assertEquals(msg, t.getMessage()); + + LogRecord record = new LogRecord(Level.SEVERE, ""); + record.setThrown(t); + CompactFormatter cf = new CompactFormatter("%6$s"); + String result = cf.format(record); + StackTraceElement[] ste = t.getStackTrace(); + String frame = CompactFormatterTest.class.getSimpleName() + + '.' + ste[0].getMethodName() + "(:" + + ste[0].getLineNumber() + ")"; + + String cns = t.getClass().getSimpleName() + + ": " + npe.getClass().getSimpleName(); + assertTrue(result, result.startsWith(cns)); + assertTrue(result, result.indexOf(cns) == result.lastIndexOf(cns)); + assertTrue(result, result.contains(msg)); + assertTrue(result, result.endsWith(frame)); + + cf = new CompactFormatter("%11$s %14$s"); + assertEquals(result, cf.format(record)); + } + + @Test + public void testFormatXMLParseXercesException() { + //com.sun.org.apache.xerces.internal.xni.parser.XMLParseException + String msg = "XML"; + String prefix = "1:two:3:four"; + Throwable t = new PrefixException(prefix, msg, null); + + LogRecord record = new LogRecord(Level.SEVERE, ""); + record.setThrown(t); + CompactFormatter cf = new CompactFormatter("%6$s"); + String result = cf.format(record); + StackTraceElement[] ste = t.getStackTrace(); + String frame = CompactFormatterTest.class.getSimpleName() + + '.' + ste[0].getMethodName() + "(:" + + ste[0].getLineNumber() + ")"; + + assertTrue(prefix, t.toString().startsWith(prefix)); + String cn = t.getClass().getSimpleName(); + assertTrue(result, result.startsWith(cn)); + assertTrue(result, result.indexOf(cn) == result.lastIndexOf(cn)); + assertTrue(result, result.contains(prefix)); + assertTrue(result, result.indexOf(prefix) == result.lastIndexOf(prefix)); + assertTrue(result, result.contains(msg)); + assertTrue(result, result.endsWith(frame)); + + cf = new CompactFormatter("%11$s %14$s"); + assertEquals(result, cf.format(record)); + } + + @Test + public void testFormatGSSException() { + //org.ietf.jgss.GSSException + String msg = "Invalid name provided"; + String prefix = PrefixException.class.getSimpleName(); + Throwable t = new PrefixException(prefix, msg, null); + assertTrue(t.toString().startsWith(t.getClass().getSimpleName())); + LogRecord record = new LogRecord(Level.SEVERE, ""); + record.setThrown(t); + CompactFormatter cf = new CompactFormatter("%6$s"); + String result = cf.format(record); + StackTraceElement[] ste = t.getStackTrace(); + String frame = CompactFormatterTest.class.getSimpleName() + + '.' + ste[0].getMethodName() + "(:" + + ste[0].getLineNumber() + ")"; + + assertTrue(prefix, t.toString().startsWith(prefix)); + String cn = t.getClass().getSimpleName(); + assertTrue(result, result.startsWith(cn)); + assertTrue(result, result.indexOf(cn) == result.lastIndexOf(cn)); + assertTrue(result, result.contains(prefix)); + assertTrue(result, result.indexOf(prefix) == result.lastIndexOf(prefix)); + assertTrue(result, result.contains(msg)); + assertTrue(result, result.endsWith(frame)); + + cf = new CompactFormatter("%11$s %14$s"); + assertEquals(result, cf.format(record)); + } + + @Test + public void testFormatMismatchedTreeNodeException() { + //org.antlr.runtime.MismatchedTreeNodeException + String prefix = ToStringException.class.getSimpleName() + + '(' + String.class.getName() + "!=" + + Throwable.class.getName() + ')'; + + Throwable t = new ToStringException(prefix, (String) null); + assertNull(t.getLocalizedMessage()); + assertNull(t.getMessage()); + LogRecord record = new LogRecord(Level.SEVERE, ""); + record.setThrown(t); + CompactFormatter cf = new CompactFormatter("%6$s"); + String result = cf.format(record); + StackTraceElement[] ste = t.getStackTrace(); + String frame = CompactFormatterTest.class.getSimpleName() + + '.' + ste[0].getMethodName() + "(:" + + ste[0].getLineNumber() + ")"; + + assertTrue(prefix, t.toString().startsWith(prefix)); + String cn = t.getClass().getSimpleName(); + assertTrue(result, result.startsWith(cn)); + assertTrue(result, result.indexOf(cn) == result.lastIndexOf(cn)); + assertTrue(result, result.contains(prefix)); + assertTrue(result, result.indexOf(prefix) == result.lastIndexOf(prefix)); + assertTrue(result, result.endsWith(frame)); + + cf = new CompactFormatter("%11$s %14$s"); + assertEquals(result, cf.format(record)); + } + + @Test + public void testFormatInnerException() { + String msg = "inner class"; + String prefix = '(' + String.class.getName() + "!=" + + Throwable.class.getName() + ')'; + + Throwable t = new ToStringException(ToStringException.class.getName() + + prefix, msg); + assertFalse(t.toString().contains(t.getLocalizedMessage())); + LogRecord record = new LogRecord(Level.SEVERE, ""); + record.setThrown(t); + CompactFormatter cf = new CompactFormatter("%6$s"); + String result = cf.format(record); + StackTraceElement[] ste = t.getStackTrace(); + String frame = CompactFormatterTest.class.getSimpleName() + + '.' + ste[0].getMethodName() + "(:" + + ste[0].getLineNumber() + ")"; + + assertTrue(prefix, t.toString().contains(prefix)); + String cn = t.getClass().getSimpleName(); + assertTrue(result, result.startsWith(cn)); + assertTrue(result, result.indexOf(cn) == result.lastIndexOf(cn)); + assertTrue(result, result.contains(prefix)); + assertTrue(result, result.indexOf(prefix) == result.lastIndexOf(prefix)); + assertTrue(result, result.contains(msg)); + assertTrue(result, result.endsWith(frame)); + + cf = new CompactFormatter("%11$s %14$s"); + assertEquals(result, cf.format(record)); + } + + @Test + public void testFormatMessage_Throwable() { + Exception e = new IOException(Exception.class.getName()); + e = new Exception(e.toString(), e); + assertNotNull(e.getMessage(), e.getMessage()); + + CompactFormatter cf = new CompactFormatter(); + String result = cf.formatMessage(e); + assertEquals(IOException.class.getSimpleName() + + ": " + Exception.class.getSimpleName(), result); + } + + @Test + public void testFormatMessage_ThrowableNull() { + CompactFormatter cf = new CompactFormatter(); + String result = cf.formatMessage((Throwable) null); + assertEquals("", result); + } + + @Test + public void testFormatMessage_ThrowableNullMessage() { + CompactFormatter cf = new CompactFormatter(); + String result = cf.formatMessage(new Throwable()); + String expect = Throwable.class.getSimpleName(); + assertEquals(expect, result); + } + + @Test(timeout = 30000) + public void testFormatMessage_ThrowableEvil() { + CompactFormatter cf = new CompactFormatter(); + cf.formatMessage(createEvilThrowable()); + } + + @Test + public void testFormatLevel() { + CompactFormatter cf = new CompactFormatter(); + LogRecord record = new LogRecord(Level.SEVERE, ""); + String result = cf.formatLevel(record); + assertEquals(record.getLevel().getLocalizedName(), result); + } + + @Test(expected = NullPointerException.class) + public void testFormatLevelNull() { + CompactFormatter cf = new CompactFormatter(); + cf.formatLevel((LogRecord) null); + fail(cf.toString()); + } + + @Test + public void testFormatLogger() { + CompactFormatter cf = new CompactFormatter(); + LogRecord record = new LogRecord(Level.SEVERE, ""); + record.setSourceMethodName(null); + record.setSourceClassName(null); + record.setLoggerName(Object.class.getName()); + String result = cf.formatLoggerName(record); + assertEquals(Object.class.getSimpleName(), result); + } + + @Test + public void testFormatRootLogger() { + testFormatLoggerNonClassName(""); + } + + @Test + public void testFormatGlobalLogger() { + testFormatLoggerNonClassName("global"); + } + + @Test + public void testFormatLoggerLeadingDot() { + testFormatLoggerNonClassName(".Hello"); + } + + @Test + public void testFormatLoggerDotDot() { + testFormatLoggerNonClassName("Hello..World"); + } + + @Test + public void testFormatLoggerColonSpace() { + testFormatLoggerNonClassName("Hello: World"); + } + + private void testFormatLoggerNonClassName(String name) { + CompactFormatter cf = new CompactFormatter(); + LogRecord record = new LogRecord(Level.SEVERE, ""); + record.setSourceMethodName(null); + record.setSourceClassName(null); + record.setLoggerName(name); + String result = cf.formatLoggerName(record); + assertEquals(name, result); + + cf = new CompactFormatter("%3$s"); + assertEquals(result, cf.format(record)); + } + + @Test(expected = NullPointerException.class) + public void testFormatLoggerNull() { + CompactFormatter cf = new CompactFormatter(); + cf.formatLoggerName((LogRecord) null); + fail(cf.toString()); + } + + @Test + public void testFormatMillis() { + String p = "%1$tc"; + CompactFormatter cf = new CompactFormatter(p); + LogRecord r = new LogRecord(Level.SEVERE, ""); + assertEquals(String.format(p, r.getMillis()), + cf.format(r)); + } + + @Test + public void testFormatMillisLocale() throws Exception { + String p = "%1$tc"; + CompactFormatter cf = new CompactFormatter(p); + Properties props = new Properties(); + LogRecord r = new LogRecord(Level.SEVERE, ""); + r.setResourceBundle(new LocaleResource(props, Locale.ENGLISH)); + assertEquals(String.format(Locale.ENGLISH, p, r.getMillis()), + cf.format(r)); + } + + @Test + public void testFormatMillisByParts() { + String p = "%1$tb %1$td, %1$tY %1$tl:%1$tM:%1$tS %1$Tp"; + CompactFormatter cf = new CompactFormatter(p); + LogRecord r = new LogRecord(Level.SEVERE, ""); + assertEquals(String.format(p, r.getMillis()), + cf.format(r)); + } + + @Test + public void testFormatMillisByPartsLocale() throws Exception { + String p = "%1$tb %1$td, %1$tY %1$tl:%1$tM:%1$tS %1$Tp"; + CompactFormatter cf = new CompactFormatter(p); + Properties props = new Properties(); + LogRecord r = new LogRecord(Level.SEVERE, ""); + r.setResourceBundle(new LocaleResource(props, Locale.ENGLISH)); + assertEquals(String.format(Locale.ENGLISH, p, r.getMillis()), + cf.format(r)); + } + + @Test + public void testFormatMillisAsLong() { + String p = "%1$tQ"; + CompactFormatter cf = new CompactFormatter(p); + LogRecord r = new LogRecord(Level.SEVERE, ""); + assertEquals(String.format(p, r.getMillis()), + cf.format(r)); + } + + @Test + public void testFormatZoneDateTime() throws Exception { + LogRecord r = new LogRecord(Level.SEVERE, ""); + Object zdt = LogManagerProperties.getZonedDateTime(r); + if (zdt != null) { + String p = "%1$tb %1$td, %1$tY %1$tl:%1$tM:%1$tS.%1$tN %1$Tp"; + CompactFormatter cf = new CompactFormatter(p); + assertEquals(String.format(p, zdt), cf.format(r)); + } else { + try { + Method m = LogRecord.class.getMethod("getInstant"); + fail(m.toString()); + } catch (final NoSuchMethodException expect) { + } + } + } + + @Test(expected = NullPointerException.class) + public void testFormatNull() { + CompactFormatter cf = new CompactFormatter(); + cf.format((LogRecord) null); + } + + @Test + public void testFormatResourceBundleName() { + CompactFormatter cf = new CompactFormatter("%15$s"); + LogRecord r = new LogRecord(Level.SEVERE, ""); + r.setResourceBundleName("name"); + String output = cf.format(r); + assertEquals(r.getResourceBundleName(), output); + } + + @Test + public void testFormatKey() { + CompactFormatter cf = new CompactFormatter("%16$s"); + LogRecord r = new LogRecord(Level.SEVERE, "message {0}"); + r.setParameters(new Object[]{2}); + String output = cf.format(r); + assertEquals(r.getMessage(), output); + assertFalse(output.equals(cf.formatMessage(r))); + } + + @Test + public void testFormatSequence() { + CompactFormatter cf = new CompactFormatter("%9$d"); + LogRecord record = new LogRecord(Level.SEVERE, ""); + String output = cf.format(record); + String expect = Long.toString(record.getSequenceNumber()); + assertEquals(expect, output); + } + + @Test + public void testFormatSourceByLogger() { + CompactFormatter cf = new CompactFormatter(); + LogRecord record = new LogRecord(Level.SEVERE, ""); + record.setSourceMethodName(null); + record.setSourceClassName(null); + record.setLoggerName(Object.class.getName()); + String result = cf.formatSource(record); + assertEquals(Object.class.getSimpleName(), result); + } + + @Test(expected = NullPointerException.class) + public void testFormatSourceNull() { + CompactFormatter cf = new CompactFormatter(); + cf.formatSource((LogRecord) null); + fail(cf.toString()); + } + + @Test + public void testFormatSourceByClass() { + CompactFormatter cf = new CompactFormatter(); + LogRecord record = new LogRecord(Level.SEVERE, ""); + record.setSourceMethodName(null); + record.setSourceClassName(Object.class.getName()); + record.setLoggerName(""); + String result = cf.formatSource(record); + assertEquals(Object.class.getSimpleName(), result); + } + + @Test + public void testFormatSourceByClassAndMethod() { + CompactFormatter cf = new CompactFormatter(); + LogRecord record = new LogRecord(Level.SEVERE, ""); + record.setSourceMethodName("method"); + record.setSourceClassName(Object.class.getName()); + record.setLoggerName(""); + String result = cf.formatSource(record); + assertFalse(result, record.getSourceClassName().equals(record.getSourceMethodName())); + assertTrue(result, result.startsWith(Object.class.getSimpleName())); + assertTrue(result, result.endsWith(record.getSourceMethodName())); + } + + @Test + public void testFormatThrownNullThrown() { + CompactFormatter cf = new CompactFormatter(); + LogRecord record = new LogRecord(Level.SEVERE, ""); + String result = cf.formatThrown(record); + assertTrue(result, result.startsWith(cf.formatMessage(record.getThrown()))); + assertTrue(result, result.endsWith(cf.formatBackTrace(record))); + } + + @Test(timeout = 30000) + public void testFormatThrownEvilThrown() { + LogRecord record = new LogRecord(Level.SEVERE, ""); + record.setThrown(createEvilThrowable()); + CompactFormatter cf = new CompactFormatter(); + cf.formatThrown(record); + } + + @Test + public void testFormatThrown() { + Exception e = new IOException("Fake I/O"); + e = new Exception(e.toString(), e); + assertNotNull(e.getMessage(), e.getMessage()); + + CompactFormatter cf = new CompactFormatter(); + LogRecord record = new LogRecord(Level.SEVERE, ""); + record.setThrown(e); + String result = cf.formatThrown(record); + assertTrue(result, result.startsWith(e.getCause().getClass().getSimpleName())); + assertTrue(result, result.contains(cf.formatMessage(record.getThrown()))); + assertTrue(result, result.endsWith(cf.formatBackTrace(record))); + } + + @Test + public void testFormatThrownLocalized() { + //sun.security.provider.PolicyParser$ParsingException + CountLocalizedException cle = new CountLocalizedException(); + CompactFormatter cf = new CompactFormatter(); + LogRecord record = new LogRecord(Level.SEVERE, ""); + record.setThrown(cle); + String result = cf.formatThrown(record); + assertNotNull(result, result); + assertTrue(cle.localizedMessage > 0); + } + + @Test + public void testInheritsFormatMessage() { + InheritsFormatMessage cf = new InheritsFormatMessage(); + LogRecord record = new LogRecord(Level.SEVERE, ""); + record.setThrown(new Throwable()); + String result = cf.formatThrown(record); + assertNotNull(cf.getClass().getName(), result); + + result = cf.formatError(record); + assertNotNull(cf.getClass().getName(), result); + } + + @Test(expected = NullPointerException.class) + public void testFormatThrownNullRecord() { + CompactFormatter cf = new CompactFormatter(); + cf.formatThrown((LogRecord) null); + fail(cf.toString()); + } + + @Test + public void testFormatThreadID() { + CompactFormatter cf = new CompactFormatter("%10$d"); + LogRecord record = new LogRecord(Level.SEVERE, ""); + record.setThreadID(10); + String output = cf.format(record); + String expect = Long.toString(record.getThreadID()); + assertEquals(expect, output); + + record.setThreadID(-1); //Largest value for the CompactFormatter. + output = cf.format(record); + expect = Long.toString((1L << 32L) - 1L); + assertEquals(expect, output); + + //Test that downcast works right. + Number id = cf.formatThreadID(record); + assertEquals(record.getThreadID(), id.intValue()); + assertEquals(expect, Long.toString(id.longValue())); + } + + @Test(expected = NullPointerException.class) + public void testFormatThreadIDNull() { + CompactFormatter cf = new CompactFormatter(); + cf.formatThreadID((LogRecord) null); + } + + @Test + public void testFormatThreadIDReturnsNull() { + CompactFormatter cf = new ThreadIDReturnsNull(); + LogRecord record = new LogRecord(Level.SEVERE, ""); + record.setThreadID(10); + assertNull(cf.formatThreadID(record)); + String output = cf.format(record); + assertEquals("null", output); + } + + @Test + public void testFormatError() { + CompactFormatter cf = new CompactFormatter("%11$s"); + LogRecord record = new LogRecord(Level.SEVERE, "message"); + record.setThrown(new Throwable("error")); + String output = cf.format(record); + assertTrue(output.startsWith(record.getThrown() + .getClass().getSimpleName())); + assertTrue(output.endsWith(record.getThrown().getMessage())); + } + + @Test(expected = NullPointerException.class) + public void testFormatErrorNull() { + CompactFormatter cf = new CompactFormatter(); + cf.formatError((LogRecord) null); + } + + @Test + public void testFormatErrorNullMessage() { + CompactFormatter cf = new CompactFormatter("%11$s"); + LogRecord record = new LogRecord(Level.SEVERE, "message"); + record.setThrown(new Throwable()); + String output = cf.format(record); + assertNotNull(output); + } + + @Test(expected = NullPointerException.class) + public void testFormatThrownMessageApplyReturnsNull() { + CompactFormatter cf = new ApplyReturnsNull(); + for (int i = 0; i < 10; i++) { + String output = cf.formatMessage(new Throwable()); + assertEquals(Throwable.class.getSimpleName(), output); + } + } + + @Test + public void testFormatMessageError() { + CompactFormatter cf = new CompactFormatter("%12$s"); + LogRecord record = new LogRecord(Level.SEVERE, "message"); + record.setThrown(new Throwable("error")); + String output = cf.format(record); + int t = output.indexOf(record.getThrown().getClass().getSimpleName()); + int f = output.indexOf('|'); + int m = output.indexOf(record.getThrown().getMessage()); + + assertTrue(output, t > -1); + assertTrue(output, m > -1); + assertTrue(output, f > -1); + assertTrue(output, t < m); + assertTrue(output, t > f); + assertTrue(output, f < m); + assertTrue(output, output.startsWith(record.getMessage())); + } + + @Test + public void testFormatErrorMessage() { + CompactFormatter cf = new CompactFormatter("%13$s"); + LogRecord record = new LogRecord(Level.SEVERE, "message"); + record.setThrown(new Throwable("error")); + String output = cf.format(record); + int t = output.indexOf(record.getThrown().getClass().getSimpleName()); + int f = output.indexOf('|'); + int m = output.indexOf(record.getThrown().getMessage()); + + assertTrue(output, t > -1); + assertTrue(output, m > -1); + assertTrue(output, f > -1); + assertTrue(output, t < m); + assertTrue(output, t < f); + assertTrue(output, f > m); + assertTrue(output, output.endsWith(record.getMessage())); + } + + @Test(expected = NullPointerException.class) + public void testErrorApplyReturnsNull() { + CompactFormatter cf = new ApplyReturnsNull(); + LogRecord r = new LogRecord(Level.SEVERE, ""); + for (int i = 0; i < 10; i++) { + String output = cf.formatError(r); + assertNotNull(output); + r.setThrown(new Throwable(Integer.toString(i), r.getThrown())); + assertNotNull(cf.format(r)); + } + } + + @Test + public void testFormatExample1() { + String p = "%7$#.160s%n"; + LogRecord r = new LogRecord(Level.SEVERE, "Encoding failed."); + RuntimeException npe = new NullPointerException(); + StackTraceElement frame = new StackTraceElement("java.lang.String", + "getBytes", "String.java", 913); + npe.setStackTrace(new StackTraceElement[]{frame}); + r.setThrown(npe); + CompactFormatter cf = new CompactFormatter(p); + String output = cf.format(r); + assertNotNull(output); + } + + @Test + public void testFormatExample2() { + String p = "%7$#.20s%n"; + LogRecord r = new LogRecord(Level.SEVERE, "Encoding failed."); + RuntimeException npe = new NullPointerException(); + StackTraceElement frame = new StackTraceElement("java.lang.String", + "getBytes", "String.java", 913); + npe.setStackTrace(new StackTraceElement[]{frame}); + r.setThrown(npe); + CompactFormatter cf = new CompactFormatter(p); + String output = cf.format(r); + assertNotNull(output); + } + + @Test + public void testFormatExample3() { + String p = "%1$tc %2$s%n%4$s: %5$s%6$s%n"; + LogRecord r = new LogRecord(Level.SEVERE, "Encoding failed."); + r.setSourceClassName("MyClass"); + r.setSourceMethodName("fatal"); + setEpochMilli(r, 1258723764000L); + RuntimeException npe = new NullPointerException(); + StackTraceElement frame = new StackTraceElement("java.lang.String", + "getBytes", "String.java", 913); + npe.setStackTrace(new StackTraceElement[]{frame}); + r.setThrown(npe); + CompactFormatter cf = new CompactFormatter(p); + String output = cf.format(r); + assertNotNull(output); + } + + @Test + public void testFormatExample4() { + String p = "%4$s: %12$#.160s%n"; + LogRecord r = new LogRecord(Level.SEVERE, "Unable to send notification."); + r.setSourceClassName("MyClass"); + r.setSourceMethodName("fatal"); + setEpochMilli(r, 1258723764000L); + + Exception t = new SocketException("Permission denied: connect"); + t = new MessagingException("Couldn't connect to host", t); + r.setThrown(t); + CompactFormatter cf = new CompactFormatter(p); + String output = cf.format(r); + assertNotNull(output); + } + + @Test + public void testFormatExample5() { + String p = "[%9$d][%1$tT][%10$d][%2$s] %5$s%n%6$s%n"; + LogRecord r = new LogRecord(Level.SEVERE, "Unable to send notification."); + r.setSequenceNumber(125); + r.setThreadID(38); + r.setSourceClassName("MyClass"); + r.setSourceMethodName("fatal"); + setEpochMilli(r, 1248203502449L); + + Exception t = new SocketException("Permission denied: connect"); + + StackTraceElement frame = new StackTraceElement( + "com.sun.mail.smtp.SMTPTransport", + "openServer", "SMTPTransport.java", 1949); + t.setStackTrace(new StackTraceElement[]{frame}); + + t = new MessagingException("Couldn't connect to host", t); + r.setThrown(t); + CompactFormatter cf = new CompactFormatter(p); + String output = cf.format(r); + assertNotNull(output); + } + + @Test + public void testFormatIllegalPattern() { + CompactFormatter f = new CompactFormatter("%1$#tc"); + try { + f.format(new LogRecord(Level.SEVERE, "")); + fail("Expected format exception."); + } catch (java.util.IllegalFormatException expect) { + } + } + + @Test(expected = NullPointerException.class) + public void testFormatApplyReturnsNull() { + CompactFormatter cf = new ApplyReturnsNull(); + LogRecord r = new LogRecord(Level.SEVERE, ""); + for (int i = 0; i < 10; i++) { + String output = cf.format(r); + assertNotNull(output); + r.setThrown(new Throwable(Integer.toString(i), r.getThrown())); + assertNotNull(cf.format(r)); + } + } + + @Test + public void testFormatBackTrace() { + Exception e = new IOException("Fake I/O"); + e = new Exception(e.toString(), e); + assertNotNull(e.getMessage(), e.getMessage()); + + CompactFormatter cf = new CompactFormatter("%14$s"); + LogRecord record = new LogRecord(Level.SEVERE, ""); + record.setThrown(e); + + String result = cf.formatBackTrace(record); + assertTrue(result, result.startsWith("CompactFormatterTest")); + assertTrue(result, result.contains("testFormatBackTrace")); + assertTrue(result, Character.isDigit(result.charAt(result.length() - 2))); + assertFalse(result, result.contains(".java")); + assertEquals(result, cf.format(record)); + } + + @Test(expected = NullPointerException.class) + public void testBackTraceApplyReturnsNull() { + CompactFormatter cf = new ApplyReturnsNull(); + LogRecord r = new LogRecord(Level.SEVERE, ""); + for (int i = 0; i < 10; i++) { + String output = cf.formatBackTrace(r); + assertNotNull(output); + r.setThrown(new Throwable(Integer.toString(i), r.getThrown())); + assertNotNull(cf.format(r)); + } + } + + @Test + public void testFormatBackTraceUnknown() { + Exception e = new IOException("Fake I/O"); + e.setStackTrace(new StackTraceElement[]{ + new StackTraceElement(CompactFormatterTest.class.getName(), + "testFormatBackTrace", null, -2)}); + assertNotNull(e.getMessage(), e.getMessage()); + + CompactFormatter cf = new CompactFormatter(); + LogRecord record = new LogRecord(Level.SEVERE, ""); + record.setThrown(e); + String result = cf.formatBackTrace(record); + assertTrue(result, result.startsWith("CompactFormatterTest")); + assertTrue(result, result.contains("testFormatBackTrace")); + } + + @Test + public void testFormatBackTracePunt() { + final Class<?> k = Collections.class; + Exception e = new NullPointerException("Fake NPE"); + e.setStackTrace(new StackTraceElement[]{ + new StackTraceElement(k.getName(), "newSetFromMap", null, 3878)}); + assertNotNull(e.getMessage(), e.getMessage()); + + CompactFormatter cf = new CompactFormatter(); + LogRecord record = new LogRecord(Level.SEVERE, ""); + record.setThrown(e); + String result = cf.formatBackTrace(record); + assertTrue(result, result.startsWith(k.getSimpleName())); + assertTrue(result, result.contains("newSetFromMap")); + } + + @Test + public void testFormatBackTraceChainPunt() { + final Class<?> k = Collections.class; + Throwable e = new NullPointerException("Fake NPE"); + e.setStackTrace(new StackTraceElement[0]); + e = new RuntimeException(e); + e.setStackTrace(new StackTraceElement[]{ + new StackTraceElement(k.getName(), "newSetFromMap", null, 3878)}); + assertNotNull(e.getMessage(), e.getMessage()); + + CompactFormatter cf = new CompactFormatter(); + LogRecord record = new LogRecord(Level.SEVERE, ""); + record.setThrown(e); + String result = cf.formatBackTrace(record); + assertTrue(result, result.startsWith(k.getSimpleName())); + assertTrue(result, result.contains("newSetFromMap")); + } + + @Test(expected = NullPointerException.class) + public void testFormatBackTraceNull() { + CompactFormatter cf = new CompactFormatter(); + cf.formatBackTrace((LogRecord) null); + fail(cf.toString()); + } + + @Test(timeout = 30000) + public void testFormatBackTraceEvil() { + LogRecord record = new LogRecord(Level.SEVERE, ""); + record.setThrown(createEvilThrowable()); + CompactFormatter cf = new CompactFormatter(); + cf.formatBackTrace(record); + } + + @Test + public void testApply() { + CompactFormatter cf = new CompactFormatter(); + assertNull(cf.apply((Throwable) null)); + + final Throwable t = new Throwable(); + Throwable e = cf.apply(t); + assertSame(t, e); + } + + @Test + public void testApplyNull() { + assertNull(new CompactFormatter().apply(null)); + } + + @Test(timeout = 30000) + public void testApplyEvil() { + CompactFormatter cf = new CompactFormatter(); + assertNotNull(cf.apply(createEvilThrowable())); + } + + private Throwable createEvilThrowable() { + Throwable third = new Throwable(); + Throwable second = new Throwable(third); + Throwable first = new Throwable(second); + return third.initCause(first); // Pure evil. + } + + @Test(expected = NullPointerException.class) + public void testIgnoreNull() { + CompactFormatter cf = new CompactFormatter(); + cf.ignore((StackTraceElement) null); + fail(cf.toString()); + } + + @Test + public void testIgnoreKnownClass() { + CompactFormatter cf = new CompactFormatter(); + String n = cf.getClass().getName(); + String f = n.concat(".java"); + StackTraceElement s = new StackTraceElement(n, "format", f, 20); + assertFalse(s.toString(), cf.ignore(s)); + } + + @Test + public void testIgnoreUnknownClass() { + CompactFormatter cf = new CompactFormatter(); + String n = UNKNOWN_CLASS_NAME; + String f = n.concat(".java"); + StackTraceElement s = new StackTraceElement(n, "format", f, 20); + assertFalse(s.toString(), cf.ignore(s)); + } + + @Test + public void testIgnorePrivateInnerClass() { + CompactFormatter cf = new CompactFormatter(); + String n = Arrays.asList("foo", "bar", "baz").getClass().getName(); + assertTrue(n, n.contains("$")); + + String f = n.concat(".java"); + StackTraceElement s = new StackTraceElement(n, "size", f, 20); + assertFalse(s.toString(), cf.ignore(s)); + } + + @Test + public void testIgnorePrivateStaticInnerClass() { + CompactFormatter cf = new CompactFormatter(); + String n = Collections.emptySet().getClass().getName(); + assertTrue(n, n.contains("$")); + + String f = n.concat(".java"); + StackTraceElement s = new StackTraceElement(n, "size", f, 20); + assertFalse(s.toString(), cf.ignore(s)); + } + + @Test + public void testIgnoreStaticUtilityClass() { + CompactFormatter cf = new CompactFormatter(); + String n = MimeUtility.class.getName(); + String f = n.concat(".java"); + StackTraceElement s = new StackTraceElement(n, "encodeText", f, 400); + assertTrue(s.toString(), cf.ignore(s)); + } + + @Test + public void testIgnoreStaticUtilityClass_Util() { + CompactFormatter cf = new CompactFormatter(); + String n = getClass().getName().concat("MimeUtility"); + String f = n.concat(".java"); + StackTraceElement s = new StackTraceElement(n, "encodeText", f, 400); + assertTrue(s.toString(), cf.ignore(s)); + } + + @Test + public void testIgnoreStaticUtilityClass_s() { + CompactFormatter cf = new CompactFormatter(); + String n = getClass().getName().concat("Collections"); + String f = n.concat(".java"); + StackTraceElement s = new StackTraceElement(n, "nCopies", f, 400); + assertTrue(s.toString(), cf.ignore(s)); + } + + @Test + public void testIgnoreStaticUtilityClass_es() { + CompactFormatter cf = new CompactFormatter(); + String n = getClass().getName().concat("Properties"); + String f = n.concat(".java"); + StackTraceElement s = new StackTraceElement(n, "get", f, 400); + assertFalse(s.toString(), cf.ignore(s)); + } + + @Test + public void testIgnoreStaticUtilityClass_Throwables() { + CompactFormatter cf = new CompactFormatter(); + String n = getClass().getName().concat("Throwables"); + String f = n.concat(".java"); + StackTraceElement s = new StackTraceElement(n, "propagate", f, 400); + assertTrue(s.toString(), cf.ignore(s)); + } + + @Test + public void testIgnoreSyntheticMethod() { + CompactFormatter cf = new CompactFormatter(); + String n = UNKNOWN_CLASS_NAME; + String f = n.concat(".java"); + StackTraceElement s = new StackTraceElement(n, "access$100", f, 10); + assertTrue(s.toString(), cf.ignore(s)); + } + + @Test + public void testIgnoreNativeMethod() { + CompactFormatter cf = new CompactFormatter(); + String n = UNKNOWN_CLASS_NAME; + String f = n.concat(".java"); + StackTraceElement s = new StackTraceElement(n, "foo", f, -2); + assertTrue(s.toString(), cf.ignore(s)); + } + + @Test + public void testIgnoreReflectMethod() { + CompactFormatter cf = new CompactFormatter(); + String n = java.lang.reflect.Method.class.getName(); + String f = n.concat(".java"); + StackTraceElement s = new StackTraceElement(n, "invoke", f, 10); + assertTrue(s.toString(), cf.ignore(s)); + } + + @Test + public void testIgnoreReflectMethodApiError() { + CompactFormatter cf = new CompactFormatter(); + String n = "java.lang.reflect.".concat(getClass().getName()); + String f = n.concat(".java"); + StackTraceElement s = new StackTraceElement(n, "invoke", f, 10); + assertTrue(s.toString(), cf.ignore(s)); + } + + @Test + public void testIgnoreReflectMethodSunError() { + CompactFormatter cf = new CompactFormatter(); + String n = "sun.reflect.".concat(getClass().getName()); + String f = n.concat(".java"); + StackTraceElement s = new StackTraceElement(n, "invoke", f, 10); + assertTrue(s.toString(), cf.ignore(s)); + } + + @Test + public void testIgnoreReflectConstructor() { + CompactFormatter cf = new CompactFormatter(); + String n = java.lang.reflect.Constructor.class.getName(); + String f = n.concat(".java"); + StackTraceElement s = new StackTraceElement(n, "newInstance", f, 10); + assertTrue(s.toString(), cf.ignore(s)); + } + + @Test + public void testIgnoreUnknownLine() { + CompactFormatter cf = new CompactFormatter(); + String n = UNKNOWN_CLASS_NAME; + StackTraceElement s = new StackTraceElement(n, "foo", null, -1); + assertTrue(s.toString(), cf.ignore(s)); + } + + @Test + public void testToAlternate() { + CompactFormatter cf = new CompactFormatter(); + assertEquals("", cf.toAlternate(LINE_SEP)); + } + + @Test + public void testToAlternateNull() { + CompactFormatter cf = new CompactFormatter(); + assertNull(cf.toAlternate((String) null)); + } + + @Test + public void testJavaMailLinkage() throws Exception { + testJavaMailLinkage(CompactFormatter.class); + } + + @Test + public void testLogManagerModifiers() throws Exception { + testLogManagerModifiers(CompactFormatter.class); + } + + @Test + public void testWebappClassLoaderFieldNames() throws Exception { + testWebappClassLoaderFieldNames(CompactFormatter.class); + } + + private static String rpad(String s, int len, String p) { + if (s.length() < len) { + StringBuilder sb = new StringBuilder(len); + sb.append(s); + for (int i = sb.length(); i < len; ++i) { + sb.append(p, 0, 1); + } + return sb.toString(); + } else { + return s; + } + } + + private final static class CountLocalizedException extends RuntimeException { + + private static final long serialVersionUID = 1L; + public int localizedMessage; + + CountLocalizedException() { + super(); + } + + @Override + public String getLocalizedMessage() { + localizedMessage++; + return super.getLocalizedMessage(); + } + } + + private final static class ToStringException extends RuntimeException { + + private static final long serialVersionUID = 1L; + private final String toString; + + ToStringException(String toString, String msg) { + super(msg); + this.toString = toString; + } + + @Override + public String toString() { + return toString; + } + } + + private final static class PrefixException extends RuntimeException { + + private static final long serialVersionUID = 1L; + private final String prefix; + + PrefixException(String prefix, String msg, Throwable cause) { + super(msg, cause); + this.prefix = prefix; + } + + @Override + public String toString() { + String message = getLocalizedMessage(); + return (message != null) ? (prefix + ": " + message) : prefix; + } + } + + /** + * An example of a broken implementation of thread ID. + */ + private static class ThreadIDReturnsNull extends CompactFormatter { + + /** + * Promote access level. + */ + ThreadIDReturnsNull() { + super("%10$d"); + } + + @Override + public Number formatThreadID(LogRecord record) { + return null; + } + } + + private static class InheritsFormatMessage extends CompactFormatter { + + InheritsFormatMessage() { + } + + @Override + public String formatMessage(Throwable t) { + return InheritsFormatMessage.class.getName(); + } + } + + /** + * An example of a broken implementation of apply. + */ + private static class ApplyReturnsNull extends CompactFormatter { + + /** + * The number of throwables. + */ + private int count; + + /** + * Promote access level. + */ + ApplyReturnsNull() { + super("%6$s%11$s%14$s%7$#.160s%8$#.160s%12$#.160s%13$#.160s"); + } + + @Override + protected Throwable apply(Throwable t) { + return (++count & 1) == 1 ? t : null; + } + } + + /** + * A properties resource bundle with locale. + */ + private static class LocaleResource extends PropertyResourceBundle { + + /** + * The locale + */ + private final Locale locale; + + /** + * Creates the locale resource. + * + * @param p the properties. + * @param l the locale. + * @throws IOException if there is a problem. + */ + LocaleResource(Properties p, Locale l) throws IOException { + super(toStream(p)); + locale = l; + } + + private static InputStream toStream(Properties p) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + p.store(out, LocaleResource.class.getName()); + return new ByteArrayInputStream(out.toByteArray()); + } + + @Override + public Locale getLocale() { + return locale; + } + } +}
diff --git a/mail/src/test/java/com/sun/mail/util/logging/DurationFilterTest.java b/mail/src/test/java/com/sun/mail/util/logging/DurationFilterTest.java new file mode 100644 index 0000000..c59682e --- /dev/null +++ b/mail/src/test/java/com/sun/mail/util/logging/DurationFilterTest.java
@@ -0,0 +1,747 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2018 Jason Mehrens. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package com.sun.mail.util.logging; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.Properties; +import java.util.logging.*; +import org.junit.*; +import static org.junit.Assert.*; + +/** + * Test case for the DurationFilter spec. + * + * @author Jason Mehrens + */ +public class DurationFilterTest extends AbstractLogging { + + public DurationFilterTest() { + } + + @BeforeClass + public static void setUpClass() { + checkJVMOptions(); + } + + @AfterClass + public static void tearDownClass() { + checkJVMOptions(); + } + + private static void checkJVMOptions() { + assertTrue(DurationFilterTest.class.desiredAssertionStatus()); + assertNull(System.getProperty("java.util.logging.manager")); + assertNull(System.getProperty("java.util.logging.config.class")); + assertNull(System.getProperty("java.util.logging.config.file")); + assertEquals(LogManager.class, LogManager.getLogManager().getClass()); + } + + @Test + public void testDeclaredClasses() throws Exception { + Class<?>[] declared = DurationFilter.class.getDeclaredClasses(); + assertEquals(Arrays.toString(declared), 0, declared.length); + } + + @Test + public void testClone() throws Exception { + DurationFilterExt source = new DurationFilterExt(); + final Filter clone = source.clone(); + assertNotNull(clone); + assertFalse(source == clone); + assertTrue(source.equals(clone)); + assertEquals(source.getClass(), clone.getClass()); + + LogRecord r = new LogRecord(Level.INFO, ""); + assertTrue(source.isLoggable(r)); + assertFalse(source.equals(clone)); + assertTrue(((DurationFilterExt) clone).clone().equals(clone)); + } + + @Test + public void testCloneState() throws Exception { + long millis = 0; + final int records = 10; + final int duration = 5 * 60 * 1000; + Level lvl = Level.INFO; + DurationFilterExt sf = new DurationFilterExt(records, duration); + String msg = Long.toString(millis); + LogRecord r = new LogRecord(lvl, msg); + + //Allow + for (int i = 0; i < records; i++) { + setEpochMilli(r, millis); + assertTrue(Integer.toString(i), sf.isLoggable(r)); + } + + Filter clone = sf.clone(); + for (int i = 0; i < records; i++) { + setEpochMilli(r, millis); + String m = Integer.toString(i); + assertFalse(m, sf.isLoggable(r)); + assertTrue(m, clone.isLoggable(r)); + } + + assertFalse(sf.isLoggable(r)); + assertFalse(clone.isLoggable(r)); + } + + @Test(timeout = 15000) + public void testIsLoggableNow() throws Exception { + final int records = 10; + final int duration = 1000; + Level lvl = Level.INFO; + DurationFilter sf = new DurationFilter(records, duration); + assertTrue(sf.isLoggable()); + + LogRecord r = new LogRecord(lvl, ""); + assertTrue(sf.isLoggable(r)); + assertTrue(sf.isLoggable()); + + //Allow + for (int i = 1; i < records; i++) { + r = new LogRecord(lvl, ""); + String msg = Integer.toString(i); + assertTrue(msg, sf.isLoggable()); + assertTrue(msg, sf.isLoggable(r)); + } + + assertFalse(sf.isLoggable()); + assertFalse(sf.isLoggable(r)); + + tickMilli(duration + 100); //Cool down and allow. + + for (int i = 0; i < records; i++) { + r = new LogRecord(lvl, ""); + String msg = Integer.toString(i); + assertTrue(msg, sf.isLoggable()); + assertTrue(msg, sf.isLoggable(r)); + } + + assertFalse(sf.isLoggable()); + assertFalse(sf.isLoggable(r)); + } + + @Test(timeout = 15000) + public void testIsIdleNow() throws Exception { + final int records = 10; + final int duration = 1000; + Level lvl = Level.INFO; + DurationFilter sf = new DurationFilter(records, duration); + LogRecord r = new LogRecord(lvl, ""); + assertTrue(sf.isIdle()); + assertTrue(sf.isLoggable(r)); + assertFalse(sf.isIdle()); + + //Allow + for (int i = 1; i < records; i++) { + r = new LogRecord(lvl, ""); + String msg = Integer.toString(i); + assertFalse(msg, sf.isIdle()); + assertTrue(msg, sf.isLoggable(r)); + } + + assertFalse(sf.isIdle()); + assertFalse(sf.isLoggable(r)); + + tickMilli(duration + 100); //Cool down and allow. + + assertTrue(sf.isIdle()); + for (int i = 0; i < records; i++) { + r = new LogRecord(lvl, ""); + String msg = Integer.toString(i); + assertTrue(msg, sf.isLoggable(r)); + assertFalse(msg, sf.isIdle()); + } + + assertFalse(sf.isIdle()); + assertFalse(sf.isLoggable(r)); + } + + @Test + public void testSaturation() { + long millis = 0; + final int records = 10; + final int duration = 5 * 60 * 1000; + Level lvl = Level.INFO; + DurationFilter sf = new DurationFilter(records, duration); + LogRecord r; + + //Allow + for (int i = 0; i < records; i++) { + ++millis; + r = new LogRecord(lvl, Long.toString(millis)); + setEpochMilli(r, millis); + assertTrue(Integer.toString(i), sf.isLoggable(r)); + } + + //Saturate. + for (int i = 0; i < records * 10; i++) { + r = new LogRecord(lvl, Long.toString(millis)); + setEpochMilli(r, millis); + assertFalse(Integer.toString(i), sf.isLoggable(r)); + } + + //Cool down and allow. + millis += duration; + for (int i = 0; i < records; i++) { + ++millis; + r = new LogRecord(lvl, Long.toString(millis)); + setEpochMilli(r, millis); + assertTrue(Integer.toString(i), sf.isLoggable(r)); + } + } + + @Test + public void testSaturateIntergral() { + long duration = 15L * 60L * 1000L; + for (long i = 0; i <= duration * 2; i++) { + testSaturateIntergral(i, duration); + } + } + + private void testSaturateIntergral(long millis, long duration) { + final int records = 10; + Level lvl = Level.INFO; + DurationFilter sf = new DurationFilter(records, duration); + LogRecord r = new LogRecord(lvl, ""); + sf.isLoggable(r); //Init the duration. + millis += (2 * duration) - 1; + for (int i = 0; i < records - 2; i++) { + setEpochMilli(r, millis); + assertTrue(Integer.toString(i), sf.isLoggable(r)); + } + + millis += 100; + setEpochMilli(r, millis); + assertTrue(sf.isLoggable(r)); + + for (int i = 0; i < records - 1; i++) { + setEpochMilli(r, millis); + assertFalse(Integer.toString(i), sf.isLoggable(r)); + } + } + + @Test + public void testSaturatePositiveOutOfOrder() { + testSaturateOutOfOrder(Integer.MAX_VALUE); + } + + @Test + public void testSaturateNegativeOutOfOrder() { + testSaturateOutOfOrder(-Integer.MAX_VALUE); + } + + @Test + public void testSaturateOverFlowOutOfOrder() { + testSaturateOutOfOrder(Long.MAX_VALUE); + } + + @Test + public void testSaturateUnderFlowOutOfOrder() { + testSaturateOutOfOrder(-Long.MAX_VALUE); + } + + public void testSaturateOutOfOrder(long millis) { + final int records = 10; + final int duration = 5 * 60 * 1000; + Level lvl = Level.INFO; + DurationFilter sf = new DurationFilter(records, duration); + LogRecord r; + + //Allow + for (int i = 0; i < records; i++) { + r = new LogRecord(lvl, Long.toString(millis)); + setEpochMilli(r, millis); + assertTrue(Integer.toString(i), sf.isLoggable(r)); + --millis; + } + + //Still saturated. + millis += duration; + final long peak = millis; + for (int i = 0; i < records; i++) { + r = new LogRecord(lvl, Long.toString(millis)); + setEpochMilli(r, millis); + assertFalse(Integer.toString(i), sf.isLoggable(r)); + } + + //Cool down and allow. + millis = peak + duration; + for (int i = 0; i < records; i++) { + r = new LogRecord(lvl, Long.toString(millis)); + setEpochMilli(r, millis); + assertTrue(Integer.toString(i), sf.isLoggable(r)); + ++millis; + } + } + + @Test + public void testTimeSpringShort() { + testClockAdjustment(5 * 60 * 1000, 1); + } + + @Test + public void testTimeSpringLong() { + testClockAdjustment(24 * 60 * 60 * 1000, 1); + } + + @Test + public void testTimeFallShort() { + testClockAdjustment(5 * 60 * 1000, -1); + } + + @Test + public void testTimeFallLong() { + testClockAdjustment(24 * 60 * 60 * 1000, -1); + } + + private void testClockAdjustment(int records, int signum) { + assertFalse(0 == signum); + assertEquals(Integer.signum(signum), signum); + long millis = 0L; + DurationFilter sf = new DurationFilter(records, records); + LogRecord r = new LogRecord(Level.INFO, ""); + for (int i = 1; i < (records / 2); i++) { + setEpochMilli(r, ++millis); + assertTrue(sf.isLoggable(r)); + } + + millis += signum * (60L * 60L * 1000L); + for (int i = (records / 2); i <= records; i++) { + setEpochMilli(r, ++millis); + assertTrue(sf.isLoggable(r)); + } + } + + @Test + public void testPredictedOverflow() { + int records = 4; + int duration = 4; + DurationFilter sf = new DurationFilter(records, duration); + for (int i = 0; i < records; i++) { + LogRecord r = new LogRecord(Level.INFO, ""); + setEpochMilli(r, Long.MAX_VALUE); + assertTrue(sf.isLoggable(r)); + } + + LogRecord r = new LogRecord(Level.INFO, ""); + setEpochMilli(r, Long.MAX_VALUE); + assertFalse(sf.isLoggable(r)); + + r = new LogRecord(Level.INFO, ""); + setEpochMilli(r, Long.MAX_VALUE + duration); + assertTrue(sf.isLoggable(r)); + } + + @Test + public void testMillisNegativeSaturation() { + int records = 4; + int duration = 4; + DurationFilter sf = new DurationFilter(records, duration); + for (int i = 0; i < records; i++) { + LogRecord r = new LogRecord(Level.INFO, ""); + setEpochMilli(r, Long.MIN_VALUE); + assertTrue(Integer.toString(i), sf.isLoggable(r)); + } + + LogRecord r = new LogRecord(Level.INFO, ""); + setEpochMilli(r, Long.MIN_VALUE); + assertFalse(sf.isLoggable(r)); + + r = new LogRecord(Level.INFO, ""); + setEpochMilli(r, Long.MIN_VALUE + duration); + assertTrue(sf.isLoggable(r)); + } + + @Test + public void testExactRate() throws Exception { + long millis = System.currentTimeMillis(); + final int records = 1000; + final int duration = 5 * 60 * 1000; + Level lvl = Level.INFO; + DurationFilter sf = new DurationFilter(records, duration); + LogRecord r; + + int period = duration / records; + assertEquals(period, (double) duration / (double) records, 0.0); + for (int i = 0; i < records * records; i++) { + r = new LogRecord(lvl, Long.toString(millis)); + setEpochMilli(r, millis); + assertTrue(Integer.toString(i), sf.isLoggable(r)); + millis += period; + } + } + + @Test + public void testCeilRate() throws Exception { + double millis = 0L; + final int records = 3; + final int duration = 40; + Level lvl = Level.INFO; + DurationFilter sf = new DurationFilter(records, duration); + LogRecord r; + + double period = duration / (double) records; + for (int i = 0; i < (duration * records) * 2; i++) { + r = new LogRecord(lvl, Double.toString(millis)); + setEpochMilli(r, (long) millis); + assertTrue(Integer.toString(i), sf.isLoggable(r)); + millis = millis + Math.ceil(period); + } + } + + @Test + public void testFloorRate() { + double millis = 0.0d; + final int records = 30; + final int duration = 400; + Level lvl = Level.INFO; + DurationFilter sf = new DurationFilter(records, duration); + LogRecord r; + long period = duration / records; + for (int i = 0; i < records; i++) { + r = new LogRecord(lvl, Long.toString((long) millis)); + setEpochMilli(r, (long) millis); + assertTrue(Integer.toString(i), sf.isLoggable(r)); + millis += period; + } + + //Saturated for records + one. + for (int i = 0; i <= records; i++) { + r = new LogRecord(lvl, Long.toString((long) millis)); + setEpochMilli(r, (long) millis); + assertFalse(Integer.toString(i), sf.isLoggable(r)); + millis += period; + } + + for (int i = 0; i < records; i++) { + r = new LogRecord(lvl, Long.toString((long) millis)); + setEpochMilli(r, (long) millis); + assertTrue(Integer.toString(i), sf.isLoggable(r)); + millis += period; + } + } + + private void testRate(long millis, long records, long duration) { + Level lvl = Level.INFO; + DurationFilter sf = new DurationFilter(records, duration); + LogRecord r = new LogRecord(lvl, Long.toString(millis)); + + for (long i = 0; i < records; i++) { + setEpochMilli(r, millis); + assertTrue(sf.isLoggable(r)); + } + + r = new LogRecord(lvl, Long.toString(millis)); + setEpochMilli(r, millis); + assertFalse(sf.isLoggable(r)); + } + + @Test + public void testOneTenthErrorRate() { + testRate(0, 10, 1); + } + + @Test + public void testOneHundredthErrorRate() { + testRate(0, 100, 1); + } + + @Test + public void testOneThousanthErrorRate() { + testRate(0, 1000, 1); + } + + @Test + public void testOneMillionthErrorRate() { + testRate(0, 1000000, 1); + } + + @Test + public void testTwoToThe53rdRate() { + testRate(0, 1, 1L << 53L); + } + + @Ignore + public void testIntegerMaxValueByTenRate() { + /** + * This can take a few minutes to run. + */ + testRate(0, Integer.MAX_VALUE, 10); + } + + @Test(expected = NullPointerException.class) + public void testIsLoggableNull() { + new DurationFilter().isLoggable((LogRecord) null); + } + + @Test + public void testEquals() { + DurationFilter one = new DurationFilter(); + DurationFilter two = new DurationFilter(); + assertTrue(one.equals(one)); + assertTrue(two.equals(two)); + assertTrue(one.equals(two)); + assertTrue(two.equals(one)); + + LogRecord r = new LogRecord(Level.INFO, ""); + assertTrue(one.isLoggable(r)); + assertTrue(one.equals(one)); + assertTrue(two.equals(two)); + assertFalse(one.equals(two)); + assertFalse(two.equals(one)); + assertFalse(one.equals((Object) null)); + assertFalse(two.equals((Object) null)); + } + + @Test + public void testHashCode() { + DurationFilter one = new DurationFilter(10, 10); + DurationFilter two = new DurationFilter(10, 10); + DurationFilter three = new DurationFilter(3, 3); + + assertTrue(one.hashCode() == two.hashCode()); + assertFalse(one.hashCode() == three.hashCode()); + + LogRecord r = new LogRecord(Level.INFO, ""); + assertTrue(one.isLoggable(r)); + assertTrue(one.hashCode() == two.hashCode()); + assertFalse(one.hashCode() == three.hashCode()); + } + + @Test + public void testToString() { + testToString(new DurationFilter()); + } + + @Test + public void testToStringEx() { + testToString(new DurationFilterExt()); + } + + private void testToString(DurationFilter f) { + String s = f.toString(); + assertTrue(s.startsWith(f.getClass().getName())); + assertTrue(s.contains("records=")); + assertTrue(s.contains("duration=")); + assertTrue(s.contains("idle=")); + assertTrue(s.contains("loggable=")); + } + + @Test + public void testJavaMailLinkage() throws Exception { + testJavaMailLinkage(DurationFilter.class); + } + + @Test + public void testLogManagerModifiers() throws Exception { + testLogManagerModifiers(DurationFilter.class); + } + + @Test + public void testWebappClassLoaderFieldNames() throws Exception { + testWebappClassLoaderFieldNames(DurationFilter.class); + } + + @Test + public void testInitRecords() throws Exception { + testInitRecords("210", 210); + } + + @Test + public void testInitRecordsZero() throws Exception { + testInitRecords("0", 1000); + } + + @Test + public void testInitRecordsNegative() throws Exception { + testInitRecords("-1", 1000); + } + + @Test + public void testInitRecordIso8601() throws Exception { + if (hasJavaTimeModule()) { + testInitRecords("PT30M", 1000); + } + } + + @Test + public void testInitDuration() throws Exception { + testInitDuration("1024", 1024); + } + + @Test + public void testInitDurationZero() throws Exception { + testInitDuration("0", 15L * 60L * 1000L); + } + + @Test + public void testInitDurationNegative() throws Exception { + testInitDuration("-1", 15L * 60L * 1000L); + } + + @Test + public void testInitDurationExp() throws Exception { + testInitDuration("15 * 60 * 1000", 15L * 60L * 1000L); + testInitDuration("15*60*1000", 15L * 60L * 1000L); + testInitDuration("15L * 60L * 1000L", 15L * 60L * 1000L); + testInitDuration("15L*60L*1000L", 15L * 60L * 1000L); + } + + @Test + public void testInitDurationExpLifetime() throws Exception { + testInitDuration("125L * 366 * 24L * 60L * 60L * 1000L", + 125L * 366 * 24L * 60L * 60L * 1000L); + } + + @Test + public void testInitDurationExpOverflow() throws Exception { + testInitDuration(Long.MAX_VALUE + " * " + + Long.MAX_VALUE, 15L * 60L * 1000L); + } + + @Test + public void testInitDurationExpAlpha() throws Exception { + testInitDuration("15LL * 60 * 1000", 15L * 60L * 1000L); + } + + @Test + public void testInitDurationExpTooManyMult() throws Exception { + testInitDuration("15L ** 60 ** 1000", 15L * 60L * 1000L); + } + + @Test + public void testInitDurationExpTrailing() throws Exception { + testInitDuration("15 * 60 * 1000*", 15L * 60L * 1000L); + } + + @Test + public void testInitDurationExpSpace() throws Exception { + testInitDuration("15 * 60 * 1000* ", 15L * 60L * 1000L); + } + + @Test + public void testInitDurationExpLeading() throws Exception { + testInitDuration("*15 * 60 * 1000", 15L * 60L * 1000L); + } + + @Test + public void testInitDurationExpAdd() throws Exception { + testInitDuration("15 + 60 + 1000", 15L * 60L * 1000L); + } + + @Test + public void testInitDurationExpDivide() throws Exception { + testInitDuration("15 / 60 / 1000", 15L * 60L * 1000L); + } + + @Test + public void testInitDurationExpSubstract() throws Exception { + testInitDuration("15 - 60 - 1000", 15L * 60L * 1000L); + } + + @Test + public void testInitNegativeDuration() throws Exception { + testInitDuration("-1024", 15L * 60L * 1000L); + } + + @Test + public void testInitDurationIso8601Ms() throws Exception { + if (hasJavaTimeModule()) { + testInitDuration("PT0.345S", 345); + } + } + + @Test + public void testInitDurationIso8601Sec() throws Exception { + if (hasJavaTimeModule()) { + testInitDuration("PT20.345S", (20L * 1000L) + 345); + } + } + + @Test + public void testInitDurationIso8601Min() throws Exception { + if (hasJavaTimeModule()) { + testInitDuration("PT30M", 30L * 60L * 1000L); + } + } + + @Test + public void testInitDurationIso8601Hour() throws Exception { + if (hasJavaTimeModule()) { + testInitDuration("PT10H", 10L * 60L * 60L * 1000L); + } + } + + @Test + public void testInitDurationIso8601Day() throws Exception { + if (hasJavaTimeModule()) { + testInitDuration("P2D", 2L * 24L * 60L * 60L * 1000L); + } + } + + @Test + public void testInitDurationIso8601All() throws Exception { + if (hasJavaTimeModule()) { + testInitDuration("P2DT3H4M20.345S", (2L * 24L * 60L * 60L * 1000L) + + (3L * 60L * 60L * 1000L) + (4L * 60L * 1000L) + + ((20L * 1000L) + 345)); + } + } + + private void testInitDuration(String d, long expect) throws Exception { + testInit("duration", d, expect); + } + + private void testInitRecords(String r, long expect) throws Exception { + testInit("records", r, expect); + } + + private void testInit(String field, String value, long expect) throws Exception { + String p = DurationFilter.class.getName(); + Properties props = new Properties(); + props.put(p + '.' + field, value); + LogManager m = LogManager.getLogManager(); + try { + read(m, props); + DurationFilter sf = new DurationFilter(); + Field f = DurationFilter.class.getDeclaredField(field); + f.setAccessible(true); + assertEquals(expect, f.get(sf)); + } finally { + m.reset(); + } + } + + public static final class DurationFilterExt extends DurationFilter + implements Cloneable { + + public DurationFilterExt() { + super(); + } + + public DurationFilterExt(long records, long duration) { + super(records, duration); + } + + @Override + public DurationFilterExt clone() throws CloneNotSupportedException { + return (DurationFilterExt) super.clone(); + } + } +}
diff --git a/mail/src/test/java/com/sun/mail/util/logging/LogManagerPropertiesTest.java b/mail/src/test/java/com/sun/mail/util/logging/LogManagerPropertiesTest.java new file mode 100644 index 0000000..9072eee --- /dev/null +++ b/mail/src/test/java/com/sun/mail/util/logging/LogManagerPropertiesTest.java
@@ -0,0 +1,1380 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2009, 2018 Jason Mehrens. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package com.sun.mail.util.logging; + +import java.io.*; +import java.lang.management.CompilationMXBean; +import java.lang.management.ManagementFactory; +import java.lang.reflect.*; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.TimeUnit; +import java.util.logging.*; +import javax.mail.*; +import javax.mail.internet.InternetAddress; +import org.junit.*; +import static org.junit.Assert.*; + +/** + * Test case for the LogManagerProperties spec. + * + * @author Jason Mehrens + */ +public class LogManagerPropertiesTest extends AbstractLogging { + + /** + * Holder used to inject Throwables into other APIs. + */ + private final static ThreadLocal<Throwable> PENDING = new ThreadLocal<>(); + + @BeforeClass + public static void setUpClass() throws Exception { + Assert.assertNull(System.getSecurityManager()); + } + + private static void fullFence() { + LogManager.getLogManager().getProperty(""); + } + + private static void assumeNoJit() { + CompilationMXBean c = ManagementFactory.getCompilationMXBean(); + if (c != null) { //-Xint + Assume.assumeNoException(new IllegalArgumentException( + c.getName() + " must be disabled.")); + } + } + + @Before + public void setUp() { + fullFence(); + } + + @After + public void tearDown() { + fullFence(); + } + + @Test + public void testDeclaredClasses() throws Exception { + Class<?>[] declared = LogManagerProperties.class.getDeclaredClasses(); + assertEquals(Arrays.toString(declared), 0, declared.length); + } + + @Test + public void testCheckAccessPresent() { + LogManager m = LogManager.getLogManager(); + m.checkAccess(); + LogManagerProperties.checkLogManagerAccess(); + + LogPermSecurityManager sm = new LogPermSecurityManager(); + sm.secure = false; + System.setSecurityManager(sm); + try { + sm.secure = true; + try { + m.checkAccess(); + fail(m.toString()); + } catch (SecurityException expect) { + } + + try { + LogManagerProperties.checkLogManagerAccess(); + fail(LogManagerProperties.class.getName()); + } catch (SecurityException expect) { + } + } finally { + sm.secure = false; + System.setSecurityManager((SecurityManager) null); + } + } + + @Ignore + public void testCheckAccessAbsent() throws Exception { + assumeNoJit(); + final Class<?> k = LogManagerProperties.class; + final Field f = k.getDeclaredField("LOG_MANAGER"); + Field mod = setAccessible(f); + try { + final Object lm = f.get(null); + f.set(null, null); + try { + fullFence(); + LogManagerProperties.checkLogManagerAccess(); + + LogPermSecurityManager sm = new LogPermSecurityManager(); + sm.secure = false; + System.setSecurityManager(sm); + try { + sm.secure = true; + try { + LogManagerProperties.checkLogManagerAccess(); + fail(LogManagerProperties.class.getName()); + } catch (SecurityException expect) { + } + } finally { + sm.secure = false; + System.setSecurityManager((SecurityManager) null); + } + } finally { + f.set(null, lm); + fullFence(); + } + } finally { + mod.setInt(f, f.getModifiers() | Modifier.FINAL); + } + } + + @Test + public void testFromLogManagerPresent() throws Exception { + String prefix = LogManagerPropertiesTest.class.getName(); + LogManager manager = LogManager.getLogManager(); + try { + String key = prefix.concat(".dummy"); + String value = "value"; + String emptyValue = "empty"; + Properties parent = new Properties(); + parent.put(key, value); + parent.put("", emptyValue); + + read(manager, parent); + assertTrue(LogManagerProperties.hasLogManager()); + assertEquals(value, LogManagerProperties.fromLogManager(key)); + assertEquals(emptyValue, LogManagerProperties.fromLogManager("")); + } finally { + manager.reset(); + } + + try { + LogManagerProperties.fromLogManager((String) null); + fail(""); + } catch (NullPointerException expect) { + } + } + + @Ignore + public void testFromLogManagerNull() throws Exception { + assumeNoJit(); + testFromLogManager((Properties) null); + } + + @Ignore + public void testFromLogManagerAbsent() throws Exception { + assumeNoJit(); + final String cfgKey = "java.util.logging.config.file"; + final Class<?> k = LogManagerProperties.class; + String old = System.getProperty(cfgKey); + try { + Properties props = new Properties(); + props.put("", "empty"); + props.put(k.getName().concat(".dummy"), "value"); + final File f = File.createTempFile(k.getName(), ".properties"); + try { + try (FileOutputStream out = new FileOutputStream(f)) { + props.store(out, "testFromLogManagerAbsent"); + } + System.setProperty(cfgKey, f.getAbsolutePath()); + final Method m = k.getDeclaredMethod("readConfiguration"); + assertTrue(Modifier.isPrivate(m.getModifiers())); + m.setAccessible(true); + props = (Properties) m.invoke(null); + testFromLogManager(props); + } finally { + assertTrue(f.toString(), f.delete() || !f.exists()); + } + } finally { + if (old != null) { + System.setProperty(cfgKey, old); + } else { + System.clearProperty(cfgKey); + } + } + } + + private void testFromLogManager(Properties parent) throws Exception { + assertTrue(LogManagerProperties.hasLogManager()); + final Class<?> k = LogManagerProperties.class; + final Field f = k.getDeclaredField("LOG_MANAGER"); + Field mod = setAccessible(f); + try { + fullFence(); + final Object lm = f.get(null); + f.set(null, parent); + try { + fullFence(); + assertFalse(LogManagerProperties.hasLogManager()); + if (parent != null) { + assertFalse(parent.isEmpty()); + for (Map.Entry<Object, Object> e : parent.entrySet()) { + String key = e.getKey().toString(); + String val = LogManagerProperties.fromLogManager(key); + assertEquals(e.getValue(), val); + } + } else { + assertNull(LogManagerProperties.fromLogManager("")); + assertNull(LogManagerProperties.fromLogManager("val")); + } + + try { + LogManagerProperties.fromLogManager((String) null); + fail(""); + } catch (NullPointerException expect) { + } + } finally { + f.set(null, lm); + fullFence(); + } + } finally { + mod.setInt(f, f.getModifiers() | Modifier.FINAL); + fullFence(); + } + } + + @Test + public void testJavaMailLinkage() throws Exception { + testJavaMailLinkage(LogManagerProperties.class); + } + + @Test + public void testWebappClassLoaderFieldNames() throws Exception { + testWebappClassLoaderFieldNames(LogManagerProperties.class); + } + + @Test + public void testClone() throws Exception { + String prefix = LogManagerPropertiesTest.class.getName(); + Properties parent; + LogManagerProperties mp; + LogManager manager = LogManager.getLogManager(); + try { + String key = prefix.concat(".dummy"); + parent = new Properties(); + parent.put(key, "value"); + + read(manager, parent); + + parent = new Properties(); + mp = new LogManagerProperties(parent, prefix); + + assertFalse(contains(mp, key, null)); + assertEquals("value", mp.getProperty(key)); + assertTrue(contains(mp, key, "value")); //ensure copy worked. + } finally { + manager.reset(); + } + + Properties clone = (Properties) mp.clone(); + assertFalse(clone instanceof LogManagerProperties); + assertEquals(Properties.class, clone.getClass()); + assertNotSame(clone, parent); + assertNotSame(clone, mp); + assertEquals(mp.size(), clone.size()); + assertTrue(clone.equals(mp)); //don't call mp.equals. + } + + @Test + public void testIsReflection() throws Exception { + assertTrue(LogManagerProperties.isReflectionClass(Constructor.class.getName())); + assertTrue(LogManagerProperties.isReflectionClass(Method.class.getName())); + } + + @Test + public void testIsStaticUtilityClass() throws Exception { + boolean nullCheck; + try { + LogManagerProperties.isStaticUtilityClass((String) null); + nullCheck = false; + } catch (NullPointerException expect) { + nullCheck = true; + } + + if (!nullCheck) { + fail("Null check"); + } + + String[] utils = { + "java.lang.System", + "java.nio.channels.Channels", + "java.util.Collections", + "javax.mail.internet.MimeUtility", + "org.junit.Assert" + }; + + testIsStaticUtilityClass(utils, true); + + String[] obj = { + "java.lang.Exception", + "java.lang.Object", + "java.lang.Runtime", + "java.io.Serializable" + }; + testIsStaticUtilityClass(obj, false); + + String[] enumerations = { + "java.util.concurrent.TimeUnit" + }; + testIsStaticUtilityClass(enumerations, false); + + String[] fail = { + "badClassName" + }; + for (String name : fail) { + boolean pass; + try { + LogManagerProperties.isStaticUtilityClass(name); + pass = false; + } catch (ClassNotFoundException expect) { + pass = true; + } + + if (!pass) { + fail(name); + } + } + } + + private void testIsStaticUtilityClass(String[] names, boolean complement) throws Exception { + assertFalse(names.length == 0); + + if (complement) { + for (String name : names) { + assertTrue(name, LogManagerProperties.isStaticUtilityClass(name)); + } + } else { + for (String name : names) { + assertFalse(name, LogManagerProperties.isStaticUtilityClass(name)); + } + } + } + + @Test + public void testGetLocalHost() throws Exception { + String host = LogManagerPropertiesTest.class.getName(); + Properties p = new Properties(); + p.setProperty("mail.smtp.localhost", host); + Session s = Session.getInstance(p); + Transport t = s.getTransport(InternetAddress.getLocalAddress(s)); + try { + String h = LogManagerProperties.getLocalHost(t); + if (h != null || isPrivateSpec(t.getClass())) { + Assert.assertEquals(host, h); + } + } catch (NoSuchMethodException notOfficial) { + if (isPrivateSpec(t.getClass())) { + fail(t.toString()); + } + } + } + + @Test + public void testGetLocalHostMissing() throws Exception { + Session session = Session.getInstance(new Properties()); + Service svc = new NoHostService(session); + try { + LogManagerProperties.getLocalHost(svc); + fail(""); + } catch (NoSuchMethodException expect) { + } + } + + @Test + public void testGetLocalHostSecure() throws Exception { + Session session = Session.getInstance(new Properties()); + Service svc = new NotAllowedService(session); + try { + LogManagerProperties.getLocalHost(svc); + } catch (SecurityException allowed) { + } catch (InvocationTargetException expect) { + Assert.assertTrue(expect.getCause() instanceof SecurityException); + } + } + + @Test + public void testGetLocalHostNull() throws Exception { + boolean fail = true; + try { + LogManagerProperties.getLocalHost((Service) null); + } catch (NullPointerException expected) { + fail = false; + } + Assert.assertFalse(fail); + } + + @Test + public void testParseDurationMs() throws Exception { + try { + long ms = LogManagerProperties.parseDurationToMillis("PT0.345S"); + assertEquals(345L, ms); + } catch (ClassNotFoundException | NoClassDefFoundError ignore) { + assertFalse(ignore.toString(), hasJavaTimeModule()); + } + } + + @Test + public void testParseDurationSec() throws Exception { + try { + long ms = LogManagerProperties.parseDurationToMillis("PT20.345S"); + assertEquals((20L * 1000L) + 345L, ms); + } catch (ClassNotFoundException | NoClassDefFoundError ignore) { + assertFalse(ignore.toString(), hasJavaTimeModule()); + } + } + + @Test + public void testParseDurationMin() throws Exception { + try { + long ms = LogManagerProperties.parseDurationToMillis("PT15M"); + assertEquals(15L * 60L * 1000L, ms); + } catch (ClassNotFoundException | NoClassDefFoundError ignore) { + assertFalse(ignore.toString(), hasJavaTimeModule()); + } + } + + @Test + public void testParseDurationHour() throws Exception { + try { + long ms = LogManagerProperties.parseDurationToMillis("PT10H"); + assertEquals(10L * 60L * 60L * 1000L, ms); + } catch (ClassNotFoundException | NoClassDefFoundError ignore) { + assertFalse(ignore.toString(), hasJavaTimeModule()); + } + } + + @Test + public void testParseDurationDay() throws Exception { + try { + long ms = LogManagerProperties.parseDurationToMillis("P2D"); + assertEquals(2L * 24L * 60L * 60L * 1000L, ms); + } catch (ClassNotFoundException | NoClassDefFoundError ignore) { + assertFalse(ignore.toString(), hasJavaTimeModule()); + } + } + + @Test + public void testParseDurationAll() throws Exception { + try { + long ms = LogManagerProperties + .parseDurationToMillis("P2DT3H4M20.345S"); + assertEquals((2L * 24L * 60L * 60L * 1000L) + + (3L * 60L * 60L * 1000L) + (4L * 60L * 1000L) + + ((20L * 1000L) + 345), ms); + } catch (ClassNotFoundException | NoClassDefFoundError ignore) { + assertFalse(ignore.toString(), hasJavaTimeModule()); + } + } + + @Test + public void testGetProperty_String() throws Exception { + String prefix = LogManagerPropertiesTest.class.getName(); + LogManager manager = LogManager.getLogManager(); + try { + String key = prefix.concat(".dummy"); + Properties parent = new Properties(); + parent.put(key, "value"); + parent.put("", "empty"); + + read(manager, parent); + + parent = new Properties(); + LogManagerProperties mp = new LogManagerProperties(parent, prefix); + + assertFalse(contains(mp, key, null)); + assertEquals("value", mp.getProperty(key)); + assertTrue(contains(mp, key, "value")); //ensure copy worked. + parent.put(key, "newValue"); + assertEquals("newValue", mp.getProperty(key)); + assertEquals("empty", mp.getProperty("")); + } finally { + manager.reset(); + } + } + + @Test + public void testGetProperty_String_String() throws Exception { + String prefix = LogManagerPropertiesTest.class.getName(); + LogManager manager = LogManager.getLogManager(); + try { + String key = prefix.concat(".dummy"); + Properties parent = new Properties(); + parent.put(key, "value"); + parent.put("", "empty"); + + read(manager, parent); + + parent = new Properties(); + LogManagerProperties mp = new LogManagerProperties(parent, prefix); + + assertFalse(contains(mp, key, null)); + assertEquals("value", mp.getProperty(key, null)); + assertTrue(contains(mp, key, "value")); //ensure copy worked. + parent.put(key, "newValue"); + assertEquals("newValue", mp.getProperty(key, null)); + assertEquals("default", mp.getProperty("unknown", "default")); + assertEquals("empty", mp.getProperty("", null)); + } finally { + manager.reset(); + } + } + + @Test + public void testGet() throws Exception { + String prefix = LogManagerPropertiesTest.class.getName(); + LogManager manager = LogManager.getLogManager(); + try { + String key = prefix.concat(".dummy"); + Properties parent = new Properties(); + parent.put(key, "value"); + parent.put("", "empty"); + + read(manager, parent); + + parent = new Properties(); + LogManagerProperties mp = new LogManagerProperties(parent, prefix); + + assertFalse(contains(mp, key, null)); + assertEquals("value", mp.get(key)); + assertTrue(contains(mp, key, "value")); //ensure copy worked. + parent.put(key, "newValue"); + assertEquals("newValue", mp.get(key)); + assertEquals("empty", mp.get("")); + } finally { + manager.reset(); + } + } + + @Test + public void testGetObject() throws Exception { + String prefix = LogManagerPropertiesTest.class.getName(); + Properties parent = new Properties(); + LogManagerProperties mp = new LogManagerProperties(parent, prefix); + String key = "key"; + Object value = new Object(); + parent.put(key, value); + + assertEquals(parent.get(key), mp.get(key)); + } + + @Test + public void testContainsKey() throws Exception { + String prefix = LogManagerPropertiesTest.class.getName(); + LogManager manager = LogManager.getLogManager(); + try { + String key = prefix.concat(".dummy"); + Properties parent = new Properties(); + parent.put(key, "value"); + parent.put("", "empty"); + + read(manager, parent); + + parent = new Properties(); + LogManagerProperties mp = new LogManagerProperties(parent, prefix); + + assertFalse(contains(mp, key, null)); + assertTrue(mp.containsKey(key)); + assertTrue(contains(mp, key, "value")); //ensure copy worked. + parent.put(key, "newValue"); + assertEquals("newValue", mp.get(key)); + assertTrue(mp.containsKey("")); + } finally { + manager.reset(); + } + } + + @Test + public void testContainsKeyObject() throws Exception { + String prefix = LogManagerPropertiesTest.class.getName(); + Properties parent = new Properties(); + LogManagerProperties mp = new LogManagerProperties(parent, prefix); + String key = "key"; + Object value = new Object(); + parent.put(key, value); + assertEquals(parent.containsKey(key), mp.containsKey(key)); + } + + @Test + public void testRemove() throws Exception { + String prefix = LogManagerPropertiesTest.class.getName(); + LogManager manager = LogManager.getLogManager(); + try { + String key = prefix.concat(".dummy"); + Properties parent = new Properties(); + parent.put(key, "value"); + parent.put("", "empty"); + + read(manager, parent); + + parent = new Properties(); + LogManagerProperties mp = new LogManagerProperties(parent, prefix); + + assertFalse(contains(mp, key, null)); + assertEquals("value", mp.remove(key)); + assertFalse(contains(mp, key, "value")); //ensure copy worked. + parent.put(key, "newValue"); + assertEquals("newValue", mp.remove(key)); + assertEquals("empty", mp.remove("")); + } finally { + manager.reset(); + } + } + + @Test + public void testRemoveObject() { + String prefix = LogManagerPropertiesTest.class.getName(); + Properties parent = new Properties(); + LogManagerProperties mp = new LogManagerProperties(parent, prefix); + String key = "key"; + Object value = new Object(); + parent.put(key, value); + assertEquals(value, parent.remove(key)); + assertEquals(parent.containsKey(key), mp.containsKey(key)); + assertNull(parent.remove(key)); + } + + @Test + public void testPut() throws Exception { + String prefix = LogManagerPropertiesTest.class.getName(); + LogManager manager = LogManager.getLogManager(); + try { + String key = prefix.concat(".dummy"); + Properties parent = new Properties(); + parent.put(key, "value"); + parent.put("", "empty"); + + read(manager, parent); + + parent = new Properties(); + LogManagerProperties mp = new LogManagerProperties(parent, prefix); + + assertFalse(contains(mp, key, null)); + assertEquals("value", mp.put(key, "newValue")); + assertFalse(contains(mp, key, "value")); //ensure copy worked. + assertTrue(contains(mp, key, "newValue")); //ensure copy worked. + parent.put(key, "defValue"); + assertEquals("newValue", mp.remove(key)); + assertEquals("defValue", mp.remove(key)); + assertEquals("empty", mp.put("", "")); + } finally { + manager.reset(); + } + } + + @Test + public void testPutObject() throws Exception { + String prefix = LogManagerPropertiesTest.class.getName(); + Properties parent = new Properties(); + LogManagerProperties mp = new LogManagerProperties(parent, prefix); + String key = "key"; + Object value = TimeUnit.MILLISECONDS; + assertNull(mp.put(key, value)); + Object newValue = TimeUnit.NANOSECONDS; + assertEquals(value, mp.put(key, newValue)); + assertEquals(newValue, mp.get(key)); + } + + @Test + public void testSetProperty() throws Exception { + String prefix = LogManagerPropertiesTest.class.getName(); + LogManager manager = LogManager.getLogManager(); + try { + String key = prefix.concat(".dummy"); + Properties parent = new Properties(); + parent.put(key, "value"); + parent.put("", "empty"); + + read(manager, parent); + + parent = new Properties(); + LogManagerProperties mp = new LogManagerProperties(parent, prefix); + + assertFalse(contains(mp, key, null)); + assertEquals("value", mp.setProperty(key, "newValue")); + assertFalse(contains(mp, key, "value")); //ensure copy worked. + assertTrue(contains(mp, key, "newValue")); //ensure copy worked. + parent.put(key, "defValue"); + assertEquals("newValue", mp.remove(key)); + assertEquals("defValue", mp.remove(key)); + assertEquals("empty", mp.setProperty("", "")); + } finally { + manager.reset(); + } + } + + @Test + public void testPropUtil() throws Exception { + String prefix = LogManagerPropertiesTest.class.getName(); + LogManager manager = LogManager.getLogManager(); + try { + String keyShort = "mail.smtp.reportsuccess"; + String key = prefix + '.' + keyShort; + Properties parent = new Properties(); + parent.put(key, "true"); + + read(manager, parent); + + parent = new Properties(); + LogManagerProperties mp = new LogManagerProperties(parent, prefix); + assertFalse(contains(mp, keyShort, null)); + + final Session session = Session.getInstance(mp); + final Object t = session.getTransport("smtp"); + if (isPrivateSpec(t.getClass())) { + final String clazzName = "com.sun.mail.smtp.SMTPTransport"; + assertEquals(clazzName, t.getClass().getName()); + } else { + assertNotNull(t); + session.getProperty(keyShort); //Force a read through session. + } + assertTrue(contains(mp, keyShort, "true")); + } finally { + manager.reset(); + } + } + + @Test + public void testToLanguageTag() throws Exception { + assertEquals("en-US", LogManagerProperties.toLanguageTag(Locale.US)); + assertEquals("en", LogManagerProperties.toLanguageTag(Locale.ENGLISH)); + assertEquals("", LogManagerProperties.toLanguageTag(new Locale("", "", ""))); + Locale l = new Locale("en", "US", "slang"); + assertEquals("en-US-slang", LogManagerProperties.toLanguageTag(l)); + l = new Locale("en", "", "slang"); + assertEquals("en--slang", LogManagerProperties.toLanguageTag(l)); + + try { + LogManagerProperties.toLanguageTag(null); + fail("Null was allowed."); + } catch (NullPointerException expect) { + } + } + + @Test + public void testNewObjectFrom() throws Exception { + try { + LogManagerProperties.newObjectFrom((String) null, Object.class); + fail("Null name was allowed."); + } catch (NullPointerException expect) { + } + + try { + LogManagerProperties.newObjectFrom(Object.class.getName(), + (Class<Object>) null); + fail("Null class was allowed."); + } catch (NullPointerException expect) { + } + + try { + LogManagerProperties.newObjectFrom((String) null, + (Class<Object>) null); + fail("Null was allowed."); + } catch (NullPointerException expect) { + } + + try { + LogManagerProperties.newObjectFrom("", Object.class); + fail("Empty class was allowed."); + } catch (ClassNotFoundException expect) { + } + + try { + LogManagerProperties.newObjectFrom(Object.class.getName(), + String.class); + fail("Wrong type was allowed."); + } catch (ClassCastException expect) { + } + + Object o = LogManagerProperties. + newObjectFrom(String.class.getName(), Object.class); + assertEquals(String.class, o.getClass()); + + String n = LogManagerProperties. + newObjectFrom(String.class.getName(), String.class); + assertEquals(String.class, n.getClass()); + } + + @Test + public void testNewAuthenticator() throws Exception { + Authenticator a = LogManagerProperties.newObjectFrom( + EmptyAuthenticator.class.getName(), + Authenticator.class); + assertEquals(EmptyAuthenticator.class, a.getClass()); + + final Class<?> type = ErrorAuthenticator.class; + a = LogManagerProperties.newObjectFrom( + type.getName(), Authenticator.class); + assertEquals(type, a.getClass()); + + setPending(new RuntimeException()); + try { + LogManagerProperties.newObjectFrom(type.getName(), + Authenticator.class); + fail("Exception was not thrown."); + } catch (InvocationTargetException expect) { + assertEquals(RuntimeException.class, expect.getCause().getClass()); + } finally { + setPending(null); + } + } + + @Test + public void testNewComparator() throws Exception { + try { + LogManagerProperties.newComparator(null); + fail("Null was allowed."); + } catch (NullPointerException expect) { + } + + try { + LogManagerProperties.newComparator(""); + fail("Empty class was allowed."); + } catch (ClassNotFoundException expect) { + } + + try { + LogManagerProperties.newComparator(Object.class.getName()); + fail("Wrong type was allowed."); + } catch (ClassCastException expect) { + } + + final Class<?> type = ErrorComparator.class; + final Comparator<? super LogRecord> c + = LogManagerProperties.newComparator(type.getName()); + assertEquals(type, c.getClass()); + + setPending(new RuntimeException()); + try { + LogManagerProperties.newComparator(type.getName()); + fail("Exception was not thrown."); + } catch (InvocationTargetException expect) { + assertEquals(RuntimeException.class, expect.getCause().getClass()); + } finally { + setPending(null); + } + } + + @Test + public void testReverseOrder() throws Exception { + try { + LogManagerProperties.reverseOrder(null); + fail("Null was allowed."); + } catch (NullPointerException expect) { + } + + Comparator<LogRecord> c = new ErrorComparator(); + Comparator<LogRecord> r = LogManagerProperties.reverseOrder(c); + assertTrue(c.getClass() != r.getClass()); + assertFalse(r instanceof ErrorComparator); + assertFalse(r instanceof AscComparator); + assertFalse(r instanceof DescComparator); + + c = new AscComparator(); + r = LogManagerProperties.reverseOrder(c); + assertTrue(r instanceof DescComparator); + + c = new AscComparator(); + r = LogManagerProperties.reverseOrder(c); + assertTrue(r instanceof DescComparator); + } + + @Test + public void testNewErrorManager() throws Exception { + try { + LogManagerProperties.newErrorManager(null); + fail("Null was allowed."); + } catch (NullPointerException expect) { + } + + try { + LogManagerProperties.newErrorManager(""); + fail("Empty class was allowed."); + } catch (ClassNotFoundException expect) { + } + + try { + LogManagerProperties.newErrorManager(Object.class.getName()); + fail("Wrong type was allowed."); + } catch (ClassCastException expect) { + } + + final Class<?> type = ErrorManager.class; + ErrorManager f = LogManagerProperties.newErrorManager(type.getName()); + assertEquals(type, f.getClass()); + + setPending(new RuntimeException()); + try { + final String name = ErrorErrorManager.class.getName(); + LogManagerProperties.newErrorManager(name); + fail("Exception was not thrown."); + } catch (InvocationTargetException expect) { + assertEquals(RuntimeException.class, expect.getCause().getClass()); + } finally { + setPending(null); + } + } + + @Test + public void testNewFilter() throws Exception { + try { + LogManagerProperties.newFilter(null); + fail("Null was allowed."); + } catch (NullPointerException expect) { + } + + try { + LogManagerProperties.newFilter(""); + fail("Empty class was allowed."); + } catch (ClassNotFoundException expect) { + } + + try { + LogManagerProperties.newFilter(Object.class.getName()); + fail("Wrong type was allowed."); + } catch (ClassCastException expect) { + } + + final Class<?> type = ErrorFilter.class; + final Filter f = LogManagerProperties.newFilter(type.getName()); + assertEquals(type, f.getClass()); + + setPending(new RuntimeException()); + try { + LogManagerProperties.newFilter(type.getName()); + fail("Exception was not thrown."); + } catch (InvocationTargetException expect) { + assertEquals(RuntimeException.class, expect.getCause().getClass()); + } finally { + setPending(null); + } + } + + @Test + public void testNewFormatter() throws Exception { + try { + LogManagerProperties.newFormatter(null); + fail("Null was allowed."); + } catch (NullPointerException expect) { + } + + try { + LogManagerProperties.newFormatter(""); + fail("Empty class was allowed."); + } catch (ClassNotFoundException expect) { + } + + try { + LogManagerProperties.newFormatter(Object.class.getName()); + fail("Wrong type was allowed."); + } catch (ClassCastException expect) { + } + + final Class<?> type = SimpleFormatter.class; + final Formatter f = LogManagerProperties.newFormatter(type.getName()); + assertEquals(type, f.getClass()); + + setPending(new RuntimeException()); + try { + final String name = ErrorFormatter.class.getName(); + LogManagerProperties.newFormatter(name); + fail("Exception was not thrown."); + } catch (InvocationTargetException expect) { + assertEquals(RuntimeException.class, expect.getCause().getClass()); + } finally { + setPending(null); + } + } + + @Test + public void testEscapingAuthenticator() throws Exception { + try { + Class<?> k = ErrorAuthenticator.class; + javax.mail.Authenticator a; + + a = LogManagerProperties.newObjectFrom(k.getName(), Authenticator.class); + assertEquals(k, a.getClass()); + + setPending(new ThreadDeath()); + try { + a = LogManagerProperties.newObjectFrom(k.getName(), Authenticator.class); + fail(String.valueOf(a)); + } catch (ThreadDeath expect) { + } + + setPending(new OutOfMemoryError()); + try { + a = LogManagerProperties.newObjectFrom(k.getName(), Authenticator.class); + fail(String.valueOf(a)); + } catch (OutOfMemoryError expect) { + } + } finally { + setPending(null); + } + } + + @Test + public void testEscapingComparator() throws Exception { + try { + Class<?> k = ErrorComparator.class; + Comparator<? super LogRecord> c; + + c = LogManagerProperties.newComparator(k.getName()); + assertEquals(k, c.getClass()); + + setPending(new ThreadDeath()); + try { + c = LogManagerProperties.newComparator(k.getName()); + fail(String.valueOf(c)); + } catch (ThreadDeath expect) { + } + + setPending(new OutOfMemoryError()); + try { + c = LogManagerProperties.newComparator(k.getName()); + fail(String.valueOf(c)); + } catch (OutOfMemoryError expect) { + } + } finally { + setPending(null); + } + } + + @Test + public void testEscapingErrorErrorManager() throws Exception { + try { + Class<?> k = ErrorErrorManager.class; + ErrorManager f; + + f = LogManagerProperties.newErrorManager(k.getName()); + assertEquals(k, f.getClass()); + + setPending(new ThreadDeath()); + try { + f = LogManagerProperties.newErrorManager(k.getName()); + fail(String.valueOf(f)); + } catch (ThreadDeath expect) { + } + + setPending(new OutOfMemoryError()); + try { + f = LogManagerProperties.newErrorManager(k.getName()); + fail(String.valueOf(f)); + } catch (OutOfMemoryError expect) { + } + } finally { + setPending(null); + } + } + + @Test + public void testEscapingFilter() throws Exception { + try { + Class<?> k = ErrorFilter.class; + Filter f; + + f = LogManagerProperties.newFilter(k.getName()); + assertEquals(k, f.getClass()); + + setPending(new ThreadDeath()); + try { + f = LogManagerProperties.newFilter(k.getName()); + fail(String.valueOf(f)); + } catch (ThreadDeath expect) { + } + + setPending(new OutOfMemoryError()); + try { + f = LogManagerProperties.newFilter(k.getName()); + fail(String.valueOf(f)); + } catch (OutOfMemoryError expect) { + } + } finally { + setPending(null); + } + } + + @Test + public void testEscapingFormatter() throws Exception { + try { + Class<?> k = ErrorFormatter.class; + Formatter f; + + f = LogManagerProperties.newFormatter(k.getName()); + assertEquals(k, f.getClass()); + + setPending(new ThreadDeath()); + try { + f = LogManagerProperties.newFormatter(k.getName()); + fail(String.valueOf(f)); + } catch (ThreadDeath expect) { + } + + setPending(new OutOfMemoryError()); + try { + f = LogManagerProperties.newFormatter(k.getName()); + fail(String.valueOf(f)); + } catch (OutOfMemoryError expect) { + } + } finally { + setPending(null); + } + } + + @Test + @SuppressWarnings("unchecked") + public void testGetZonedDateTime() throws Exception { + LogRecord r1 = new LogRecord(Level.SEVERE, ""); + LogRecord r2 = new LogRecord(Level.SEVERE, ""); + try { + final Class<?> k = Class.forName("java.time.ZonedDateTime"); + setEpochSecond(r1, 100, 1); + setEpochSecond(r2, 100, 1); + Comparable<Object> c1 = (Comparable<Object>) + LogManagerProperties.getZonedDateTime(r1); + Comparable<Object> c2 = (Comparable<Object>) + LogManagerProperties.getZonedDateTime(r2); + + assertEquals(k, c1.getClass()); + assertEquals(k, c2.getClass()); + assertNotSame(c1, c2); + assertEquals(c1.getClass(), c2.getClass()); + assertEquals(0, c1.compareTo(c2)); + } catch (final NoSuchMethodException preJdk9) { + assertNull(LogManagerProperties.getZonedDateTime(r1)); + assertNull(LogManagerProperties.getZonedDateTime(r2)); + assertTrue(hasJavaTimeModule()); + } catch (final ClassNotFoundException preJdk8) { + assertNull(LogManagerProperties.getZonedDateTime(r1)); + assertNull(LogManagerProperties.getZonedDateTime(r2)); + assertFalse(hasJavaTimeModule()); + } + } + + @Test(expected=NullPointerException.class) + public void testGetZonedDateTimeNull() throws Exception { + LogManagerProperties.getZonedDateTime((LogRecord) null); + } + + private static void setPending(final Throwable t) { + if (t != null) { + PENDING.set(t); + } else { + PENDING.remove(); + } + } + + static void throwPendingIfSet() { + Throwable t = PENDING.get(); + if (t != null) { + if (t instanceof Error) { + t = t.fillInStackTrace(); + assert t instanceof Error : t; + throw (Error) t; + } else if (t instanceof RuntimeException) { + t = t.fillInStackTrace(); + assert t instanceof RuntimeException : t; + throw (RuntimeException) t; + } else { + throw new AssertionError(t); + } + } + } + + private static Field setAccessible(Field f) { + f.setAccessible(true); + try { + assumeNoJit(); + if (Modifier.isFinal(f.getModifiers())) { + Field mod = Field.class.getDeclaredField("modifiers"); + mod.setAccessible(true); + mod.setInt(f, f.getModifiers() & ~Modifier.FINAL); + return mod; + } + } catch (RuntimeException | ReflectiveOperationException re) { + Assume.assumeNoException(re); + } + throw new AssertionError(); + } + + private boolean contains(Properties props, String key, String value) { + if (key == null) { + throw new NullPointerException(); + } + + //walk the entry set so we don't preload a key from the manager. + for (Map.Entry<?, ?> e : props.entrySet()) { + if (key.equals(e.getKey())) { + return value.equals(e.getValue()); + } + } + return false; + } + + public static final class EmptyAuthenticator extends javax.mail.Authenticator { + + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return null; + } + } + + public static final class ErrorAuthenticator extends javax.mail.Authenticator { + + public ErrorAuthenticator() { + throwPendingIfSet(); + } + + @Override + protected PasswordAuthentication getPasswordAuthentication() { + throw new Error(""); + } + } + + public static class ErrorComparator implements Comparator<LogRecord>, + Serializable { + + private static final long serialVersionUID = 1L; + + public ErrorComparator() { + throwPendingIfSet(); + } + + @SuppressWarnings("override") //JDK-6954234 + public int compare(LogRecord r1, LogRecord r2) { + throw new Error(""); + } + } + + public static class AscComparator implements Comparator<LogRecord>, + Serializable { + + private static final long serialVersionUID = 1L; + + @SuppressWarnings("override") //JDK-6954234 + public int compare(LogRecord r1, LogRecord r2) { + throw new UnsupportedOperationException(); + } + + @SuppressWarnings("override") + public Comparator<LogRecord> reversed() { + return new DescComparator(); + } + } + + public static class DescComparator implements Comparator<LogRecord>, + Serializable { + + private static final long serialVersionUID = 1L; + + @SuppressWarnings("override") //JDK-6954234 + public int compare(LogRecord r1, LogRecord r2) { + throw new UnsupportedOperationException(); + } + + @SuppressWarnings("override") + public Comparator<LogRecord> reversed() { + return new AscComparator(); + } + } + + public static class ErrorFilter implements Filter { + + public ErrorFilter() { + throwPendingIfSet(); + } + + @SuppressWarnings("override") //JDK-6954234 + public boolean isLoggable(LogRecord record) { + throw new Error(""); + } + } + + public static class ErrorFormatter extends Formatter { + + public ErrorFormatter() { + throwPendingIfSet(); + } + + @Override + public String format(LogRecord record) { + throw new Error(""); + } + } + + public static class ErrorErrorManager extends ErrorManager { + + public ErrorErrorManager() { + throwPendingIfSet(); + } + + @Override + public void error(String msg, Exception ex, int code) { + throw new Error(""); + } + } + + private static final class NoHostService extends Service { + + public NoHostService(Session session) { + super(session, new URLName("test://somehost")); + } + } + + private static final class NotAllowedService extends Service { + + public NotAllowedService(Session session) { + super(session, new URLName("test://somehost")); + } + + public String getLocalHost() { + throw new SecurityException(); + } + } + + private static final class LogPermSecurityManager extends SecurityManager { + + volatile boolean secure = false; + + LogPermSecurityManager() { + } + + @Override + public void checkPermission(java.security.Permission perm) { + try { //Call super class always for java.security.debug tracing. + super.checkPermission(perm); + checkPermission(perm, new SecurityException(perm.toString())); + } catch (SecurityException se) { + checkPermission(perm, se); + } + } + + @Override + public void checkPermission(java.security.Permission perm, Object context) { + try { //Call super class always for java.security.debug tracing. + super.checkPermission(perm, context); + checkPermission(perm, new SecurityException(perm.toString())); + } catch (SecurityException se) { + checkPermission(perm, se); + } + } + + private void checkPermission(java.security.Permission perm, SecurityException se) { + if (secure && perm instanceof LoggingPermission) { + throw se; + } + } + } +}
diff --git a/mail/src/test/java/com/sun/mail/util/logging/MailHandlerTest.java b/mail/src/test/java/com/sun/mail/util/logging/MailHandlerTest.java new file mode 100644 index 0000000..a28721d --- /dev/null +++ b/mail/src/test/java/com/sun/mail/util/logging/MailHandlerTest.java
@@ -0,0 +1,8520 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2009, 2018 Jason Mehrens. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package com.sun.mail.util.logging; + +import java.io.*; +import java.lang.management.CompilationMXBean; +import java.lang.management.ManagementFactory; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.net.*; +import java.util.*; +import java.util.logging.*; +import java.util.logging.Formatter; +import javax.activation.*; +import javax.mail.*; +import javax.mail.Authenticator; +import javax.mail.PasswordAuthentication; +import javax.mail.internet.*; +import org.junit.*; +import static org.junit.Assert.*; + +/** + * Test case for the MailHandler spec. + * + * @author Jason Mehrens + */ +public class MailHandlerTest extends AbstractLogging { + + /** + * See LogManager. + */ + private static final String LOG_CFG_KEY = "java.util.logging.config.file"; + /** + * Holder used to inject Throwables into other APIs. + */ + private static final ThreadLocal<Throwable> PENDING = new ThreadLocal<>(); + /** + * Holder used to inject ClassLoaders into other APIs. + */ + private static final ThreadLocal<ClassLoader> LOADER = new ThreadLocal<>(); + /** + * Stores the value of a port that is not used on the local machine. + */ + private static volatile int OPEN_PORT = Integer.MIN_VALUE; + /** + * A host name that can not be resolved. + */ + private static final String UNKNOWN_HOST = "bad-host-name"; + /** + * Stores a writable directory that is in the class path and visible to the + * context class loader. + */ + private static volatile File anyClassPathDir = null; + /** + * Used to prevent G.C. of loggers. + */ + private volatile Object hardRef; + + @BeforeClass + public static void setUpClass() throws Exception { + checkJVMOptions(); + OPEN_PORT = findOpenPort(); + checkUnknownHost(); + assertTrue(findClassPathDir().isDirectory()); + } + + @AfterClass + public static void tearDownClass() throws Exception { + checkJVMOptions(); + assertTrue(checkUnusedPort(OPEN_PORT)); + OPEN_PORT = Integer.MIN_VALUE; + checkUnknownHost(); + anyClassPathDir = null; + } + + private static void assumeNoJit() { + CompilationMXBean c = ManagementFactory.getCompilationMXBean(); + if (c != null) { //-Xint + Assume.assumeNoException(new IllegalArgumentException( + c.getName() + " must be disabled.")); + } + } + + private static void fullFence() { + LogManager.getLogManager().getProperty(""); + } + + @Before + public void setUp() { + fullFence(); + assertNull(hardRef); + } + + @After + public void tearDown() { + fullFence(); + hardRef = null; + } + + private static void checkUnknownHost() throws Exception { + try { + InetAddress.getByName(UNKNOWN_HOST); + throw new AssertionError(UNKNOWN_HOST); + } catch (UnknownHostException expect) { + } + } + + private static void checkJVMOptions() throws Exception { + assertTrue(MailHandlerTest.class.desiredAssertionStatus()); + assertNull(System.getProperty("java.util.logging.manager")); + assertNull(System.getProperty("java.util.logging.config.class")); + assertNull(System.getProperty(LOG_CFG_KEY)); + assertEquals(LogManager.class, LogManager.getLogManager().getClass()); + assertTrue(LOW_CAPACITY < NUM_RUNS); + //Try to hold MAX_CAPACITY array with log records. + assertTrue((60L * 1024L * 1024L) <= Runtime.getRuntime().maxMemory()); + try { + if (InetAddress.getLocalHost().getHostName().length() == 0) { + throw new UnknownHostException(); + } + } catch (UnknownHostException UHE) { + throw new AssertionError(UHE); + } + } + + private static Throwable getPending() { + return PENDING.get(); + } + + private static void setPending(final Throwable t) { + if (t != null) { + PENDING.set(t); + } else { + PENDING.remove(); + } + } + + private static Field setAccessible(Field f) { + f.setAccessible(true); + try { + assumeNoJit(); + if (Modifier.isFinal(f.getModifiers())) { + Field mod = Field.class.getDeclaredField("modifiers"); + mod.setAccessible(true); + mod.setInt(f, f.getModifiers() & ~Modifier.FINAL); + return mod; + } + } catch (RuntimeException re) { + Assume.assumeNoException(re); + } catch (Exception e) { + Assume.assumeNoException(e); + } + throw new AssertionError(); + } + + private static void set(ClassLoader expect) { + if (expect == null) { + LOADER.remove(); + } else { + LOADER.set(expect); + } + } + + static void throwPending() { + Throwable t = PENDING.get(); + if (t instanceof Error) { + t = t.fillInStackTrace(); + assert t instanceof Error : t; + throw (Error) t; + } else if (t instanceof RuntimeException) { + t = t.fillInStackTrace(); + assert t instanceof RuntimeException : t; + throw (RuntimeException) t; + } else { + throw new AssertionError(t); + } + } + + static boolean isSecurityDebug() { + boolean debug; + final String value = System.getProperty("java.security.debug"); + if (value != null) { + debug = value.contains("all") + || value.contains("access") + || value.contains("stack"); + } else { + debug = false; + } + return debug; + } + + static void securityDebugPrint(Throwable se) { + @SuppressWarnings("UseOfSystemOutOrSystemErr") + final PrintStream err = System.err; + err.println("Suppressed security exception to allow access:"); + se.printStackTrace(err); + } + + static void checkContextClassLoader(ClassLoader expect) { + Object ccl = Thread.currentThread().getContextClassLoader(); + if (expect != ccl) { + AssertionError ae = new AssertionError(expect + " != " + ccl + + ", sm=" + System.getSecurityManager()); + dump(ae); + throw ae; + } + } + + @Test + public void testChildClassLoader() { + assertNull(System.getSecurityManager()); + final Thread thread = Thread.currentThread(); + final ClassLoader ccl = thread.getContextClassLoader(); + try { + URLClassLoader child = new URLClassLoader(new URL[0], ccl); + + thread.setContextClassLoader(child); + testCallingClassLoader((ClassLoaderSecurityManager) null, child); + + thread.setContextClassLoader(child); + testCallingClassLoader(new ClassLoaderSecurityManager(), child); + } finally { + thread.setContextClassLoader(ccl); + } + assertNull(System.getSecurityManager()); + } + + private void testCallingClassLoader( + ClassLoaderSecurityManager sm, ClassLoader expect) { + InternalErrorManager em = new ClassLoaderErrorManager(expect); + try { + MailHandler instance = new MailHandler(createInitProperties("")); + try { + if (sm != null) { + System.setSecurityManager(sm); + sm.secure = true; + } + instance.setErrorManager(em); + instance.setLevel(Level.ALL); + instance.setPushLevel(Level.SEVERE); + instance.setComparator(new ClassLoaderComparator(expect)); + instance.setFilter(new ClassLoaderFilterFormatter(expect)); + instance.setPushFilter(new ClassLoaderFilterFormatter(expect)); + instance.setSubject(new ClassLoaderFilterFormatter(expect)); + instance.setFormatter(new ClassLoaderFilterFormatter(expect)); + instance.setAttachmentFormatters( + new ClassLoaderFilterFormatter(expect, "testCCL")); + instance.setAttachmentFilters(new ClassLoaderFilterFormatter(expect)); + instance.setAttachmentNames(new ClassLoaderFilterFormatter(expect)); + instance.publish(new LogRecord(Level.WARNING, "")); + instance.publish(new LogRecord(Level.SEVERE, "")); + } finally { + instance.close(); + } + } finally { + if (sm != null) { + sm.secure = false; + System.setSecurityManager((SecurityManager) null); + } + } + + assert em != null; + for (Exception exception : em.exceptions) { + Throwable t = exception; + if (t instanceof MessagingException == false) { + dump(t); + fail(t.toString()); + } + } + assertFalse(em.exceptions.isEmpty()); + } + + @Test + public void testVerifyClassLoader() throws Exception { + assertNull(System.getSecurityManager()); + final Thread thread = Thread.currentThread(); + final ClassLoader ccl = thread.getContextClassLoader(); + try { + URLClassLoader child = new URLClassLoader(new URL[0], ccl); + + thread.setContextClassLoader(child); + testVerify((ClassLoaderSecurityManager) null, child); + + thread.setContextClassLoader(child); + testVerify(new ClassLoaderSecurityManager(), child); + } finally { + thread.setContextClassLoader(ccl); + } + assertNull(System.getSecurityManager()); + } + + @Test + public void testJavaMailLinkage() throws Exception { + /** + * The MailHandler has to depend on the official JavaMail spec classes. + * The logging-mailhandler.jar needs to be portable to other platforms + * so it doesn't depend on reference implementation classes directly. + */ + testJavaMailLinkage(MailHandler.class, false); + } + + @Test + public void testLogManagerModifiers() throws Exception { + testLogManagerModifiers(MailHandler.class); + } + + @Test + public void testWebappClassLoaderFieldNames() throws Exception { + testWebappClassLoaderFieldNames(MailHandler.class); + } + + private void testVerify(ClassLoaderSecurityManager sm, ClassLoader expect) throws Exception { + final LogManager manager = LogManager.getLogManager(); + InternalErrorManager em = null; + set(expect); + try { + String p = MailHandler.class.getName(); + Properties props = createInitProperties(p); + props.put(p.concat(".verify"), "local"); + props.put(p.concat(".comparator"), ClassLoaderComparator.class.getName()); + props.put(p.concat(".pushFilter"), ClassLoaderFilterFormatter.class.getName()); + props.put(p.concat(".subject"), ClassLoaderFilterFormatter.class.getName()); + props.put(p.concat(".errorManager"), ClassLoaderErrorManager.class.getName()); + + read(manager, props); + + if (sm != null) { + System.setSecurityManager(sm); + sm.secure = true; + } + + MailHandler instance = new MailHandler(); + try { + em = internalErrorManagerFrom(instance); + } finally { + instance.close(); + } + } finally { + if (sm != null) { + sm.secure = false; + System.setSecurityManager((SecurityManager) null); + } + set((ClassLoader) null); + manager.reset(); + } + + assert em != null; + for (Exception exception : em.exceptions) { + Throwable t = exception; + if (t instanceof MessagingException == false) { + dump(t); + } + } + assertFalse(em.exceptions.isEmpty()); + } + + @Test + public void testSetMailPropertiesClassLoader() throws Exception { + assertNull(System.getSecurityManager()); + final Thread thread = Thread.currentThread(); + final ClassLoader ccl = thread.getContextClassLoader(); + try { + URLClassLoader child = new URLClassLoader(new URL[0], ccl); + + thread.setContextClassLoader(child); + testSetMailProperties((ClassLoaderSecurityManager) null, child); + + thread.setContextClassLoader(child); + testSetMailProperties(new ClassLoaderSecurityManager(), child); + } finally { + thread.setContextClassLoader(ccl); + } + assertNull(System.getSecurityManager()); + } + + private void testSetMailProperties(ClassLoaderSecurityManager sm, ClassLoader expect) throws Exception { + InternalErrorManager em = new ClassLoaderErrorManager(expect); + try { + Properties props = createInitProperties(""); + props.put("verify", "local"); + + if (sm != null) { + System.setSecurityManager(sm); + sm.secure = true; + } + + MailHandler instance = new MailHandler(); + try { + instance.setErrorManager(em); + instance.setComparator(new ClassLoaderComparator(expect)); + instance.setFilter(new ClassLoaderFilterFormatter(expect)); + instance.setPushFilter(new ClassLoaderFilterFormatter(expect)); + instance.setSubject(new ClassLoaderFilterFormatter(expect)); + instance.setFormatter(new ClassLoaderFilterFormatter(expect)); + + instance.setMailProperties(props); + } finally { + instance.close(); + } + } finally { + if (sm != null) { + sm.secure = false; + System.setSecurityManager((SecurityManager) null); + } + } + + assert em != null; + for (Exception exception : em.exceptions) { + Throwable t = exception; + if (t instanceof MessagingException == false) { + dump(t); + } + } + assertFalse(em.exceptions.isEmpty()); + } + + @Test + public void testIsLoggable() { + final Level[] lvls = getAllLevels(); + if (lvls.length > 0) { + LogRecord record = new LogRecord(Level.INFO, ""); + for (Level lvl : lvls) { + testLoggable(lvl, null); + testLoggable(lvl, record); + } + } else { + fail("No predefined levels."); + } + } + + private void testLoggable(Level lvl, LogRecord record) { + MailHandler instance = new MailHandler(createInitProperties("")); + InternalErrorManager em = new InternalErrorManager(); + instance.setErrorManager(em); + + instance.setLevel(lvl); + MemoryHandler mem = null; + boolean result = false; + boolean expect = true; + try { + result = instance.isLoggable(record); + mem = new MemoryHandler(new ConsoleHandler(), 100, Level.OFF); + mem.setErrorManager(em); + mem.setLevel(lvl); + expect = mem.isLoggable(record); + } catch (RuntimeException mailEx) { + try { + if (mem != null) { + fail("MemoryHandler threw and exception: " + mailEx); + } else { + mem = new MemoryHandler(new ConsoleHandler(), 100, Level.OFF); + mem.setErrorManager(em); + mem.setLevel(lvl); + expect = mem.isLoggable(record); + fail("MailHandler threw and exception: " + mailEx); + } + } catch (RuntimeException memEx) { + assertEquals(memEx.getClass(), mailEx.getClass()); + result = false; + expect = false; + } + } + assertEquals(expect, result); + + instance.setLevel(Level.INFO); + instance.setFilter(BooleanFilter.FALSE); + instance.setAttachmentFormatters( + new Formatter[]{new SimpleFormatter(), new XMLFormatter()}); + //null filter makes all records INFO and above loggable. + instance.setAttachmentFilters(new Filter[]{BooleanFilter.FALSE, null}); + assertEquals(false, instance.isLoggable(new LogRecord(Level.FINEST, ""))); + assertEquals(true, instance.isLoggable(new LogRecord(Level.INFO, ""))); + assertEquals(true, instance.isLoggable(new LogRecord(Level.WARNING, ""))); + assertEquals(true, instance.isLoggable(new LogRecord(Level.SEVERE, ""))); + + assertEquals(em.exceptions.isEmpty(), true); + } + + @Test + public void testPostConstruct() { + MailHandler instance = new MailHandler(createInitProperties("")); + InternalErrorManager em = new InternalErrorManager(); + instance.setErrorManager(em); + instance.postConstruct(); + + assertEquals(true, em.exceptions.isEmpty()); + instance.close(); + } + + @Test + public void testPreDestroy() { + MailHandler instance = new MailHandler(createInitProperties("")); + InternalErrorManager em = new InternalErrorManager(); + instance.setErrorManager(em); + instance.preDestroy(); + + assertEquals(true, em.exceptions.isEmpty()); + instance.close(); + + instance = createHandlerWithRecords(); + em = internalErrorManagerFrom(instance); + instance.preDestroy(); + + assertEquals(1, em.exceptions.size()); + assertEquals(true, em.exceptions.get(0) instanceof MessagingException); + instance.close(); + + //Test for valid message. + instance = createHandlerWithRecords(); + instance.setErrorManager(new FlushErrorManager(instance)); + instance.preDestroy(); + instance.close(); + + //Test that preDestroy is normal priority. + instance = new MailHandler(2); + instance.setMailProperties(createInitProperties("")); + instance.setLevel(Level.ALL); + instance.setErrorManager(new FlushErrorManager(instance)); + instance.setPushFilter((Filter) null); + instance.setPushLevel(Level.OFF); + LogRecord record = new LogRecord(Level.INFO, ""); + instance.publish(record); //should flush. + instance.preDestroy(); + instance.push(); //Trigger an error if data is present here. + instance.close(); + } + + @Test + public void testReflectiveLifeCycleAccess() throws Exception { + final Class<?> k = MailHandler.class; + final Method construct = k.getDeclaredMethod("postConstruct"); + final Method destory = k.getDeclaredMethod("preDestroy"); + testLifeCycleProtypes(construct); + testLifeCycleProtypes(destory); + + MailHandler h = new MailHandler(); + try { + h.setMailProperties(createInitProperties("")); + InternalErrorManager em = new InternalErrorManager(); + h.setErrorManager(em); + + construct.invoke(h); + destory.invoke(h); + + for (Throwable t : em.exceptions) { + dump(t); + } + assertEquals(true, em.exceptions.isEmpty()); + } finally { + h.close(); + } + } + + private void testLifeCycleProtypes(Method m) throws Exception { + final String s = m.toString(); + assertTrue(s, MailHandler.class.equals(m.getDeclaringClass())); + assertTrue(s, Modifier.isPublic(m.getModifiers())); + assertFalse(s, Modifier.isStatic(m.getModifiers())); + assertTrue(s, Void.TYPE.equals(m.getReturnType())); + assertTrue(s, m.getParameterTypes().length == 0); + assertTrue(s, m.getExceptionTypes().length == 0); + } + + @Test + public void testNoDependencyOnJavaxAnnotations() throws Exception { + testNoDependencyOnJavaxAnnotations(MailHandler.class); + } + + @Test + public void testPreDestroyLinkageError() throws Exception { + testLinkageErrorWithStack("preDestroy"); + } + + @Test + public void testPreDestroyLinkageErrorEmpty() throws Exception { + testLinkageErrorEmptyStack("preDestroy"); + } + + @Test + public void testPublish() { + MailHandler instance = createHandlerWithRecords(); + InternalErrorManager em = internalErrorManagerFrom(instance); + assertEquals(em.exceptions.isEmpty(), true); + instance.close(); + + assertEquals(true, em.exceptions.get(0) instanceof MessagingException); + assertEquals(1, em.exceptions.size()); + + //Test for valid message. + instance = createHandlerWithRecords(); + instance.setErrorManager(new FlushErrorManager(instance)); + + final Level[] lvls = getAllLevels(); + String SOURCE_CLASS = MailHandlerTest.class.getName(); + String SOURCE_METHOD = "testPublish"; + for (Level lvl : lvls) { + LogRecord r = new LogRecord(lvl, ""); + r.setSourceClassName(SOURCE_CLASS); + r.setSourceMethodName(SOURCE_METHOD); + instance.publish(r); + } + + instance.close(); + } + + @Test + public void testPublishLinkageError() throws Exception { + testLinkageErrorWithStack("publish"); + } + + @Test + public void testPublishLinkageErrorEmpty() throws Exception { + testLinkageErrorEmptyStack("publish"); + } + + @Test + public void testPublishDuringClose() { + final Level[] lvls = getAllLevels(); + for (int levelIndex = 0; levelIndex < lvls.length; levelIndex++) { + MailHandler instance = new MailHandler(lvls.length + 2); + InternalErrorManager em = new InternalErrorManager(); + instance.setErrorManager(em); + Properties props = createInitProperties(""); + instance.setMailProperties(props); + + Authenticator auth = new EmptyAuthenticator(); + Filter filter = BooleanFilter.TRUE; + Formatter formatter = new SimpleFormatter(); + instance.setSubject("publishDuringClose"); + Formatter subject = instance.getSubject(); + + instance.setAuthenticator(auth); + instance.setLevel(Level.ALL); + instance.setFormatter(formatter); + instance.setFilter(filter); + instance.setPushLevel(Level.OFF); + instance.setPushFilter(filter); + instance.setAttachmentFormatters(new Formatter[]{formatter}); + instance.setAttachmentFilters(new Filter[]{filter}); + instance.setAttachmentNames(new Formatter[]{subject}); + + assertTrue(em.exceptions.isEmpty()); + + final String msg = instance.toString(); + for (int j = 0; j < lvls.length; j++) { + Level oldLevel = instance.getLevel(); + Level lvl = lvls[(levelIndex + j) % lvls.length]; + CloseLogRecord r = new CloseLogRecord(lvl, msg, instance); + assertFalse(r.isClosed()); + instance.publish(r); + if (!oldLevel.equals(Level.OFF)) { + assertEquals(Level.OFF, instance.getLevel()); + assertTrue(r.isClosed()); + } + } + + //Close is not allowed to change any settings. + assertEquals(Level.OFF, instance.getLevel()); + assertEquals(props, instance.getMailProperties()); + assertEquals(auth, instance.getAuthenticator()); + assertEquals(subject, instance.getSubject()); + assertEquals(filter, instance.getFilter()); + assertEquals(formatter, instance.getFormatter()); + assertEquals(Level.OFF, instance.getPushLevel()); + assertEquals(filter, instance.getPushFilter()); + assertEquals(formatter, instance.getAttachmentFormatters()[0]); + assertEquals(filter, instance.getAttachmentFilters()[0]); + assertEquals(subject, instance.getAttachmentNames()[0]); + + //ensure one transport error. + assertEquals(1, em.exceptions.size()); + assertTrue(em.exceptions.get(0) instanceof MessagingException); + } + } + + @Test + public void testPublishDuringCloseLinkageError() throws Exception { + testLinkageErrorWithStack("publishDuringClose"); + } + + @Test + public void testPublishDuringCloseLinkageErrorEmpty() throws Exception { + testLinkageErrorEmptyStack("publishDuringClose"); + } + + private MailHandler createHandlerWithRecords() { + final Level[] lvls = getAllLevels(); + + MailHandler instance = new MailHandler(lvls.length + 2); + InternalErrorManager em = new InternalErrorManager(); + instance.setErrorManager(em); + Properties props = createInitProperties(""); + instance.setMailProperties(props); + instance.setLevel(Level.ALL); + instance.setFilter((Filter) null); + instance.setPushLevel(Level.OFF); + instance.setPushFilter((Filter) null); + + final String msg = instance.toString(); + for (Level lvl : lvls) { + LogRecord r = new LogRecord(lvl, msg); + r.setSourceClassName(MailHandlerTest.class.getName()); + r.setLoggerName(r.getSourceClassName()); + r.setSourceMethodName("createHandlerWithRecords"); + instance.publish(r); + } + return instance; + } + + @Test + public void testErrorSubjectFormatter() { + MailHandler instance = new MailHandler(2); + instance.setLevel(Level.ALL); + Properties props = createInitProperties(""); + instance.setMailProperties(props); + + InternalErrorManager em = new InternalErrorManager(); + instance.setErrorManager(em); + instance.setSubject(new ErrorFormatter()); + + LogRecord record = new LogRecord(Level.INFO, ""); + instance.publish(record); + try { + instance.push(); + fail("Error didn't escape push."); + } catch (Error expected) { + if (expected.getClass() != Error.class) { + throw expected; + } else { + assertEquals(Level.ALL, instance.getLevel()); + } + } + + instance.publish(record); + try { + instance.flush(); + fail("Error didn't escape flush."); + } catch (Error expected) { + if (expected.getClass() != Error.class) { + throw expected; + } else { + assertEquals(Level.ALL, instance.getLevel()); + } + } + + instance.publish(record); + record = new LogRecord(Level.INFO, ""); + try { + instance.publish(record); + fail("Error didn't escape publish at full capacity."); + } catch (Error expected) { + if (expected.getClass() != Error.class) { + throw expected; + } else { + assertEquals(Level.ALL, instance.getLevel()); + } + } + + instance.publish(record); + try { + instance.close(); + fail("Error didn't escape close."); + } catch (Error expected) { + if (expected.getClass() != Error.class) { + throw expected; + } else { + assertEquals(Level.OFF, instance.getLevel()); + } + } + + instance.close(); + final int size = em.exceptions.size(); + if (size > 0) { + fail(em.exceptions.toString()); + } + } + + @Test + public void testErrorComparator() { + testErrorComparator(0); + testErrorComparator(1); + testErrorComparator(2); + testErrorComparator(3); + testErrorComparator(10); + testErrorComparator(999); + testErrorComparator(1000); + testErrorComparator(1001); + } + + private void testErrorComparator(int records) { + assertTrue("Invalid argument.", records >= 0); + Properties props = createInitProperties(""); + MailHandler instance = new MailHandler(props); + instance.setComparator(new ErrorComparator()); + instance.setErrorManager(new InternalErrorManager()); + boolean normal = false; + try { + try { + for (int i = 0; i < records; ++i) { + instance.publish(new LogRecord(Level.SEVERE, "")); + } + } finally { + instance.close(); + } + normal = true; + } catch (Error e) { + if (records == 0 || e.getClass() != Error.class) { + throw e; + } else { + InternalErrorManager em = internalErrorManagerFrom(instance); + for (Throwable t : em.exceptions) { + dump(t); + } + assertEquals(true, em.exceptions.isEmpty()); + } + } + + if (normal) { + assertTrue(records == 0); + } + } + + @Test + public void testThrowFormatters() { + MailHandler instance = new MailHandler(createInitProperties("")); + instance.setLevel(Level.ALL); + InternalErrorManager em = new InternalErrorManager(); + instance.setErrorManager(em); + instance.setComparator(new ThrowComparator()); + instance.setFormatter(new ThrowFormatter()); + instance.setSubject(new ThrowFormatter()); + instance.setAttachmentFormatters(new Formatter[]{new ThrowFormatter()}); + instance.setAttachmentNames(new Formatter[]{new ThrowFormatter()}); + + LogRecord record = new LogRecord(Level.INFO, ""); + instance.publish(record); + instance.close(); + + final int size = em.exceptions.size(); + if (size > 0) { + for (int i = 0; i < em.exceptions.size() - 1; i++) { + assertEquals(true, em.exceptions.get(i) instanceof RuntimeException); + } + assertEquals(true, + em.exceptions.get(size - 1) instanceof MessagingException); + return; + } + fail("No runtime exceptions reported"); + } + + @Test + public void testErrorFormatters() { + MailHandler instance = new MailHandler(createInitProperties("")); + instance.setLevel(Level.ALL); + InternalErrorManager em = new InternalErrorManager(); + instance.setErrorManager(em); + instance.setComparator(new ErrorComparator()); + instance.setFormatter(new ErrorFormatter()); + instance.setSubject(new ErrorFormatter()); + instance.setAttachmentFormatters(new Formatter[]{new ErrorFormatter()}); + instance.setAttachmentNames(new Formatter[]{new ErrorFormatter()}); + + LogRecord record = new LogRecord(Level.INFO, ""); + instance.publish(record); + try { + instance.close(); + fail("Error was swallowed."); + } catch (Error expect) { + if (expect.getClass() != Error.class) { + throw expect; + } + } + } + + @Test + public void testErrorFilters() { + LogRecord record = new LogRecord(Level.INFO, ""); + MemoryHandler mh = new MemoryHandler(new ConsoleHandler(), 100, Level.OFF); + mh.setFilter(new ErrorFilter()); + MailHandler instance = null; + try { + boolean expect = mh.isLoggable(record); + instance = new MailHandler(createInitProperties("")); + instance.setLevel(Level.ALL); + instance.setFilter(new ErrorFilter()); + boolean result = instance.isLoggable(record); + assertEquals(expect, result); + } catch (Error expectEx) { + if (instance == null) { + try { + instance = new MailHandler(createInitProperties("")); + instance.setLevel(Level.ALL); + instance.setFilter(new ErrorFilter()); + instance.isLoggable(record); + fail("Doesn't match the memory handler."); + } catch (Error resultEx) { + assertEquals(expectEx.getClass(), resultEx.getClass()); + } + } else { + fail("Doesn't match the memory handler."); + } + } + + assert instance != null; + instance.setFilter((Filter) null); + + Properties props = new Properties(); + props.put("mail.smtp.host", UNKNOWN_HOST); + instance.setMailProperties(props); + + InternalErrorManager em = new InternalErrorManager(); + instance.setErrorManager(em); + + instance.setAttachmentFormatters(new Formatter[]{new SimpleFormatter()}); + instance.setAttachmentFilters(new Filter[]{new ErrorFilter()}); + instance.setAttachmentNames(new String[]{"test.txt"}); + + instance.publish(record); + try { + instance.close(); + fail("Error was swallowed."); + } catch (Error expect) { + if (expect.getClass() != Error.class) { + throw expect; + } + } + + assertEquals(true, em.exceptions.isEmpty()); + } + + @Test + public void testThrowComparator() { + testThrowComparator(0); + testThrowComparator(1); + testThrowComparator(2); + testThrowComparator(3); + testThrowComparator(10); + testThrowComparator(999); + testThrowComparator(1000); + testThrowComparator(1001); + } + + private void testThrowComparator(int records) { + assertTrue(records >= 0); + MailHandler instance = new MailHandler(createInitProperties("")); + instance.setComparator(new ThrowComparator()); + instance.setErrorManager(new InternalErrorManager()); + try { + for (int i = 0; i < records; ++i) { + instance.publish(new LogRecord(Level.SEVERE, "")); + } + } finally { + instance.close(); + } + + InternalErrorManager em = internalErrorManagerFrom(instance); + boolean seenError = false; + for (Throwable t : em.exceptions) { + if (isConnectOrTimeout(t)) { + continue; + } else if (t.getClass() == RuntimeException.class) { + seenError = true; + continue; //expect. + } else { + dump(t); + fail(t.toString()); + } + } + + if (records == 0) { + assertEquals(true, em.exceptions.isEmpty()); + } else { + assertTrue("Exception was not thrown.", seenError); + assertEquals(true, !em.exceptions.isEmpty()); + } + } + + @Test + public void testThrowFilters() { + LogRecord record = new LogRecord(Level.INFO, ""); + MemoryHandler mh = new MemoryHandler(new ConsoleHandler(), 100, Level.OFF); + mh.setFilter(new ThrowFilter()); + MailHandler instance = null; + try { + boolean expect = mh.isLoggable(record); + instance = new MailHandler(createInitProperties("")); + instance.setLevel(Level.ALL); + instance.setFilter(new ThrowFilter()); + boolean result = instance.isLoggable(record); + assertEquals(expect, result); + } catch (RuntimeException expectEx) { + if (instance == null) { + try { + instance = new MailHandler(createInitProperties("")); + instance.setLevel(Level.ALL); + instance.setFilter(new ThrowFilter()); + instance.isLoggable(record); + fail("Doesn't match the memory handler."); + } catch (RuntimeException resultEx) { + assertEquals(expectEx.getClass(), resultEx.getClass()); + } + } else { + fail("Doesn't match the memory handler."); + } + } + + assert instance != null; + instance.setFilter((Filter) null); + + Properties props = new Properties(); + props.put("mail.smtp.host", UNKNOWN_HOST); + instance.setMailProperties(props); + + InternalErrorManager em = new InternalErrorManager(); + instance.setErrorManager(em); + + instance.setAttachmentFormatters(new Formatter[]{new SimpleFormatter()}); + instance.setAttachmentFilters(new Filter[]{new ThrowFilter()}); + instance.setAttachmentNames(new String[]{"test.txt"}); + + instance.publish(record); + instance.close(); + + assertEquals(true, !em.exceptions.isEmpty()); + } + + @Test + public void testEmpty() { + MailHandler instance = createHandlerWithRecords(); + instance.setFormatter(new SimpleFormatter()); + instance.setAttachmentFormatters(new Formatter[]{ + new EmptyFormatter(), new SimpleFormatter(), new SimpleFormatter()}); + testEmpty(instance); + + instance = createHandlerWithRecords(); + instance.setFormatter(new SimpleFormatter()); + instance.setAttachmentFormatters(new Formatter[]{ + new SimpleFormatter(), new EmptyFormatter(), new SimpleFormatter()}); + testEmpty(instance); + + instance = createHandlerWithRecords(); + instance.setFormatter(new SimpleFormatter()); + instance.setAttachmentFormatters(new Formatter[]{ + new SimpleFormatter(), new SimpleFormatter(), new EmptyFormatter()}); + testEmpty(instance); + + instance = createHandlerWithRecords(); + instance.setFormatter(new EmptyFormatter()); + instance.setAttachmentFormatters(new Formatter[]{ + new SimpleFormatter(), new SimpleFormatter(), new SimpleFormatter()}); + testEmpty(instance); + + instance = createHandlerWithRecords(); + instance.setFormatter(new EmptyFormatter()); + instance.setAttachmentFormatters(new Formatter[]{ + new SimpleFormatter(), new EmptyFormatter(), new SimpleFormatter()}); + testEmpty(instance); + + instance = createHandlerWithRecords(); + instance.setFormatter(new SimpleFormatter()); + instance.setAttachmentFormatters(new Formatter[]{new EmptyFormatter()}); + testEmpty(instance); + + instance = createHandlerWithRecords(); + instance.setFormatter(new EmptyFormatter()); + instance.setAttachmentFormatters(new Formatter[]{new SimpleFormatter()}); + testEmpty(instance); + + instance = createHandlerWithRecords(); + instance.setFormatter(new EmptyFormatter()); + instance.setAttachmentFormatters(new Formatter[]{new EmptyFormatter()}); + testEmpty(instance); + + instance = createHandlerWithRecords(); + instance.setFormatter(new EmptyFormatter()); + testEmpty(instance); + } + + private void testEmpty(MailHandler instance) { + Properties props = instance.getMailProperties(); + props.setProperty("mail.from", "localhost@localdomain"); + props.setProperty("mail.to", "localhost@localdomain"); + instance.setMailProperties(props); + + MessageErrorManager empty = new MessageErrorManager(instance.getMailProperties()) { + + @Override + public void error(MimeMessage msg, Throwable t, int code) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try { + msg.saveChanges(); + msg.writeTo(out); + } catch (Throwable ex) { + fail(ex.toString()); + } + } + }; + instance.setErrorManager(empty); + instance.close(); + } + + private void testAttachmentInvariants(boolean error) throws Exception { + MailHandler target = new MailHandler(createInitProperties("")); + try { + InternalErrorManager em = internalErrorManagerFrom(target); + if (error) { + assertFalse(em.exceptions.isEmpty()); + boolean unexpected = false; + for (Exception e : em.exceptions) { + if (e instanceof IndexOutOfBoundsException == false) { + dump(e); + unexpected = true; + } + } + assertFalse(unexpected); + } else { + for (Exception e : em.exceptions) { + dump(e); + } + assertTrue(em.exceptions.isEmpty()); + } + int len = target.getAttachmentFormatters().length; + assertTrue(String.valueOf(len), len > 0); + assertEquals(len, target.getAttachmentFilters().length); + assertEquals(len, target.getAttachmentNames().length); + } finally { + target.close(); + } + } + + @Test + public void testAlignEmptyFilter() throws Exception { + String p = MailHandler.class.getName(); + Properties props = createInitProperties(p); + props.put(p.concat(".attachment.formatters"), SimpleFormatter.class.getName()); + props.put(p.concat(".attachment.names"), "att.txt"); + final LogManager manager = LogManager.getLogManager(); + try { + read(manager, props); + testAttachmentInvariants(false); + } finally { + manager.reset(); + } + } + + @Test + public void testAlignEmptyNames() throws Exception { + String p = MailHandler.class.getName(); + Properties props = createInitProperties(p); + props.put(p.concat(".attachment.formatters"), SimpleFormatter.class.getName()); + props.put(p.concat(".attachment.filters"), ErrorFilter.class.getName()); + final LogManager manager = LogManager.getLogManager(); + try { + read(manager, props); + testAttachmentInvariants(false); + } finally { + manager.reset(); + } + } + + @Test + public void testAlignEmptyFilterAndNames() throws Exception { + String p = MailHandler.class.getName(); + Properties props = createInitProperties(p); + props.put(p.concat(".attachment.formatters"), SimpleFormatter.class.getName()); + final LogManager manager = LogManager.getLogManager(); + try { + read(manager, props); + testAttachmentInvariants(false); + } finally { + manager.reset(); + } + } + + @Test + public void testAlignErrorFilter() throws Exception { + String p = MailHandler.class.getName(); + Properties props = createInitProperties(p); + props.put(p.concat(".attachment.formatters"), + SimpleFormatter.class.getName() + ", " + SimpleFormatter.class.getName()); + props.put(p.concat(".attachment.filters"), ErrorFilter.class.getName()); + final LogManager manager = LogManager.getLogManager(); + try { + read(manager, props); + testAttachmentInvariants(true); + } finally { + manager.reset(); + } + } + + @Test + public void testAlignErrorNames() throws Exception { + String p = MailHandler.class.getName(); + Properties props = createInitProperties(p); + props.put(p.concat(".attachment.formatters"), SimpleFormatter.class.getName()); + props.put(p.concat(".attachment.names"), "att.txt, extra.txt"); + final LogManager manager = LogManager.getLogManager(); + try { + read(manager, props); + testAttachmentInvariants(true); + } finally { + manager.reset(); + } + } + + @Test + public void testAlignErrorFilterAndNames() throws Exception { + String p = MailHandler.class.getName(); + Properties props = createInitProperties(p); + props.put(p.concat(".attachment.formatters"), + SimpleFormatter.class.getName() + ", " + SimpleFormatter.class.getName()); + props.put(p.concat(".attachment.filters"), + ErrorFilter.class.getName() + "," + ErrorFilter.class.getName() + + "," + ErrorFilter.class.getName()); + props.put(p.concat(".attachment.names"), "att.txt, next.txt, extra.txt"); + final LogManager manager = LogManager.getLogManager(); + try { + read(manager, props); + testAttachmentInvariants(true); + } finally { + manager.reset(); + } + } + + @Test + public void testEncoding() throws Exception { + final String enc = "iso8859_1"; + //names are different but equal encodings. + final String found = MimeUtility.mimeCharset(enc); + Class<?> k = Session.getInstance(new Properties()) + .getTransport("smtp").getClass(); + if (isPrivateSpec(k)) { + assertFalse(enc + "==" + found, enc.equals(found)); + } + + LogManager manager = LogManager.getLogManager(); + final MailHandler instance = new MailHandler(createInitProperties("")); + MessageErrorManager em = new MessageErrorManager(instance.getMailProperties()) { + + @Override + protected void error(MimeMessage msg, Throwable t, int code) { + try { + MimeMultipart multi = (MimeMultipart) msg.getContent(); + BodyPart body = multi.getBodyPart(0); + assertEquals(Part.INLINE, body.getDisposition()); + ContentType ct = new ContentType(body.getContentType()); + assertEquals(MimeUtility.mimeCharset(enc), ct.getParameter("charset")); + + BodyPart attach = multi.getBodyPart(1); + ct = new ContentType(attach.getContentType()); + assertEquals(MimeUtility.mimeCharset(enc), ct.getParameter("charset")); + } catch (Throwable E) { + dump(E); + fail(E.toString()); + } + } + }; + + instance.setErrorManager(em); + Properties props = createInitProperties(""); + props.put("mail.to", "localhost@localdomain"); + instance.setMailProperties(props); + instance.setAttachmentFormatters(new Formatter[]{new XMLFormatter()}); + instance.setAttachmentNames(new String[]{"all.xml"}); + String p = instance.getClass().getName(); + + assertEquals(manager.getProperty(p.concat(".encoding")), instance.getEncoding()); + try { + instance.setEncoding("unsupported encoding exception"); + fail("Missing encoding check."); + } catch (UnsupportedEncodingException expect) { + } + assertEquals(manager.getProperty(p.concat(".encoding")), instance.getEncoding()); + + assertTrue(em.exceptions.isEmpty()); + + instance.setEncoding(enc); + assertEquals(enc, instance.getEncoding()); + instance.setSubject("ORA-17043=Ung\u00FCltige maximale Stream-Gr\u00F6\u00DFe"); + LogRecord record = new LogRecord(Level.SEVERE, "Zeit\u00FCberschreitung bei Anweisung"); + instance.publish(record); + instance.close(); + } + + @Test + public void testErrorManager() { + MailHandler h = new MailHandler(); + assertNotNull(h.getErrorManager()); + try { + h.setErrorManager((ErrorManager) null); + } catch (NullPointerException expect) { + assertNotNull(h); + } + assertNotNull(h.getErrorManager()); + + ErrorManager em = new ErrorManager(); + h.setErrorManager(em); + assertSame(em, h.getErrorManager()); + } + + @Test + public void testFormatter() { + MailHandler h = new MailHandler(); + assertNotNull(h.getFormatter()); + try { + h.setFormatter((Formatter) null); + } catch (NullPointerException expect) { + assertNotNull(h); + } + assertNotNull(h.getFormatter()); + + SimpleFormatter f = new SimpleFormatter(); + h.setFormatter(f); + assertSame(f, h.getFormatter()); + } + + @Test + public void testFilter() { + MailHandler h = new MailHandler(); + assertNull(h.getFilter()); + h.setFilter((Filter) null); + assertNull(h.getFilter()); + + BooleanFilter f = new BooleanFilter(); + h.setFilter(f); + assertSame(f, h.getFilter()); + + h.setFilter((Filter) null); + assertNull(h.getFilter()); + } + + @Test + public void testStatefulFilter() { + MailHandler h = new MailHandler(); + h.setMailProperties(createInitProperties("")); + InternalErrorManager em = new FlushErrorManager(h); + h.setErrorManager(em); + CountingFilter cf = new CountingFilter(); + h.setFilter(cf); + int MAX_RECORDS = 100; + for (int i = 0; i < MAX_RECORDS; i++) { + LogRecord r = new LogRecord(Level.SEVERE, ""); + h.publish(r); + } + h.close(); + assertEquals(MAX_RECORDS, cf.count); + for (Exception exception : em.exceptions) { + if (!isConnectOrTimeout(exception)) { + dump(exception); + fail(String.valueOf(exception)); + } + } + assertFalse(em.exceptions.isEmpty()); + } + + @Test + public void testStatefulAttachmentFilter() { + MailHandler h = new MailHandler(); + h.setMailProperties(createInitProperties("")); + InternalErrorManager em = new FlushErrorManager(h); + h.setErrorManager(em); + CountingFilter negativeOne = new CountingFilter(BooleanFilter.FALSE); + h.setFilter(negativeOne); + h.setAttachmentFormatters(new SimpleFormatter(), new SimpleFormatter(), + new SimpleFormatter()); + CountingFilter one = new CountingFilter(BooleanFilter.FALSE); + CountingFilter two = new CountingFilter(); + h.setAttachmentFilters(BooleanFilter.FALSE, one, two); + int MAX_RECORDS = 100; + for (int i = 0; i < MAX_RECORDS; i++) { + LogRecord r = new LogRecord(Level.SEVERE, ""); + h.publish(r); + } + h.close(); + + assertEquals(MAX_RECORDS, negativeOne.count); + assertEquals(MAX_RECORDS, one.count); + assertEquals(MAX_RECORDS, two.count); + for (Exception exception : em.exceptions) { + if (!isConnectOrTimeout(exception)) { + dump(exception); + fail(String.valueOf(exception)); + } + } + assertFalse(em.exceptions.isEmpty()); + } + + @Test + public void testStatefulInternAttachmentFilter() { + testStatefulAttachmentFilter(false); + } + + private void testStatefulAttachmentFilter(boolean clear) { + MailHandler h = new MailHandler(); + h.setMailProperties(createInitProperties("")); + InternalErrorManager em = new FlushErrorManager(h); + h.setErrorManager(em); + CountingFilter cf = new CountingFilter(BooleanFilter.TRUE); + h.setFilter(cf); + h.setAttachmentFormatters(new SimpleFormatter(), new SimpleFormatter()); + CountingFilter one = new CountingFilter(); + h.setAttachmentFilters(cf, one); + int MAX_RECORDS = 100; + for (int i = 0; i < MAX_RECORDS; i++) { + LogRecord r = new LogRecord(Level.SEVERE, ""); + h.publish(r); + } + + if (clear) { + Filter[] af = h.getAttachmentFilters(); + h.setAttachmentFormatters(); + h.setAttachmentFormatters(new SimpleFormatter(), + new SimpleFormatter()); + h.setAttachmentFilters(af); + } + h.close(); + + assertEquals(MAX_RECORDS, cf.count); + assertEquals(MAX_RECORDS, one.count); + for (Exception exception : em.exceptions) { + if (!isConnectOrTimeout(exception)) { + dump(exception); + fail(String.valueOf(exception)); + } + } + assertFalse(em.exceptions.isEmpty()); + } + + @Test + public void testStatefulAttachmentFilterClearMatches() { + testStatefulAttachmentFilter(true); + } + + @Test + public void testStatefulPushFilter() { + MailHandler h = new MailHandler(); + h.setMailProperties(createInitProperties("")); + InternalErrorManager em = new PushErrorManager(h); + h.setErrorManager(em); + CountingFilter cf = new CountingFilter(); + h.setFilter(cf); + h.setPushLevel(Level.ALL); + h.setPushFilter(cf); + LogRecord r = new LogRecord(Level.SEVERE, ""); + h.publish(r); + h.close(); + assertEquals(1, cf.count); + for (Exception exception : em.exceptions) { + if (!isConnectOrTimeout(exception)) { + dump(exception); + fail(String.valueOf(exception)); + } + } + assertFalse(em.exceptions.isEmpty()); + } + + private void testStatefulPushAttachmentFilter(boolean clear) { + final MailHandler h = new MailHandler(); + h.setMailProperties(createInitProperties("")); + final InternalErrorManager em = new PushErrorManager(h); + h.setErrorManager(em); + final CountingFilter cf = new CountingFilter(BooleanFilter.FALSE); + h.setFilter(cf); + h.setPushLevel(Level.ALL); + final CountingFilter push = new CountingFilter(); + h.setPushFilter(push); + h.setAttachmentFormatters(new SimpleFormatter(), new SimpleFormatter(), new SimpleFormatter()); + final CountingFilter one = new CountingFilter(BooleanFilter.FALSE); + final CountingFilter two = new CountingFilter(BooleanFilter.FALSE); + + if (clear) { + h.setAttachmentFilters(one, two, + new Filter() { + @Override + public boolean isLoggable(LogRecord record) { + h.setAttachmentFormatters(new SimpleFormatter(), + new SimpleFormatter()); + h.setAttachmentFilters(one, push); + return push.isLoggable(record); + } + + }); + } else { + h.setAttachmentFilters(one, two, push); + } + + LogRecord r = new LogRecord(Level.SEVERE, ""); + h.publish(r); + h.close(); + + if (clear) { + assertEquals(2, cf.count); + assertEquals(2, one.count); + assertEquals(1, two.count); + assertEquals(3, push.count); + } else { + assertEquals(1, cf.count); + assertEquals(1, one.count); + assertEquals(1, two.count); + assertEquals(1, push.count); + } + for (Exception exception : em.exceptions) { + if (!isConnectOrTimeout(exception)) { + dump(exception); + fail(String.valueOf(exception)); + } + } + assertFalse(em.exceptions.isEmpty()); + } + + @Test + public void testStatefulPushAttachmentFilter() { + testStatefulPushAttachmentFilter(false); + } + + @Test + public void testStatefulPushFilterClearMatches() { + testStatefulPushAttachmentFilter(true); + } + + @Test + public void testPushInsidePush() { + final Level[] lvls = getAllLevels(); + + MailHandler instance = new MailHandler(lvls.length + 2); + InternalErrorManager em = new InternalErrorManager(); + instance.setErrorManager(em); + instance.setMailProperties(createInitProperties("")); + instance.setLevel(Level.ALL); + instance.setFilter((Filter) null); + instance.setPushLevel(Level.OFF); + instance.setPushFilter((Filter) null); + + instance.setFormatter(new SimpleFormatter() { + + @Override + public String getHead(Handler h) { + assert h instanceof MailHandler : h; + try { + h.flush(); + } catch (Throwable T) { + fail(T.toString()); + } + return super.getHead(h); + } + + @Override + public String getTail(Handler h) { + assert h instanceof MailHandler : h; + final Filter filter = h.getFilter(); + try { + h.setFilter(filter); + } catch (Throwable T) { + fail(T.toString()); + } + + final Level lvl = h.getLevel(); + try { + h.setLevel(lvl); + } catch (Throwable T) { + fail(T.toString()); + } + + final String enc = h.getEncoding(); + try { + h.setEncoding(enc); + } catch (Throwable T) { + fail(T.toString()); + } + + try { + h.setFormatter(new SimpleFormatter()); + } catch (Throwable T) { + fail(T.toString()); + } + + try { + h.close(); + assertEquals(h.getLevel(), Level.OFF); + } catch (Throwable T) { + fail(T.toString()); + } + return super.getTail(h); + } + }); + + Formatter push = new SimpleFormatter() { + + @Override + public String getHead(Handler h) { + assert h instanceof MailHandler : h; + try { + ((MailHandler) h).push(); + } catch (Throwable T) { + fail(T.toString()); + } + return super.getHead(h); + } + + @Override + public String getTail(Handler h) { + assert h instanceof MailHandler : h; + try { + ((MailHandler) h).push(); + } catch (Throwable T) { + fail(T.toString()); + } + return super.getTail(h); + } + }; + + Formatter atFor = new SimpleFormatter() { + + @Override + public String getHead(Handler h) { + assert h instanceof MailHandler : h; + MailHandler mh = (MailHandler) h; + Formatter[] f = mh.getAttachmentFormatters(); + try { + mh.setAttachmentFormatters(f); + fail("Mutable formatter."); + } catch (IllegalStateException pass) { + } catch (Throwable T) { + fail(T.toString()); + } + return super.getHead(h); + } + + @Override + public String getTail(Handler h) { + getHead(h); + return super.getTail(h); + } + }; + + Formatter atName = new SimpleFormatter() { + + @Override + public String getHead(Handler h) { + assert h instanceof MailHandler : h; + MailHandler mh = (MailHandler) h; + Formatter[] f = mh.getAttachmentNames(); + try { + mh.setAttachmentNames(f); + fail("Mutable formatter."); + } catch (IllegalStateException pass) { + } catch (Throwable T) { + fail(T.toString()); + } + return super.getHead(h); + } + + @Override + public String getTail(Handler h) { + getHead(h); + return super.getTail(h); + } + }; + + Formatter atFilter = new SimpleFormatter() { + + @Override + public String getHead(Handler h) { + assert h instanceof MailHandler; + MailHandler mh = (MailHandler) h; + Filter[] f = mh.getAttachmentFilters(); + try { + mh.setAttachmentFilters(f); + fail("Mutable filters."); + } catch (IllegalStateException pass) { + } catch (Throwable T) { + fail(T.toString()); + } + return super.getHead(h); + } + + @Override + public String getTail(Handler h) { + getHead(h); + return super.getTail(h); + } + }; + + Formatter nameComp = new Formatter() { + + @Override + public String getHead(Handler h) { + assert h instanceof MailHandler : h; + MailHandler mh = (MailHandler) h; + Comparator<? super LogRecord> c = mh.getComparator(); + try { + mh.setComparator(c); + fail("Mutable comparator."); + } catch (IllegalStateException pass) { + } catch (Throwable T) { + fail(T.toString()); + } + return super.getHead(h); + } + + @Override + public String format(LogRecord r) { + return ""; + } + + @Override + public String getTail(Handler h) { + getHead(h); + return "name.txt"; + } + }; + + Formatter nameMail = new Formatter() { + + @Override + public String getHead(Handler h) { + assert h instanceof MailHandler : h; + MailHandler mh = (MailHandler) h; + Properties props = mh.getMailProperties(); + try { + mh.setMailProperties(props); + fail("Mutable props."); + } catch (IllegalStateException pass) { + } catch (Throwable T) { + fail(T.toString()); + } + return super.getHead(h); + } + + @Override + public String format(LogRecord r) { + return ""; + } + + @Override + public String getTail(Handler h) { + getHead(h); + return "name.txt"; + } + }; + + Formatter nameSub = new Formatter() { + + @Override + public String getHead(Handler h) { + assert h instanceof MailHandler : h; + MailHandler mh = (MailHandler) h; + Formatter f = mh.getSubject(); + try { + mh.setSubject(f); + fail("Mutable subject."); + } catch (IllegalStateException pass) { + } catch (Throwable T) { + fail(T.toString()); + } + return super.getHead(h); + } + + @Override + public String format(LogRecord r) { + return ""; + } + + @Override + public String getTail(Handler h) { + getHead(h); + return "name.txt"; + } + }; + + Formatter nameAuth = new Formatter() { + + @Override + public String getHead(Handler h) { + assert h instanceof MailHandler : h; + MailHandler mh = (MailHandler) h; + Authenticator a = mh.getAuthenticator(); + try { + mh.setAuthenticator(a); + fail("Mutable Authenticator."); + } catch (IllegalStateException pass) { + } catch (Throwable T) { + fail(T.toString()); + } + return super.getHead(h); + } + + @Override + public String format(LogRecord r) { + return ""; + } + + @Override + public String getTail(Handler h) { + getHead(h); + return "name.txt"; + } + }; + + instance.setAttachmentFormatters( + new Formatter[]{push, atFor, atName, atFilter}); + instance.setAttachmentNames( + new Formatter[]{nameComp, nameMail, nameSub, nameAuth}); + + String SOURCE_CLASS = MailHandlerTest.class.getName(); + String SOURCE_METHOD = "testPushInsidePush"; + for (Level lvl : lvls) { + LogRecord r = new LogRecord(lvl, ""); + r.setSourceClassName(SOURCE_CLASS); + r.setSourceMethodName(SOURCE_METHOD); + instance.publish(r); + } + instance.flush(); + + for (Exception exception : em.exceptions) { + Throwable t = exception; + if ((t instanceof MessagingException == false) + && (t instanceof IllegalStateException == false)) { + dump(t); + fail(String.valueOf(t)); + } + } + assertFalse(em.exceptions.isEmpty()); + } + + @Test + public void testPush() { + MailHandler instance = new MailHandler(createInitProperties("")); + InternalErrorManager em = new InternalErrorManager(); + instance.setErrorManager(em); + instance.push(); + assertEquals(true, em.exceptions.isEmpty()); + instance.close(); + + instance = createHandlerWithRecords(); + em = internalErrorManagerFrom(instance); + instance.push(); + + assertEquals(1, em.exceptions.size()); + assertEquals(true, em.exceptions.get(0) instanceof MessagingException); + instance.close(); + + //Test for valid message. + instance = createHandlerWithRecords(); + instance.setErrorManager(new PushErrorManager(instance)); + instance.push(); + instance.close(); + + instance = new MailHandler(1); + instance.setMailProperties(createInitProperties("")); + instance.setLevel(Level.ALL); + instance.setErrorManager(new PushErrorManager(instance)); + instance.setPushFilter((Filter) null); + instance.setPushLevel(Level.INFO); + LogRecord record = new LogRecord(Level.SEVERE, ""); + instance.publish(record); //should push. + instance.close(); //cause a flush if publish didn't push. + } + + @Test + public void testFlush() { + MailHandler instance = new MailHandler(createInitProperties("")); + InternalErrorManager em = new InternalErrorManager(); + instance.setErrorManager(em); + instance.flush(); + + assertEquals(true, em.exceptions.isEmpty()); + instance.close(); + + instance = createHandlerWithRecords(); + em = internalErrorManagerFrom(instance); + instance.flush(); + + assertEquals(1, em.exceptions.size()); + assertEquals(true, em.exceptions.get(0) instanceof MessagingException); + instance.close(); + + //Test for valid message. + instance = createHandlerWithRecords(); + instance.setErrorManager(new FlushErrorManager(instance)); + instance.flush(); + instance.close(); + + instance = new MailHandler(1); + instance.setMailProperties(createInitProperties("")); + instance.setLevel(Level.ALL); + instance.setErrorManager(new FlushErrorManager(instance)); + instance.setPushFilter((Filter) null); + instance.setPushLevel(Level.SEVERE); + LogRecord record = new LogRecord(Level.INFO, ""); + instance.publish(record); //should flush. + instance.push(); //make FlushErrorManager fail if handler didn't flush. + instance.close(); + } + + @Test + public void testFlushLinkageError() throws Exception { + testLinkageErrorWithStack("flush"); + } + + @Test + public void testFlushLinkageErrorEmpty() throws Exception { + testLinkageErrorEmptyStack("flush"); + } + + @Test + public void testClose() { + LogRecord record = new LogRecord(Level.INFO, ""); + MailHandler instance = new MailHandler(createInitProperties("")); + InternalErrorManager em = new InternalErrorManager(); + instance.setErrorManager(em); + int capacity = instance.getCapacity(); + + assertNotNull(instance.getLevel()); + + instance.setLevel(Level.ALL); + assertEquals(true, instance.isLoggable(record)); + + instance.close(); + + assertEquals(false, instance.isLoggable(record)); + assertEquals(Level.OFF, instance.getLevel()); + + instance.setLevel(Level.ALL); + assertEquals(Level.OFF, instance.getLevel()); + + assertEquals(capacity, instance.getCapacity()); + assertEquals(true, em.exceptions.isEmpty()); + + instance = createHandlerWithRecords(); + em = internalErrorManagerFrom(instance); + instance.close(); + + assertEquals(1, em.exceptions.size()); + assertEquals(true, em.exceptions.get(0) instanceof MessagingException); + + //Test for valid message. + instance = createHandlerWithRecords(); + instance.setErrorManager(new FlushErrorManager(instance)); + instance.close(); + } + + @Test + public void testCloseLinkageError() throws Exception { + testLinkageErrorWithStack("close"); + } + + @Test + public void testCloseLinkageErrorEmpty() throws Exception { + testLinkageErrorEmptyStack("close"); + } + + private void testLinkageErrorWithStack(String method) throws IOException { + PrintStream ls = new LinkageErrorStream(); + @SuppressWarnings("UseOfSystemOutOrSystemErr") + final PrintStream err = System.err; + try { + System.setErr(new LinkageErrorStream()); + boolean linkageErrorEscapes = false; + try { + ErrorManager em = new ErrorManager(); + em.error(null, null, ErrorManager.GENERIC_FAILURE); + } catch (LinkageError expect) { + linkageErrorEscapes = expect.getStackTrace().length != 0; + } + Assume.assumeTrue(linkageErrorEscapes); + + System.setErr(ls); + MailHandler instance = new MailHandler(createInitProperties("")); + try { + assertEquals(ErrorManager.class, + instance.getErrorManager().getClass()); + instance.publish(new LogRecord(Level.SEVERE, "")); + if ("preDestroy".equals(method)) { + instance.preDestroy(); + } else if ("publish".equals(method)) { + instance.setPushLevel(Level.ALL); + instance.publish(new LogRecord(Level.SEVERE, "")); + } else if ("publishDuringClose".equals(method)) { + CloseLogRecord r + = new CloseLogRecord(Level.SEVERE, "", instance); + instance.publish(r); + } else if ("flush".equals(method)) { + instance.flush(); + } else if ("push".equals(method)) { + instance.push(); + } else if ("close".equals(method)) { + instance.close(); + } else { + fail(method); + } + assertTrue(ls.checkError()); + } finally { + instance.close(); + } + assertTrue(ls.checkError()); + } finally { + System.setErr(err); + } + } + + private void testLinkageErrorEmptyStack(String method) throws IOException { + PrintStream ls = new LinkageErrorStream(new StackTraceElement[0]); + @SuppressWarnings("UseOfSystemOutOrSystemErr") + final PrintStream err = System.err; + final Thread.UncaughtExceptionHandler ueh + = Thread.currentThread().getUncaughtExceptionHandler(); + try { + CountingUncaughtExceptionHandler cueh + = new CountingUncaughtExceptionHandler(); + System.setErr(new LinkageErrorStream(new StackTraceElement[0])); + Thread.currentThread().setUncaughtExceptionHandler(cueh); + boolean linkageErrorEscapes = false; + try { + ErrorManager em = new ErrorManager(); + em.error(null, null, ErrorManager.GENERIC_FAILURE); + } catch (LinkageError expect) { + linkageErrorEscapes = true; + } + Assume.assumeTrue(linkageErrorEscapes); + + System.setErr(ls); + MailHandler instance = new MailHandler(createInitProperties("")); + try { + assertEquals(ErrorManager.class, + instance.getErrorManager().getClass()); + instance.publish(new LogRecord(Level.SEVERE, "")); + if ("preDestroy".equals(method)) { + instance.preDestroy(); + } else if ("publish".equals(method)) { + instance.setPushLevel(Level.ALL); + instance.publish(new LogRecord(Level.SEVERE, "")); + } else if ("publishDuringClose".equals(method)) { + CloseLogRecord r + = new CloseLogRecord(Level.SEVERE, "", instance); + instance.publish(r); + } else if ("flush".equals(method)) { + instance.flush(); + } else if ("push".equals(method)) { + instance.push(); + } else if ("close".equals(method)) { + instance.close(); + } else { + fail(method); + } + assertTrue(ls.checkError()); + } finally { + instance.close(); + } + assertTrue(ls.checkError()); + assertEquals(1, cueh.count); + } finally { + System.setErr(err); + Thread.currentThread().setUncaughtExceptionHandler(ueh); + } + } + + @Test + public void testCloseContextClassLoader() { + assertNull(System.getSecurityManager()); + final Thread thread = Thread.currentThread(); + final ClassLoader ccl = thread.getContextClassLoader(); + try { + testCloseContextClassLoader0(); + } finally { + thread.setContextClassLoader(ccl); + } + assertNull(System.getSecurityManager()); + } + + private void testCloseContextClassLoader0() { + + InternalErrorManager em = new ActivationErrorManager(); + MailHandler instance = createHandlerWithRecords(); + try { + instance.setErrorManager(em); + ClassLoader expect = instance.getClass().getClassLoader(); + assertNotNull(expect); + instance.setAuthenticator(new ClassLoaderAuthenticator(expect)); + + /** + * java.util.logging.LogManager$Cleaner has a null CCL. + */ + Thread.currentThread().setContextClassLoader(null); + } finally { + instance.close(); + } + + for (Exception exception : em.exceptions) { + Throwable t = exception; + if (!isConnectOrTimeout(t)) { + dump(t); + fail(t.toString()); + } + } + } + + @Test + public void testLevel() { + MailHandler instance = new MailHandler(createInitProperties("")); + InternalErrorManager em = new InternalErrorManager(); + instance.setErrorManager(em); + + assertNotNull(instance.getLevel()); + try { + instance.setLevel((Level) null); + fail("Null level was allowed"); + } catch (NullPointerException pass) { + assertNotNull(instance); + } catch (RuntimeException re) { + fail(re.toString()); + } + assertNotNull(instance.getLevel()); + + final Level[] lvls = getAllLevels(); + for (Level lvl : lvls) { + instance.setLevel(lvl); + assertEquals(instance.getLevel(), lvl); + } + + instance.setLevel(Level.WARNING); + instance.close(); + assertEquals(Level.OFF, instance.getLevel()); + for (Level lvl : lvls) { + instance.setLevel(lvl); + assertEquals(Level.OFF, instance.getLevel()); + } + assertEquals(true, em.exceptions.isEmpty()); + } + + @Test + public void testLevelBeforeClose() { + MailHandler instance = this.createHandlerWithRecords(); + InternalErrorManager em = new InternalErrorManager(); + instance.setErrorManager(em); + + final Level expect = Level.WARNING; + instance.setLevel(expect); + + instance.setFormatter(new LevelCheckingFormatter(expect)); + instance.close(); + + for (Exception exception : em.exceptions) { + Throwable t = exception; + if (t instanceof MessagingException) { + if (!isConnectOrTimeout(t)) { + dump(t); + fail(t.toString()); + } + } else { + dump(t); + fail(t.toString()); + } + } + assertFalse(em.exceptions.isEmpty()); + } + + @Test + public void testLevelAfterClose() { + MailHandler instance = new MailHandler(createInitProperties("")); + InternalErrorManager em = new InternalErrorManager(); + instance.setErrorManager(em); + + instance.setLevel(Level.WARNING); + instance.setFormatter(new LevelCheckingFormatter(Level.OFF)); + instance.publish(new CloseLogRecord(Level.SEVERE, "", instance)); + assertEquals(Level.OFF, instance.getLevel()); + + instance.close(); + for (Exception exception : em.exceptions) { + Throwable t = exception; + if (t instanceof MessagingException) { + if (!isConnectOrTimeout(t)) { + dump(t); + fail(t.toString()); + } + } else { + dump(t); + fail(t.toString()); + } + } + assertFalse(em.exceptions.isEmpty()); + } + + @Test + public void testLogManagerReset() throws IOException { + LogManager manager = LogManager.getLogManager(); + try { + assertEquals(LogManager.class, manager.getClass()); + MailHandler instance = startLogManagerReset("remote"); + InternalErrorManager em = internalErrorManagerFrom(instance); + + manager.reset(); + + for (Exception exception : em.exceptions) { + Throwable t = exception; + if (t instanceof MessagingException) { + if (isNoRecipientAddress(t)) { + continue; + } + if (!isConnectOrTimeout(t)) { + dump(t); + fail(t.toString()); + } + } else { + dump(t); + fail(t.toString()); + } + } + + instance = startLogManagerReset("local"); + em = internalErrorManagerFrom(instance); + + for (Exception exception : em.exceptions) { + Throwable t = exception; + if (t instanceof MessagingException) { + if (isNoRecipientAddress(t)) { + continue; + } + if (!isConnectOrTimeout(t)) { + dump(t); + fail(t.toString()); + } + } else { + dump(t); + fail(t.toString()); + } + } + + manager.reset(); + + for (Exception exception : em.exceptions) { + Throwable t = exception; + if (t instanceof MessagingException) { + if (isNoRecipientAddress(t)) { + continue; + } + if (!isConnectOrTimeout(t)) { + dump(t); + fail(t.toString()); + } + } else { + dump(t); + fail(t.toString()); + } + } + + String[] noVerify = new String[]{null, "", "null"}; + for (int v = 0; v < noVerify.length; v++) { + instance = startLogManagerReset(noVerify[v]); + em = internalErrorManagerFrom(instance); + + for (Exception exception : em.exceptions) { + Throwable t = exception; + dump(t); + fail("Verify index=" + v); + } + + manager.reset(); + + //No verify results in failed send. + for (Exception exception : em.exceptions) { + Throwable t = exception; + if (t instanceof MessagingException) { + if (isNoRecipientAddress(t)) { + continue; + } + if (!isConnectOrTimeout(t)) { + dump(t); + fail("Verify index=" + v); + } + } else { + dump(t); + fail("Verify index=" + v); + } + } + } + + instance = startLogManagerReset("bad-enum-name"); + em = internalErrorManagerFrom(instance); + + manager.reset(); + + //Allow the LogManagerProperties to copy on a bad enum type. + boolean foundIllegalArg = false; + for (Exception exception : em.exceptions) { + Throwable t = exception; + if (t instanceof IllegalArgumentException) { + foundIllegalArg = true; + } else if (t instanceof RuntimeException) { + dump(t); + fail(t.toString()); + } + } + + assertTrue(foundIllegalArg); + assertFalse(em.exceptions.isEmpty()); + } finally { + hardRef = null; + manager.reset(); + } + } + + /** + * Setup and load the standard properties. + * + * @param verify the value of verify enum. + * @return a MailHandler + * @throws IOException if there is a problem. + */ + private MailHandler startLogManagerReset(String verify) throws IOException { + LogManager manager = LogManager.getLogManager(); + manager.reset(); + + final String p = MailHandler.class.getName(); + Properties props = createInitProperties(p); + props.put(p.concat(".mail.from"), "localhost@localdomain"); + props.put(p.concat(".mail.to"), ""); + props.put(p.concat(".mail.cc"), ""); + props.put(p.concat(".mail.bcc"), ""); + props.put(p.concat(".subject"), p.concat(" test")); + props.put(p.concat(".errorManager"), InternalErrorManager.class.getName()); + if (verify != null) { + props.put(p.concat(".verify"), verify); + } + read(manager, props); + + assertNotNull(manager.getProperty(p.concat(".mail.host"))); + assertNotNull(manager.getProperty(p.concat(".mail.smtp.host"))); + assertNotNull(manager.getProperty(p.concat(".mail.smtp.port"))); + assertNotNull(manager.getProperty(p.concat(".mail.to"))); + assertNotNull(manager.getProperty(p.concat(".mail.cc"))); + assertNotNull(manager.getProperty(p.concat(".subject"))); + assertNotNull(manager.getProperty(p.concat(".mail.from"))); + assertEquals(verify, manager.getProperty(p.concat(".verify"))); + assertNotNull(manager.getProperty(p.concat(".mail.smtp.connectiontimeout"))); + assertNotNull(manager.getProperty(p.concat(".mail.smtp.timeout"))); + + MailHandler instance = new MailHandler(10); + instance.setLevel(Level.ALL); + + //Don't auto compute a default recipient. + Properties bug7092981 = createInitProperties(""); + bug7092981.setProperty("mail.to", ""); + bug7092981.setProperty("mail.cc", ""); + bug7092981.setProperty("mail.bcc", ""); + instance.setMailProperties(bug7092981); + + assertEquals(InternalErrorManager.class, instance.getErrorManager().getClass()); + + final String CLASS_NAME = MailHandlerTest.class.getName(); + Logger logger = Logger.getLogger(CLASS_NAME); + hardRef = logger; + logger.setUseParentHandlers(false); + logger.addHandler(instance); + + logger.log(Level.SEVERE, "Verify is {0}.", verify); + logger.log(Level.SEVERE, "Verify is {0}.", verify); + return instance; + } + + @Test + public void testPushLevel() { + MailHandler instance = new MailHandler(createInitProperties("")); + InternalErrorManager em = new InternalErrorManager(); + instance.setErrorManager(em); + + assertNotNull(instance.getPushLevel()); + + try { + instance.setPushLevel((Level) null); + fail("Null level was allowed"); + } catch (NullPointerException pass) { + } catch (RuntimeException re) { + fail(re.toString()); + } + + final Level[] lvls = getAllLevels(); + for (Level lvl : lvls) { + instance.setPushLevel(lvl); + assertEquals(instance.getPushLevel(), lvl); + } + + instance.close(); + for (Level lvl : lvls) { + instance.setPushLevel(lvl); + assertEquals(instance.getPushLevel(), lvl); + } + assertEquals(true, em.exceptions.isEmpty()); + } + + @Test + public void testPushLinkageError() throws Exception { + testLinkageErrorWithStack("push"); + } + + @Test + public void testPushLinkageErrorEmpty() throws Exception { + testLinkageErrorEmptyStack("push"); + } + + @Test + public void testPushFilter() { + MailHandler instance = new MailHandler(createInitProperties("")); + InternalErrorManager em = new InternalErrorManager(); + instance.setErrorManager(em); + + try { + instance.setPushFilter((Filter) null); + } catch (RuntimeException RE) { + fail(RE.toString()); + } + assertNull(instance.getPushFilter()); + + instance.setPushFilter(BooleanFilter.TRUE); + assertEquals(BooleanFilter.TRUE, instance.getPushFilter()); + + assertEquals(true, em.exceptions.isEmpty()); + + instance = createHandlerWithRecords(); + instance.setErrorManager(new PushErrorManager(instance)); + instance.setPushFilter(BooleanFilter.TRUE); + instance.setLevel(Level.ALL); + instance.setPushLevel(Level.WARNING); + instance.publish(new LogRecord(Level.SEVERE, "")); + instance.close(); + } + + @Test + public void testContentTypeOf() throws IOException { + MailHandler instance = new MailHandler(createInitProperties("")); + InternalErrorManager em = new InternalErrorManager(); + instance.setErrorManager(em); + instance.setEncoding((String) null); + String head = instance.contentTypeOf(new XMLFormatter().getHead(instance)); + assertEquals("application/xml", head); + instance.setEncoding("US-ASCII"); + + head = instance.contentTypeOf(new XMLFormatter().getHead(instance)); + assertEquals("application/xml", head); + + instance.setEncoding((String) null); + head = instance.contentTypeOf(new SimpleFormatter().getHead(instance)); + assertNull(head); + + instance.setEncoding("US-ASCII"); + head = instance.contentTypeOf(new SimpleFormatter().getHead(instance)); + assertNull(head); + + instance.setEncoding((String) null); + head = instance.contentTypeOf(new HeadFormatter("<HTML><BODY>").getHead(instance)); + assertEquals("text/html", head); + + instance.setEncoding((String) null); + head = instance.contentTypeOf(new HeadFormatter("<html><body>").getHead(instance)); + assertEquals("text/html", head); + + instance.setEncoding("US-ASCII"); + head = instance.contentTypeOf(new HeadFormatter("<HTML><BODY>").getHead(instance)); + assertEquals("text/html", head); + + instance.setEncoding("US-ASCII"); + head = instance.contentTypeOf(new HeadFormatter("<HTML><HEAD></HEAD>" + + "<BODY></BODY></HTML>").getHead(instance)); + assertEquals("text/html", head); + + instance.setEncoding((String) null); + head = instance.contentTypeOf(new HeadFormatter("Head").getHead(instance)); + if (head != null) {//null is assumed to be plain text. + assertEquals("text/plain", head); + } + + instance.setEncoding("US-ASCII"); + head = instance.contentTypeOf(new HeadFormatter("Head").getHead(instance)); + if (head != null) { //null is assumed to be plain text. + assertEquals("text/plain", head); + } + + instance.setEncoding("US-ASCII"); + head = instance.contentTypeOf(new HeadFormatter("Head.......Neck.......Body").getHead(instance)); + if (head != null) { //null is assumed to be plain text. + assertEquals("text/plain", head); + } + instance.close(); + + for (Exception exception : em.exceptions) { + fail(exception.toString()); + } + } + + @Test + public void testContentTypeOfFormatter() { + MailHandler instance = new MailHandler(createInitProperties("")); + InternalErrorManager em = new InternalErrorManager(); + instance.setErrorManager(em); + + synchronized (instance) { + assertNull(instance.contentTypeOf(new SimpleFormatter())); + assertNull(instance.contentTypeOf(new SimpleFormatter(){})); + + assertEquals("application/xml", instance.contentTypeOf(new XMLFormatter())); + assertEquals("application/xml", instance.contentTypeOf(new XMLFormatter(){})); + } + + /** + * None of the Formatter methods that can generate content should be + * invoked during a verify as that could lead to poor startup times. + */ + class UnsupportedHTML extends Formatter { + + @Override + public String getHead(Handler h) { + throw new UnsupportedOperationException(); + } + + @Override + public String format(LogRecord record) { + throw new UnsupportedOperationException(); + } + + @Override + public String getTail(Handler h) { + throw new UnsupportedOperationException(); + } + + @Override + public String toString() { + throw new UnsupportedOperationException(); + } + } + + synchronized (instance) { + assertEquals("text/html", instance.contentTypeOf(new UnsupportedHTML())); + assertEquals("text/html", instance.contentTypeOf(new UnsupportedHTML(){})); + } + + instance.close(); + for (Exception exception : em.exceptions) { + fail(exception.toString()); + } + } + + @Test + public void testGuessContentTypeReadlimit() throws Exception { + class LastMarkInputStream extends ByteArrayInputStream { + + private int lastReadLimit; + + LastMarkInputStream() { + super(new byte[1024]); + } + + @Override + public synchronized void mark(int readlimit) { + this.lastReadLimit = readlimit; + super.mark(readlimit); + } + + public synchronized int getLastReadLimit() { + return lastReadLimit; + } + } + + LastMarkInputStream in = new LastMarkInputStream(); + URLConnection.guessContentTypeFromStream(in); + + //See MAX_CHARS in MailHandler.contentTypeOf + final int lastLimit = in.getLastReadLimit(); + assertTrue(String.valueOf(lastLimit), 25 >= lastLimit); + } + + @Test + public void testContentTypeNestedFormatter() throws Exception { + String expected = "application/xml; charset=us-ascii"; + String type = getInlineContentType(new CollectorFormatter("{0}{1}{2}", + new XMLFormatter(), new SeverityComparator())); + assertEquals(expected, type); + + + expected = "text/plain; charset=us-ascii"; + type = getInlineContentType(new CollectorFormatter("{0}{1}{2}", + new CompactFormatter(), new SeverityComparator())); + assertEquals(expected, type); + } + + @Test + public void testContentTypeNestedMultiFormatter() throws Exception { + String expected = "application/xml; charset=us-ascii"; + String type = getInlineMultiContentType(new CollectorFormatter("{0}{1}{2}", + new XMLFormatter(), new SeverityComparator())); + assertEquals(expected, type); + + + expected = "text/plain; charset=us-ascii"; + type = getInlineMultiContentType(new CollectorFormatter("{0}{1}{2}", + new CompactFormatter(), new SeverityComparator())); + assertEquals(expected, type); + } + + @Test + public void testContentTypeMultiOverride() throws Exception { + String expected = "application/xml; charset=us-ascii"; + String type = getInlineMultiContentType(new XMLFormatter()); + assertEquals(expected, type); + + MimetypesFileTypeMap m = new MimetypesFileTypeMap(); + m.addMimeTypes("text/plain txt TXT XMLFormatter"); + final FileTypeMap old = FileTypeMap.getDefaultFileTypeMap(); + FileTypeMap.setDefaultFileTypeMap(m); + try { + type = getInlineMultiContentType(new XMLFormatter()); + assertEquals("text/plain; charset=us-ascii", type); + } finally { + FileTypeMap.setDefaultFileTypeMap(old); + } + + type = getInlineMultiContentType(new XMLFormatter()); + assertEquals(expected, type); + } + + private String getInlineMultiContentType(Formatter f) throws Exception { + final String[] value = new String[1]; + MailHandler instance = new MailHandler(createInitProperties("")); + instance.setAttachmentFormatters(new SimpleFormatter()); + instance.setAttachmentFilters(BooleanFilter.FALSE); + instance.setEncoding("us-ascii"); + + MessageErrorManager em = new MessageErrorManager(instance.getMailProperties()) { + + @Override + protected void error(MimeMessage msg, Throwable t, int code) { + try { + MimeMultipart multi = (MimeMultipart) msg.getContent(); + BodyPart body = multi.getBodyPart(0); + assertEquals(Part.INLINE, body.getDisposition()); + String desc = msg.getDescription(); + assertTrue(desc.contains("Sorted using")); + assertTrue(desc.contains("pushed when")); + value[0] = body.getContentType(); + } catch (Throwable E) { + dump(E); + fail(E.toString()); + } + } + }; + instance.setErrorManager(em); + Properties props = createInitProperties(""); + props.put("mail.to", "localhost@localdomain"); + instance.setMailProperties(props); + instance.setFormatter(f); + instance.publish(new LogRecord(Level.SEVERE, "test")); + instance.close(); + + return value[0]; + } + + @Test + public void testContentTypeOverride() throws Exception { + String expected = "application/xml; charset=us-ascii"; + String type = getInlineContentType(new XMLFormatter()); + assertEquals(expected, type); + + MimetypesFileTypeMap m = new MimetypesFileTypeMap(); + m.addMimeTypes("text/plain txt TXT XMLFormatter"); + final FileTypeMap old = FileTypeMap.getDefaultFileTypeMap(); + FileTypeMap.setDefaultFileTypeMap(m); + try { + type = getInlineContentType(new XMLFormatter()); + assertEquals("text/plain; charset=us-ascii", type); + } finally { + FileTypeMap.setDefaultFileTypeMap(old); + } + + type = getInlineContentType(new XMLFormatter()); + assertEquals(expected, type); + } + + private ErrorManager getSuperErrorManager(MailHandler h) throws Exception { + Method hem = MailHandler.class.getDeclaredMethod("defaultErrorManager"); + hem.setAccessible(true); + return (ErrorManager) hem.invoke(h); + } + + private String getInlineContentType(Formatter f) throws Exception { + final String[] value = new String[1]; + MailHandler instance = new MailHandler(createInitProperties("")); + instance.setEncoding("us-ascii"); + MessageErrorManager em = new MessageErrorManager(instance.getMailProperties()) { + + @Override + protected void error(MimeMessage msg, Throwable t, int code) { + try { + String desc = msg.getDescription(); + assertTrue(desc.contains("filtered with")); + assertTrue(desc.contains("named by")); + value[0] = msg.getContentType(); + } catch (Throwable E) { + dump(E); + fail(E.toString()); + } + } + }; + instance.setErrorManager(em); + Properties props = createInitProperties(""); + props.put("mail.to", "localhost@localdomain"); + instance.setMailProperties(props); + instance.setFormatter(f); + instance.publish(new LogRecord(Level.SEVERE, "test")); + instance.close(); + + return value[0]; + } + + @Test + public void testAcceptLang() throws Exception { + class LangManager extends MessageErrorManager { + + LangManager(final Properties props) { + super(props); + } + + @Override + protected void error(MimeMessage msg, Throwable t, int code) { + try { + final Locale locale = Locale.getDefault(); + String lang = LogManagerProperties.toLanguageTag(locale); + if (lang.length() != 0) { + assertEquals(lang, msg.getHeader("Accept-Language", null)); + } else { + assertEquals("", locale.getLanguage()); + } + + MimeMultipart mp = (MimeMultipart) msg.getContent(); + assertTrue(mp.getCount() > 0); + for (int i = 0; i < mp.getCount(); i++) { + MimePart part = (MimePart) mp.getBodyPart(i); + if (lang.length() != 0) { + assertEquals(lang, part.getHeader("Accept-Language", null)); + } else { + assertEquals("", locale.getLanguage()); + } + } + } catch (RuntimeException re) { + dump(re); + throw new AssertionError(re); + } catch (Exception ex) { + dump(ex); + throw new AssertionError(ex); + } + } + } + + Formatter[] formatters = new Formatter[]{new SimpleFormatter(), new SimpleFormatter()}; + InternalErrorManager em; + MailHandler target; + Locale locale = Locale.getDefault(); + try { + target = new MailHandler(createInitProperties("")); + try { + em = new LangManager(target.getMailProperties()); + target.setErrorManager(em); + target.setAttachmentFormatters(formatters); + + Locale.setDefault(new Locale("", "", "")); + target.publish(new LogRecord(Level.SEVERE, "")); + target.flush(); + + Locale.setDefault(Locale.ENGLISH); + target.publish(new LogRecord(Level.SEVERE, "")); + target.flush(); + + Locale.setDefault(Locale.GERMAN); + target.publish(new LogRecord(Level.SEVERE, "")); + target.flush(); + + Locale.setDefault(Locale.FRANCE); + target.publish(new LogRecord(Level.SEVERE, "")); + target.flush(); + } finally { + target.close(); + } + } finally { + Locale.setDefault(locale); + } + } + + @Test + public void testContentLangBase() throws Exception { + + class Base extends MessageErrorManager { + + private final String bundleName; + + Base(Properties props, final String bundleName) { + super(props); + this.bundleName = bundleName; + } + + @Override + protected void error(MimeMessage msg, Throwable t, int code) { + try { + assertNotNull(bundleName); + MimeMultipart mp = (MimeMultipart) msg.getContent(); + Locale l = Locale.getDefault(); + assertEquals(LogManagerProperties.toLanguageTag(l), msg.getHeader("Accept-Language", null)); + String lang[] = msg.getContentLanguage(); + assertNotNull(lang); + assertEquals(LogManagerProperties.toLanguageTag(l), lang[0]); + assertEquals(1, mp.getCount()); + MimePart part; + + part = (MimePart) mp.getBodyPart(0); + lang = part.getContentLanguage(); + assertNotNull(lang); + assertEquals(LogManagerProperties.toLanguageTag(l), lang[0]); + assertEquals(LogManagerProperties.toLanguageTag(l), part.getHeader("Accept-Language", null)); + } catch (RuntimeException re) { + dump(re); + throw new AssertionError(re); + } catch (Exception ex) { + dump(ex); + throw new AssertionError(ex); + } + } + } + + MailHandler target = new MailHandler(createInitProperties("")); + target.setAttachmentFormatters(new SimpleFormatter()); + target.setAttachmentFilters(BooleanFilter.FALSE); + + Properties props = new Properties(); + props.put("motd", "Hello MailHandler!"); + final String p = MailHandler.class.getName(); + final Locale l = Locale.getDefault(); + final String name = MailHandler.class.getSimpleName().concat("base"); + final File f = File.createTempFile(name, ".properties", findClassPathDir()); + Locale.setDefault(Locale.US); + try { + try (FileOutputStream fos = new FileOutputStream(f)) { + props.store(fos, "No Comment"); + } + + String bundleName = f.getName().substring(0, f.getName().lastIndexOf('.')); + target.setErrorManager(new Base(target.getMailProperties(), bundleName)); + final Logger log = Logger.getLogger(p + '.' + f.getName(), bundleName); + hardRef = log; + try { + assertNotNull(log.getResourceBundle()); + assertNotNull(log.getResourceBundleName()); + + log.addHandler(target); + try { + log.setUseParentHandlers(false); + log.log(Level.SEVERE, "motd"); + } finally { + log.removeHandler(target); + } + } finally { + hardRef = null; + } + + target.close(); + + InternalErrorManager em = internalErrorManagerFrom(target); + for (Exception t : em.exceptions) { + if (isConnectOrTimeout(t)) { + continue; + } + dump(t); + fail(t.toString()); + } + assertFalse(em.exceptions.isEmpty()); + } finally { + Locale.setDefault(l); + if (!f.delete() && f.exists()) { + f.deleteOnExit(); + } + } + } + + @Test + public void testContentLangInfer() throws Exception { + + class Infer extends MessageErrorManager { + + private final Locale expect; + + Infer(Properties props, final Locale expect) { + super(props); + this.expect = expect; + } + + @Override + protected void error(MimeMessage msg, Throwable t, int code) { + try { + MimeMultipart mp = (MimeMultipart) msg.getContent(); + Locale l = Locale.getDefault(); + assertFalse(l.getCountry().equals(expect.getCountry())); + + assertEquals(LogManagerProperties.toLanguageTag(l), msg.getHeader("Accept-Language", null)); + String lang[] = msg.getContentLanguage(); + assertEquals(1, lang.length); + assertEquals(LogManagerProperties.toLanguageTag(expect), lang[0]); + assertEquals(1, mp.getCount()); + MimePart part; + + part = (MimePart) mp.getBodyPart(0); + lang = part.getContentLanguage(); + assertEquals(1, lang.length); + assertEquals(LogManagerProperties.toLanguageTag(expect), lang[0]); + assertEquals(LogManagerProperties.toLanguageTag(l), part.getHeader("Accept-Language", null)); + } catch (RuntimeException re) { + dump(re); + throw new AssertionError(re); + } catch (Exception ex) { + dump(ex); + throw new AssertionError(ex); + } + } + } + + MailHandler target; + Locale cl; + String logPrefix; + Properties props = new Properties(); + props.put("motd", "Hello MailHandler!"); + final String p = MailHandler.class.getName(); + final Locale l = Locale.getDefault(); + final String name = MailHandler.class.getSimpleName().concat("infer"); + final File f = File.createTempFile(name, "_" + + Locale.ENGLISH.getLanguage() + ".properties", findClassPathDir()); + try { + try (FileOutputStream fos = new FileOutputStream(f)) { + props.store(fos, "No Comment"); + } + + String bundleName = f.getName().substring(0, f.getName().lastIndexOf('_')); + assertTrue(!bundleName.contains(Locale.ENGLISH.getLanguage())); + + cl = Locale.US; + target = new MailHandler(createInitProperties("")); + target.setAttachmentFormatters(new SimpleFormatter()); + target.setAttachmentFilters(BooleanFilter.FALSE); + + target.setErrorManager(new Infer(target.getMailProperties(), Locale.ENGLISH)); + logPrefix = p + '.' + f.getName() + cl; + testContentLangInfer(target, logPrefix, bundleName, cl); + + cl = Locale.UK; + target = new MailHandler(createInitProperties("")); + target.setAttachmentFormatters(new SimpleFormatter()); + target.setAttachmentFilters(BooleanFilter.FALSE); + target.setErrorManager(new Infer(target.getMailProperties(), Locale.ENGLISH)); + logPrefix = p + '.' + f.getName() + cl; + testContentLangInfer(target, logPrefix, bundleName, cl); + } finally { + Locale.setDefault(l); + if (!f.delete() && f.exists()) { + f.deleteOnExit(); + } + } + } + + private void testContentLangInfer(MailHandler target, String logPrefix, String bundleName, Locale cl) { + Locale.setDefault(cl); + Logger log = Logger.getLogger(logPrefix + cl, bundleName); + hardRef = log; + try { + assertNotNull(log.getResourceBundle()); + assertNotNull(log.getResourceBundleName()); + + log.addHandler(target); + try { + log.setUseParentHandlers(false); + log.log(Level.SEVERE, "motd"); + } finally { + log.removeHandler(target); + } + } finally { + hardRef = null; + } + + target.close(); + + InternalErrorManager em = internalErrorManagerFrom(target); + for (Exception t : em.exceptions) { + if (isConnectOrTimeout(t)) { + continue; + } + dump(t); + fail(t.toString()); + } + assertFalse(em.exceptions.isEmpty()); + } + + @Test + public void testContentLangExact() throws Exception { + MailHandler target = new MailHandler(createInitProperties("")); + target.setErrorManager(new MessageErrorManager(target.getMailProperties()) { + + @Override + protected void error(MimeMessage msg, Throwable t, int code) { + try { + MimeMultipart mp = (MimeMultipart) msg.getContent(); + Locale l = Locale.getDefault(); + assertEquals(LogManagerProperties.toLanguageTag(l), msg.getHeader("Accept-Language", null)); + String lang[] = msg.getContentLanguage(); + assertEquals(LogManagerProperties.toLanguageTag(Locale.ENGLISH), lang[0]); + assertEquals(LogManagerProperties.toLanguageTag(Locale.GERMAN), lang[1]); + assertEquals(LogManagerProperties.toLanguageTag(Locale.FRANCE), lang[2]); + assertEquals(4, mp.getCount()); + MimePart part; + + part = (MimePart) mp.getBodyPart(0); + assertEquals(LogManagerProperties.toLanguageTag(l), part.getHeader("Accept-Language", null)); + assertNull(part.getHeader("Content-Language", ",")); + + part = (MimePart) mp.getBodyPart(1); + assertEquals(LogManagerProperties.toLanguageTag(l), part.getHeader("Accept-Language", null)); + assertEquals(LogManagerProperties.toLanguageTag(Locale.ENGLISH), part.getHeader("Content-Language", ",")); + + part = (MimePart) mp.getBodyPart(2); + assertEquals(LogManagerProperties.toLanguageTag(l), part.getHeader("Accept-Language", null)); + assertEquals(LogManagerProperties.toLanguageTag(Locale.GERMAN), part.getHeader("Content-Language", ",")); + + part = (MimePart) mp.getBodyPart(3); + assertEquals(LogManagerProperties.toLanguageTag(l), part.getHeader("Accept-Language", null)); + assertEquals(LogManagerProperties.toLanguageTag(Locale.FRANCE), part.getHeader("Content-Language", ",")); + } catch (RuntimeException re) { + dump(re); + throw new AssertionError(re); + } catch (Exception ex) { + dump(ex); + throw new AssertionError(ex); + } + } + }); + + target.setLevel(Level.ALL); + target.setFilter(new LocaleFilter(Locale.JAPANESE, true)); + target.setPushLevel(Level.OFF); + target.setAttachmentFormatters(new Formatter[]{ + new SimpleFormatter(), new SimpleFormatter(), new SimpleFormatter()}); + target.setAttachmentFilters(new Filter[]{ + new LocaleFilter(Locale.ENGLISH, false), + new LocaleFilter(Locale.GERMAN, false), + new LocaleFilter(Locale.FRANCE, false)}); //just the language. + + assertEquals(3, target.getAttachmentFormatters().length); + assertEquals(3, target.getAttachmentFilters().length); + + final List<File> files = new ArrayList<>(); + final Properties props = new Properties(); + final Locale current = Locale.getDefault(); + try { + File f; + Locale.setDefault(new Locale("", "", "")); + f = testContentLangExact(target, props, "_"); + files.add(f); + + props.put("motd", "Hello MailHandler!"); + Locale.setDefault(Locale.ENGLISH); + f = testContentLangExact(target, props, "_"); + files.add(f); + + props.put("motd", "Hallo MailHandler!"); + Locale.setDefault(Locale.GERMAN); + f = testContentLangExact(target, props, "_"); + files.add(f); + + props.put("motd", "Bonjour MailHandler!"); + Locale.setDefault(Locale.FRANCE); //just the language. + f = testContentLangExact(target, props, "_"); + files.add(f); + + Locale.setDefault(new Locale("", "", "")); + f = testContentLangExact(target, props, "_"); + files.add(f); + + Locale.setDefault(new Locale("", "", "")); + f = testContentLangExact(target, props, "."); + files.add(f); + + props.put("motd", "Hello MailHandler!"); + Locale.setDefault(Locale.ENGLISH); + f = testContentLangExact(target, props, "."); + files.add(f); + + props.put("motd", "Hallo MailHandler!"); + Locale.setDefault(Locale.GERMAN); + f = testContentLangExact(target, props, "."); + files.add(f); + + props.put("motd", "Bonjour MailHandler!"); + Locale.setDefault(Locale.FRANCE); //just the language. + f = testContentLangExact(target, props, "."); + files.add(f); + + Locale.setDefault(new Locale("", "", "")); + f = testContentLangExact(target, props, "."); + files.add(f); + } finally { + Locale.setDefault(current); + for (File f : files) { + if (!f.delete() && f.exists()) { + f.deleteOnExit(); + } + } + } + + target.close(); + + InternalErrorManager em = internalErrorManagerFrom(target); + for (Exception t : em.exceptions) { + if (isConnectOrTimeout(t)) { + continue; + } + dump(t); + fail(t.toString()); + } + assertFalse(em.exceptions.isEmpty()); + } + + private File testContentLangExact(MailHandler target, Properties props, String exact) throws Exception { + final String p = MailHandler.class.getName(); + final Locale l = Locale.getDefault(); + boolean fail = true; + final String name = MailHandler.class.getSimpleName().concat("motd"); + assertTrue(name, name.indexOf(exact) < 1); + + String prefix; + if (l.getLanguage().length() != 0) { + prefix = "_" + l; + } else { + prefix = ""; + } + final File f = File.createTempFile(name, prefix + ".properties", findClassPathDir()); + try { + try (FileOutputStream fos = new FileOutputStream(f)) { + props.store(fos, "No Comment"); + } + + Logger log; + if (l.getLanguage().length() == 0) { + log = Logger.getLogger(p + '.' + f.getName()); + assertNull(log.getResourceBundle()); + } else { + final String loggerName = p + '.' + f.getName() + '.' + l; + if (".".equals(exact)) { + log = Logger.getLogger(loggerName, + f.getName().substring(0, f.getName().lastIndexOf(exact))); + } else if ("_".equals(exact)) { + log = Logger.getLogger(loggerName, + f.getName().substring(0, f.getName().indexOf(exact))); + } else { + throw new IllegalArgumentException(exact); + } + assertNotNull(log.getResourceBundle()); + assertNotNull(log.getResourceBundleName()); + } + + hardRef = log; + try { + log.setUseParentHandlers(false); + try { + log.addHandler(target); + log.log(Level.INFO, "motd"); + fail = false; + } finally { + log.removeHandler(target); + } + } finally { + hardRef = null; + } + } finally { + if (fail) { + if (!f.delete() && f.exists()) { + f.deleteOnExit(); + } + } + } + return f; + } + + /** + * Find a writable directory that is in the class path. + * + * @return a File directory. + * @throws IOException if there is a problem. + * @throws FileNotFoundException if there are no directories in class path. + */ + @SuppressWarnings("ThrowableInitCause") + private static File findClassPathDir() throws IOException { + File f = anyClassPathDir; + if (f != null) { + return f; + } + + ClassLoader ccl = Thread.currentThread().getContextClassLoader(); + if (ccl == null) { + ccl = ClassLoader.getSystemClassLoader(); + } + + if (ccl == null) { + throw new IllegalStateException("Missing classloader."); + } + + String path = System.getProperty("java.class.path"); + String[] dirs = path.split(System.getProperty("path.separator")); + IOException fail = null; + for (String dir : dirs) { + f = new File(dir.trim()); + if (f.isFile()) { + f = f.getParentFile(); + if (f == null) { + continue; + } + } + + try { + if (f.isDirectory()) { + final String name = MailHandlerTest.class.getName(); + final File tmp = File.createTempFile(name, ".tmp", f); + final URL url = ccl.getResource(tmp.getName()); + if (!tmp.delete() && tmp.exists()) { + IOException ioe = new IOException(tmp.toString()); + dump(ioe); + throw ioe; + } + + if (url == null || !tmp.equals(new File(url.toURI()))) { + throw new FileNotFoundException(tmp + "not visible from " + ccl); + } + anyClassPathDir = f; + return f; + } else { + fail = new FileNotFoundException(f.toString()); + } + } catch (final IOException ioe) { + fail = ioe; + } catch (final URISyntaxException | IllegalArgumentException use) { + fail = (IOException) new IOException(use.toString()).initCause(use); + } + } + + if (fail != null) { + throw fail; + } + + //modify the classpath to include a writable directory. + throw new FileNotFoundException(path); + } + + @Test + public void testComparator() { + MailHandler instance = new MailHandler(createInitProperties("")); + InternalErrorManager em = new InternalErrorManager(); + instance.setErrorManager(em); + + try { + instance.setComparator((Comparator<LogRecord>) null); + } catch (RuntimeException RE) { + fail(RE.toString()); + } + assertNull(instance.getComparator()); + + UselessComparator uselessComparator = new UselessComparator(); + Comparator<? super LogRecord> result = instance.getComparator(); + assertEquals(false, uselessComparator.equals(result)); + + instance.setComparator(uselessComparator); + result = instance.getComparator(); + + assertTrue(uselessComparator.equals(result)); + + RawTypeComparator raw = new RawTypeComparator(); + instance.setComparator(raw); + assertTrue(raw.equals(instance.getComparator())); + + assertEquals(true, em.exceptions.isEmpty()); + instance.close(); + } + + @Test + public void testCapacity() { + try { + MailHandler h = new MailHandler(-1); + h.getCapacity(); + fail("Negative capacity was allowed."); + } catch (IllegalArgumentException pass) { + } catch (RuntimeException RE) { + fail(RE.toString()); + } + + try { + MailHandler h = new MailHandler(0); + h.getCapacity(); + fail("Zero capacity was allowed."); + } catch (IllegalArgumentException pass) { + } catch (RuntimeException RE) { + fail(RE.toString()); + } + + try { + MailHandler h = new MailHandler(1); + h.getCapacity(); + } catch (RuntimeException RE) { + fail(RE.toString()); + } + + final int expResult = 20; + MailHandler instance = new MailHandler(20); + instance.setMailProperties(createInitProperties("")); + InternalErrorManager em = new InternalErrorManager(); + instance.setErrorManager(em); + + int result = instance.getCapacity(); + assertEquals(expResult, result); + instance.close(); + + result = instance.getCapacity(); + assertEquals(expResult, result); + assertEquals(true, em.exceptions.isEmpty()); + instance.close(); + + String SOURCE_CLASS = MailHandlerTest.class.getName(); + String SOURCE_METHOD = "testCapacity"; + for (int i = 0; i <= NUM_RUNS; i++) { + instance = new MailHandler(nextCapacity(i)); + instance.setLevel(Level.ALL); + instance.setPushLevel(Level.OFF); + em = new InternalErrorManager(); + instance.setErrorManager(em); + CountingFormatter formatter = new CountingFormatter(); + instance.setFormatter(formatter); + Properties props = createInitProperties(""); + instance.setMailProperties(props); + for (int j = 0; j < instance.getCapacity(); j++) { + LogRecord r = new LogRecord(Level.INFO, ""); + r.setSourceClassName(SOURCE_CLASS); + r.setSourceMethodName(SOURCE_METHOD); + instance.publish(r); + } + assertEquals(instance.getCapacity(), formatter.format); + assertEquals(1, formatter.head); + assertEquals(1, formatter.tail); + assertEquals(1, em.exceptions.size()); + assertTrue(em.exceptions.get(0) instanceof MessagingException); + instance.close(); + } + } + private static final int LOW_CAPACITY = 1000; + private static final int MAX_CAPACITY = 1 << 18; + private static final int NUM_RUNS = LOW_CAPACITY + 42; + private static final Random RANDOM = new Random(); + + /** + * Test all numbers between 1 and low capacity. + * + * @param capacity the current capacity. + * @return the next random capacity. + */ + private int nextCapacity(int capacity) { + if (capacity <= LOW_CAPACITY) { + return ++capacity; + } else { + if (capacity < NUM_RUNS) { + int next; + do { + next = RANDOM.nextInt(MAX_CAPACITY); + } while (next <= LOW_CAPACITY); + return next; + } else { + return MAX_CAPACITY; + } + } + } + + @Test + public void testAuthenticator_Authenticator_Arg() { + Authenticator auth = new EmptyAuthenticator(); + + MailHandler instance = new MailHandler(createInitProperties("")); + InternalErrorManager em = new InternalErrorManager(); + instance.setErrorManager(em); + + try { + instance.setAuthenticator((Authenticator) null); + } catch (RuntimeException RE) { + fail(RE.toString()); + } + + try { + instance.setAuthenticator(instance.getAuthenticator()); + } catch (RuntimeException RE) { + fail(RE.toString()); + } + + try { + instance.setAuthenticator(auth); + assertEquals(auth, instance.getAuthenticator()); + } catch (RuntimeException RE) { + fail(RE.toString()); + } + + assertEquals(true, em.exceptions.isEmpty()); + + instance = createHandlerWithRecords(); + instance.setAuthenticator(new ThrowAuthenticator()); + em = internalErrorManagerFrom(instance); + instance.close(); + + assertEquals(1, em.exceptions.size()); + assertEquals(true, em.exceptions.get(0) instanceof MessagingException); + } + + @Test + public void testAuthenticator_Char_Array_Arg() { + PasswordAuthentication pa; + MailHandler instance = new MailHandler(createInitProperties("")); + InternalErrorManager em = new InternalErrorManager(); + instance.setErrorManager(em); + + //Null literal means actual password value here. + instance.setAuthenticator("null".toCharArray()); + pa = passwordAuthentication(instance.getAuthenticator(), "user"); + assertEquals("user", pa.getUserName()); + assertEquals("null", pa.getPassword()); + + instance.setAuthenticator("Null".toCharArray()); + pa = passwordAuthentication(instance.getAuthenticator(), "user"); + assertEquals("user", pa.getUserName()); + assertEquals("Null", pa.getPassword()); + + instance.setAuthenticator("NULL".toCharArray()); + pa = passwordAuthentication(instance.getAuthenticator(), "user"); + assertEquals("user", pa.getUserName()); + assertEquals("NULL", pa.getPassword()); + + try { + instance.setAuthenticator((char[]) null); + } catch (RuntimeException RE) { + fail(RE.toString()); + } + + try { + instance.setAuthenticator(instance.getAuthenticator()); + } catch (RuntimeException RE) { + fail(RE.toString()); + } + + try { + instance.setAuthenticator("password".toCharArray()); + pa = passwordAuthentication( + instance.getAuthenticator(), "user"); + assertEquals("user", pa.getUserName()); + assertEquals("password", pa.getPassword()); + } catch (RuntimeException RE) { + fail(RE.toString()); + } + + assertEquals(true, em.exceptions.isEmpty()); + + instance = createHandlerWithRecords(); + instance.setAuthenticator("password".toCharArray()); + em = internalErrorManagerFrom(instance); + instance.close(); + + assertEquals(1, em.exceptions.size()); + assertEquals(true, em.exceptions.get(0) instanceof MessagingException); + } + + @Test + public void testMailProperties() throws Exception { + Properties props = new Properties(); + MailHandler instance = new MailHandler(); + InternalErrorManager em = new InternalErrorManager(); + instance.setErrorManager(em); + + assertNotNull(instance.getMailProperties()); + assertEquals(Properties.class, instance.getMailProperties().getClass()); + + try { + instance.setMailProperties((Properties) null); + fail("Null was allowed."); + } catch (NullPointerException pass) { + } catch (RuntimeException RE) { + fail(RE.toString()); + } + + instance.setMailProperties(props); + Properties stored = instance.getMailProperties(); + + assertNotNull(stored); + assertNotSame(props, stored); + assertEquals(props.getClass(), stored.getClass()); + + assertEquals(true, em.exceptions.isEmpty()); + instance.close(); + + instance = createHandlerWithRecords(); + props = instance.getMailProperties(); + em = new InternalErrorManager(); + instance.setErrorManager(em); + + props.setProperty("mail.from", "localhost@localdomain"); + props.setProperty("mail.to", "localhost@localdomain"); + instance.setMailProperties(props); + instance.flush(); + for (Exception exception : em.exceptions) { + final Throwable t = exception; + if (isConnectOrTimeout(t)) { + continue; + } else { + dump(t); + fail(t.toString()); + } + } + assertFalse(em.exceptions.isEmpty()); + + props.setProperty("mail.from", "localhost@localdomain"); + props.setProperty("mail.to", "::1@@"); + instance.setMailProperties(props); + + em = new InternalErrorManager(); + instance.setErrorManager(em); + + instance.publish(new LogRecord(Level.SEVERE, "test")); + instance.close(); + int failed = 0; + for (Exception exception : em.exceptions) { + final Throwable t = exception; + if (t instanceof AddressException || isConnectOrTimeout(t)) { + continue; + } else { + dump(t); + failed++; + } + } + assertEquals(0, failed); + assertFalse(em.exceptions.isEmpty()); + } + + @Test + public void testEmptyAddressParse() throws Exception { + //Assumed to never return null in the MailHandler. + InternetAddress[] a = InternetAddress.parse("", false); + assertTrue(a.length == 0); + } + + @Test + public void testDefaultRecipient() throws Exception { + Properties props = createInitProperties(""); + props.remove("mail.from"); + props.remove("mail.to"); + props.remove("mail.cc"); + props.remove("mail.bcc"); + + //User didn't specify so auto compute addresses. + assertNull(props.getProperty("mail.from")); + assertNull(props.get("mail.from")); + + assertNull(props.getProperty("mail.to")); + assertNull(props.get("mail.to")); + testDefaultRecipient(props); + + //User override of TO and FROM addresses. + props.setProperty("mail.from", ""); + props.setProperty("mail.to", ""); + testDefaultRecipient(props); + + //Fixed TO and FROM. + props.setProperty("mail.from", "localhost@localdomain"); + props.setProperty("mail.to", "otherhost@localdomain"); + testDefaultRecipient(props); + + //Compute TO and FROM with a fixed CC and BCC. + props.remove("mail.from"); + props.remove("mail.to"); + assertNull(props.getProperty("mail.to")); + assertNull(props.get("mail.to")); + props.setProperty("mail.cc", "localhost@localdomain"); + props.setProperty("mail.bcc", "otherhost@localdomain"); + testDefaultRecipient(props); + } + + private void testDefaultRecipient(Properties addresses) throws Exception { + + class DefaultRecipient extends MessageErrorManager { + + DefaultRecipient(Properties props) { + super(props); + } + + private Address[] parseKey(Session s, String key) throws Exception { + final String v = s.getProperty(key); + if (v == null) { + return null; //value not present for key. + } else if (v.length() == 0) { + return new Address[0]; //value empty for key. + } else { + return InternetAddress.parse(v, true); + } + } + + @Override + protected void error(MimeMessage msg, Throwable t, int code) { + Session s = new MessageContext(msg).getSession(); + try { + Address[] local = new Address[]{ + InternetAddress.getLocalAddress(s)}; + Address[] expectFrom = parseKey(s, "mail.from"); + Address[] expectTo = parseKey(s, "mail.to"); + Address[] expectCc = parseKey(s, "mail.cc"); + Address[] expectBcc = parseKey(s, "mail.bcc"); + + checkAddress(expectFrom == null ? local : expectFrom, + msg.getFrom()); + checkAddress(expectTo == null ? local : expectTo, + msg.getRecipients(Message.RecipientType.TO)); + + assertArrayEquals(expectCc, + msg.getRecipients(Message.RecipientType.CC)); + assertArrayEquals(expectBcc, + msg.getRecipients(Message.RecipientType.BCC)); + + List<Address> all = asList(msg.getAllRecipients()); + assertTrue(all.containsAll(asList(expectTo))); + assertTrue(all.containsAll(asList(expectCc))); + assertTrue(all.containsAll(asList(expectBcc))); + } catch (Throwable fail) { + dump(fail); + fail(fail.toString()); + } + } + + private void checkAddress(Address[] expect, Address[] found) { + if (expect.length == 0) { + assertTrue(found == null || found.length == 0); + } else { + assertArrayEquals(expect, found); + } + } + + private List<Address> asList(Address... a) { + return Arrays.asList(a == null ? new Address[0] : a); + } + } + MailHandler instance = createHandlerWithRecords(); + Properties props = instance.getMailProperties(); + props.putAll(addresses); + InternalErrorManager em = new DefaultRecipient(props); + instance.setErrorManager(em); + + assertNotNull(instance.getMailProperties()); + assertEquals(Properties.class, instance.getMailProperties().getClass()); + + instance.setMailProperties(props); + instance.close(); + for (Exception exception : em.exceptions) { + final Throwable t = exception; + if (isConnectOrTimeout(t) || t instanceof SendFailedException) { + continue; + } else { + dump(t); + fail(t.toString()); + } + } + + assertFalse(em.exceptions.isEmpty()); + } + + @Test + public void testDefaultUrlName() throws Exception { + Properties props = createInitProperties(""); + props.put("mail.transport.protocol", "smtp"); + Session s = Session.getInstance(props); + Transport t = s.getTransport(); + if (isPrivateSpec(t.getClass())) { + assertEquals(UNKNOWN_HOST, t.getURLName().getHost()); + } + } + + @Test + public void testAttachmentFilters() { + MailHandler instance = new MailHandler(createInitProperties("")); + InternalErrorManager em = new InternalErrorManager(); + instance.setErrorManager(em); + + Filter[] result = instance.getAttachmentFilters(); + assertNotNull(result); + assertEquals(result.length, instance.getAttachmentFormatters().length); + + assertEquals(false, instance.getAttachmentFilters() == result); + + if (instance.getAttachmentFormatters().length != 0) { + instance.setAttachmentFormatters(new Formatter[0]); + } + + try { + instance.setAttachmentFilters((Filter[]) null); + fail("Null allowed."); + } catch (NullPointerException pass) { + } catch (RuntimeException re) { + fail(re.toString()); + } + + try { + instance.setAttachmentFilters(new Filter[0]); + } catch (RuntimeException re) { + fail(re.toString()); + } + + try { + assertEquals(0, instance.getAttachmentFormatters().length); + + instance.setAttachmentFilters(new Filter[]{BooleanFilter.TRUE}); + fail("Filter to formatter mismatch."); + } catch (IndexOutOfBoundsException pass) { + } catch (RuntimeException re) { + fail(re.toString()); + } + + instance.setAttachmentFormatters( + new Formatter[]{new SimpleFormatter(), new XMLFormatter()}); + + try { + assertEquals(2, instance.getAttachmentFormatters().length); + + instance.setAttachmentFilters(new Filter[]{BooleanFilter.TRUE}); + fail("Filter to formatter mismatch."); + } catch (IndexOutOfBoundsException pass) { + } catch (RuntimeException re) { + fail(re.toString()); + } + + try { + assertEquals(2, instance.getAttachmentFormatters().length); + Filter[] filters = new Filter[]{BooleanFilter.TRUE, BooleanFilter.TRUE}; + assertEquals(instance.getAttachmentFormatters().length, filters.length); + instance.setAttachmentFilters(filters); + } catch (RuntimeException re) { + fail(re.toString()); + } + + try { + assertEquals(2, instance.getAttachmentFormatters().length); + Filter[] filters = new Filter[]{null, null}; + assert filters != null; //Suppress broken NPE hint with assert. + assertEquals(instance.getAttachmentFormatters().length, filters.length); + instance.setAttachmentFilters(filters); + } catch (RuntimeException re) { + fail(re.toString()); + } + + try { + assertEquals(2, instance.getAttachmentFormatters().length); + instance.setAttachmentFilters(new Filter[0]); + fail("Filter to formatter mismatch."); + } catch (IndexOutOfBoundsException pass) { + } catch (RuntimeException re) { + fail(re.toString()); + } + + try { + assertEquals(instance.getAttachmentFormatters().length, 2); + Filter[] filters = new Filter[]{null, null}; + instance.setAttachmentFilters(filters); + filters[0] = BooleanFilter.TRUE; + assertEquals(filters[0], filters[0]); + assertEquals(filters[0].equals(instance.getAttachmentFilters()[0]), false); + } catch (RuntimeException re) { + fail(re.toString()); + } + + assertEquals(instance.getAttachmentFormatters().length, 2); + //Force a subclass array. + instance.setAttachmentFilters(new ThrowFilter[]{new ThrowFilter(), new ThrowFilter()}); + assertEquals(Filter[].class, instance.getAttachmentFilters().getClass()); + + assertEquals(em.exceptions.isEmpty(), true); + instance.close(); + } + + @Test + public void testAttachmentFiltersDefaults() { + MailHandler instance = new MailHandler(createInitProperties("")); + InternalErrorManager em = new InternalErrorManager(); + instance.setErrorManager(em); + instance.setFilter(new ErrorFilter()); + final Formatter f = new SimpleFormatter(); + instance.setAttachmentFormatters(f, f, f, f); + + for (Exception exception : em.exceptions) { + dump(exception); + } + assertTrue(em.exceptions.isEmpty()); + + assertEquals(ErrorFilter.class, instance.getFilter().getClass()); + assertEquals(instance.getFilter(), instance.getAttachmentFilters()[0]); + assertEquals(instance.getFilter(), instance.getAttachmentFilters()[1]); + assertEquals(instance.getFilter(), instance.getAttachmentFilters()[2]); + assertEquals(instance.getFilter(), instance.getAttachmentFilters()[3]); + + instance.setAttachmentFilters(null, null, null, null); + assertEquals(ErrorFilter.class, instance.getFilter().getClass()); + assertNull(instance.getAttachmentFilters()[0]); + assertNull(instance.getAttachmentFilters()[1]); + assertNull(instance.getAttachmentFilters()[2]); + assertNull(instance.getAttachmentFilters()[3]); + } + + @Test + public void testAttachmentFormatters() { + MailHandler instance = new MailHandler(createInitProperties("")); + + InternalErrorManager em = new InternalErrorManager(); + instance.setErrorManager(em); + + Formatter[] result = instance.getAttachmentFormatters(); + assertNotNull(result); + assertEquals(result == instance.getAttachmentFormatters(), false); + + assertEquals(result.length, instance.getAttachmentFilters().length); + assertEquals(result.length, instance.getAttachmentNames().length); + + result = new Formatter[]{new SimpleFormatter(), new XMLFormatter()}; + instance.setAttachmentFormatters(result); + + assertEquals(result.length, instance.getAttachmentFilters().length); + assertEquals(result.length, instance.getAttachmentNames().length); + + result[0] = new XMLFormatter(); + result[1] = new SimpleFormatter(); + assertEquals(result[1].getClass(), + instance.getAttachmentFormatters()[0].getClass()); + assertEquals(result[0].getClass(), + instance.getAttachmentFormatters()[1].getClass()); + + try { + instance.setAttachmentFormatters((Formatter[]) null); + fail("Null was allowed."); + } catch (NullPointerException NPE) { + } catch (RuntimeException re) { + fail(re.toString()); + } + + result[0] = null; + try { + instance.setAttachmentFormatters(result); + fail("Null index was allowed."); + } catch (NullPointerException NPE) { + } catch (RuntimeException re) { + fail(re.toString()); + } + + result = new Formatter[0]; + try { + instance.setAttachmentFormatters(result); + assertEquals(result.length, instance.getAttachmentFilters().length); + assertEquals(result.length, instance.getAttachmentNames().length); + } catch (RuntimeException re) { + fail(re.toString()); + } + + instance.setAttachmentFormatters(new ThrowFormatter[]{new ThrowFormatter()}); + assertEquals(Formatter[].class, instance.getAttachmentFormatters().getClass()); + assertEquals(Filter[].class, instance.getAttachmentFilters().getClass()); + assertEquals(Formatter[].class, instance.getAttachmentNames().getClass()); + + assertEquals(em.exceptions.isEmpty(), true); + instance.close(); + } + + @Test + public void testAttachmentNames_StringArr() { + Formatter[] names; + MailHandler instance = new MailHandler(createInitProperties("")); + InternalErrorManager em = new InternalErrorManager(); + instance.setErrorManager(em); + + names = instance.getAttachmentNames(); + assertNotNull(names); + + try { + instance.setAttachmentNames((String[]) null); + fail("Null was allowed."); + } catch (RuntimeException re) { + assertEquals(NullPointerException.class, re.getClass()); + } + + if (instance.getAttachmentFormatters().length > 0) { + instance.setAttachmentFormatters(new Formatter[0]); + } + + try { + instance.setAttachmentNames(new String[0]); + } catch (RuntimeException re) { + fail(re.toString()); + } + + try { + instance.setAttachmentNames(new String[1]); + fail("Mismatch with attachment formatters."); + } catch (NullPointerException | IndexOutOfBoundsException pass) { + } catch (RuntimeException re) { + fail(re.toString()); + } + + instance.setAttachmentFormatters( + new Formatter[]{new SimpleFormatter(), new XMLFormatter()}); + try { + instance.setAttachmentNames(new String[2]); + fail("Null index was allowed."); + } catch (NullPointerException pass) { + } catch (RuntimeException re) { + fail(re.toString()); + } + + Formatter[] formatters = instance.getAttachmentFormatters(); + names = instance.getAttachmentNames(); + + assertEquals(names[0].toString(), String.valueOf(formatters[0])); + assertEquals(names[1].toString(), String.valueOf(formatters[1])); + + String[] stringNames = new String[]{"error.txt", "error.xml"}; + instance.setAttachmentNames(stringNames); + assertEquals(stringNames[0], instance.getAttachmentNames()[0].toString()); + assertEquals(stringNames[1], instance.getAttachmentNames()[1].toString()); + + stringNames[0] = "info.txt"; + assertEquals(stringNames[0].equals( + instance.getAttachmentNames()[0].toString()), false); + + try { + instance.setAttachmentNames(new String[0]); + fail("Names mismatch formatters."); + } catch (IndexOutOfBoundsException pass) { + } catch (RuntimeException re) { + fail(re.toString()); + } + + assertEquals(true, em.exceptions.isEmpty()); + } + + @Test + public void testAttachmentNames_FormatterArr() { + Formatter[] formatters; + MailHandler instance = new MailHandler(createInitProperties("")); + InternalErrorManager em = new InternalErrorManager(); + instance.setErrorManager(em); + + assertNotNull(instance.getAttachmentNames()); + + try { + instance.setAttachmentNames((Formatter[]) null); + fail("Null was allowed."); + } catch (NullPointerException pass) { + } catch (RuntimeException re) { + fail(re.toString()); + } + + if (instance.getAttachmentFormatters().length > 0) { + instance.setAttachmentFormatters(new Formatter[0]); + } + + try { + instance.setAttachmentNames(new Formatter[2]); + fail("formatter mismatch."); + } catch (NullPointerException | IndexOutOfBoundsException pass) { + } catch (RuntimeException re) { + fail(re.toString()); + } + + instance.setAttachmentFormatters( + new Formatter[]{new SimpleFormatter(), new XMLFormatter()}); + + assertEquals(instance.getAttachmentFormatters().length, + instance.getAttachmentNames().length); + + formatters = new Formatter[]{new SimpleFormatter(), new XMLFormatter()}; + instance.setAttachmentNames(formatters); + formatters[0] = new XMLFormatter(); + assertEquals(formatters[0].equals(instance.getAttachmentNames()[0]), false); + + instance.setAttachmentNames(new ThrowFormatter[]{new ThrowFormatter(), new ThrowFormatter()}); + assertEquals(Formatter[].class, instance.getAttachmentNames().getClass()); + assertEquals(em.exceptions.isEmpty(), true); + instance.close(); + } + + @Test + public void testSubject_String() { + String subject = "Test subject."; + MailHandler instance = new MailHandler(createInitProperties("")); + InternalErrorManager em = new InternalErrorManager(); + instance.setErrorManager(em); + + assertNotNull(instance.getSubject()); + + try { + instance.setSubject((String) null); + fail("Null subject was allowed."); + } catch (NullPointerException pass) { + } catch (RuntimeException re) { + fail(re.toString()); + } + + instance.setSubject(subject); + assertEquals(subject, instance.getSubject().toString()); + + assertEquals(em.exceptions.isEmpty(), true); + instance.close(); + } + + @Test + public void testTailFormatters() { + MailHandler instance = new MailHandler(createInitProperties("")); + InternalErrorManager em = new InternalErrorManager(); + instance.setErrorManager(em); + + instance.setSubject(instance.toString()); + Formatter f1 = instance.getSubject(); + assertEquals(f1, f1); + assertEquals(f1.hashCode(), f1.hashCode()); + assertEquals(f1.toString(), f1.toString()); + + instance.setSubject(instance.toString()); + Formatter f2 = instance.getSubject(); + assertEquals(f2, f2); + assertEquals(f2.hashCode(), f2.hashCode()); + assertEquals(f2.toString(), f2.toString()); + + assertEquals(f1.getClass(), f2.getClass()); + assertEquals(f1.toString(), f2.toString()); + + Formatter same = new XMLFormatter(); + instance.setAttachmentFormatters( + new Formatter[]{same, same}); + Formatter[] formatters = instance.getAttachmentNames(); + f1 = formatters[0]; + f2 = formatters[1]; + + assertEquals(f1, f1); + assertEquals(f1.hashCode(), f1.hashCode()); + assertEquals(f1.toString(), f1.toString()); + + assertEquals(f2, f2); + assertEquals(f2.hashCode(), f2.hashCode()); + assertEquals(f2.toString(), f2.toString()); + + assertEquals(f1.getClass(), f2.getClass()); + assertEquals(f1.toString(), f2.toString()); + + assertFalse(f1.equals(new SimpleFormatter())); + assertFalse(new SimpleFormatter().equals(f1)); + assertFalse(f2.equals(new SimpleFormatter())); + assertFalse(new SimpleFormatter().equals(f2)); + + //New in JavaMail 1.4.4. + assertEquals(f1, f2); + assertEquals(f1.hashCode(), f2.hashCode()); + + assertEquals(em.exceptions.isEmpty(), true); + instance.close(); + } + + @Test + public void testSubject_Formatter() { + Formatter format = new SimpleFormatter(); + MailHandler instance = new MailHandler(createInitProperties("")); + InternalErrorManager em = new InternalErrorManager(); + instance.setErrorManager(em); + + assertNotNull(instance.getSubject()); + assertEquals(CollectorFormatter.class, instance.getSubject().getClass()); + + try { + instance.setSubject((Formatter) null); + fail("Null subject was allowed."); + } catch (NullPointerException pass) { + } catch (RuntimeException re) { + fail(re.toString()); + } + + instance.setSubject(format); + assertEquals(format, instance.getSubject()); + + assertEquals(true, em.exceptions.isEmpty()); + instance.close(); + } + + @Test + public void testAttachmentFilterSwapBeforePush() { + MailHandler instance = new MailHandler(10); + instance.setMailProperties(createInitProperties("")); + instance.setLevel(Level.ALL); + instance.setPushLevel(Level.OFF); + instance.setPushFilter((Filter) null); + instance.setFilter(BooleanFilter.FALSE); + instance.setAttachmentFormatters(new Formatter[]{new XMLFormatter()}); + instance.setAttachmentFilters(new Filter[]{null}); + InternalErrorManager em = new InternalErrorManager(); + instance.setErrorManager(em); + + LogRecord record = new LogRecord(Level.SEVERE, "lost record"); + assertTrue(instance.isLoggable(record)); + + instance.publish(record); + instance.setAttachmentFilters(new Filter[]{BooleanFilter.FALSE}); + assertFalse(instance.isLoggable(record)); + instance.close(); + + int seenFormat = 0; + for (Exception exception : em.exceptions) { + if (exception instanceof MessagingException) { + continue; + } else if (exception instanceof RuntimeException + && exception.getMessage().contains(instance.getFilter().toString()) + && exception.getMessage().contains(Arrays.asList(instance.getAttachmentFilters()).toString())) { + seenFormat++; + continue; //expected. + } else { + fail(String.valueOf(exception)); + } + } + assertTrue("No format error", seenFormat > 0); + } + + @Test + public void testFilterSwapBeforePush() { + MailHandler instance = new MailHandler(10); + instance.setMailProperties(createInitProperties("")); + instance.setLevel(Level.ALL); + instance.setPushLevel(Level.OFF); + instance.setPushFilter((Filter) null); + instance.setAttachmentFormatters(new SimpleFormatter()); + instance.setAttachmentFilters(BooleanFilter.FALSE); + InternalErrorManager em = new InternalErrorManager(); + instance.setErrorManager(em); + + LogRecord record = new LogRecord(Level.SEVERE, "lost record"); + assertTrue(instance.isLoggable(record)); + + instance.publish(record); + instance.setFilter(BooleanFilter.FALSE); + assertFalse(instance.isLoggable(record)); + instance.close(); + + int seenFormat = 0; + for (Exception exception : em.exceptions) { + if (exception instanceof MessagingException) { + continue; + } else if (exception instanceof RuntimeException + && exception.getMessage().contains(instance.getFilter().toString())) { + seenFormat++; + continue; //expected. + } else { + fail(String.valueOf(exception)); + } + } + assertTrue("No format error", seenFormat > 0); + } + + @Test + public void testFilterFlipFlop() { + MailHandler instance = new MailHandlerOverride(10); + instance.setMailProperties(createInitProperties("")); + instance.setLevel(Level.ALL); + instance.setPushLevel(Level.OFF); + instance.setPushFilter((Filter) null); + FlipFlopFilter badFilter = new FlipFlopFilter(); + instance.setFilter(badFilter); + instance.setAttachmentFormatters(new SimpleFormatter()); + + InternalErrorManager em = new InternalErrorManager(); + instance.setErrorManager(em); + + LogRecord record = new LogRecord(Level.SEVERE, "lost record"); + + assertSame(badFilter, instance.getFilter()); + badFilter.value = true; + assertSame(badFilter, instance.getFilter()); + + assertTrue(instance.isLoggable(record)); + instance.publish(record); + badFilter.value = false; + + assertSame(badFilter, instance.getFilter()); + assertFalse(instance.isLoggable(record)); + instance.close(); + assertSame(badFilter, instance.getFilter()); + + int seenFormat = 0; + for (Exception exception : em.exceptions) { + if (exception instanceof MessagingException) { + continue; + } else if (exception instanceof RuntimeException + && exception.getMessage().contains(instance.getFilter().toString())) { + seenFormat++; + continue; //expected. + } else { + fail(String.valueOf(exception)); + } + } + assertTrue("No format error", seenFormat > 0); + } + + @Test + public void testFilterReentrance() { + Logger logger = Logger.getLogger("testFilterReentrance"); + + MailHandler instance = new MailHandler(2); + instance.setMailProperties(createInitProperties("")); + InternalErrorManager em = new InternalErrorManager(); + instance.setErrorManager(em); + instance.setFilter(new ReentranceFilter()); + + logger.setUseParentHandlers(false); + logger.setLevel(Level.ALL); + logger.addHandler(instance); + hardRef = logger; + try { + assertNotNull(hardRef); + logger.logp(Level.SEVERE, MailHandlerTest.class.getName(), "testFilterReentrance", "test"); + + int seenIse = 0; + for (Exception exception : em.exceptions) { + if (exception instanceof MessagingException) { + continue; + } else if (exception instanceof IllegalStateException) { + seenIse++; + continue; //expected. + } else { + fail(String.valueOf(exception)); + } + } + + assertTrue("No IllegalStateException", seenIse > 0); + } finally { + logger.removeHandler(instance); + logger.setLevel((Level) null); + logger.setUseParentHandlers(true); + hardRef = null; + } + } + + @Test + public void testPushFilterReentrance() { + testPushFilterReentrance(1, 1); + testPushFilterReentrance(1, 2); + testPushFilterReentrance(1, 1000); + testPushFilterReentrance(500, 1000); + testPushFilterReentrance(1000, 1000); + } + + private void testPushFilterReentrance(int records, int cap) { + assert records <= cap : records; + Logger logger = Logger.getLogger("testPushFilterReentrance"); + + MailHandler instance = new MailHandler(cap); + + Properties props = createInitProperties(""); + instance.setMailProperties(props); + + InternalErrorManager em = new InternalErrorManager(); + instance.setErrorManager(em); + instance.setPushLevel(Level.ALL); + instance.setPushFilter(new ReentranceFilter()); + + logger.setUseParentHandlers(false); + logger.setLevel(Level.ALL); + logger.addHandler(instance); + hardRef = logger; + try { + assertNotNull(hardRef); + + while (records-- > 0) { + logger.logp(Level.SEVERE, MailHandlerTest.class.getName(), "testPushFilterReentrance", "test"); + } + instance.close(); + + for (Exception exception : em.exceptions) { + Throwable t = exception; + if ((t instanceof MessagingException == false) + && (t instanceof IllegalStateException == false)) { + dump(t); + fail(String.valueOf(t)); + } + } + assertFalse(em.exceptions.isEmpty()); + } finally { + logger.removeHandler(instance); + logger.setLevel((Level) null); + logger.setUseParentHandlers(true); + hardRef = null; + } + } + + @Test + public void testMailDebugLowCap() throws Exception { + MailHandler instance = new MailHandler(1); + try { + testMailDebug(instance, 2); + } finally { + instance.close(); + } + } + + @Test + public void testMailDebugPushLevel() throws Exception { + MailHandler instance = new MailHandler(1000); + try { + instance.setPushLevel(Level.ALL); + testMailDebug(instance, 2); + } finally { + instance.close(); + } + } + + @Test + public void testMailDebugPush() throws Exception { + MailHandler instance = new MailHandler(4); + try { + testMailDebug(instance, -3); + } finally { + instance.close(); + } + } + + @Test + public void testMailDebugFlush() throws Exception { + MailHandler instance = new MailHandler(3); + try { + testMailDebug(instance, -2); + } finally { + instance.close(); + } + } + + @Test + public void testMailDebugClose() throws Exception { + MailHandler instance = new MailHandler(1000); + try { + testMailDebug(instance, -1); + } finally { + instance.close(); + } + } + + @Test + public void testMailDebugErrorManager() throws Exception { + MailHandler instance = new MailHandler(1); + try { + instance.setErrorManager(new MailDebugErrorManager()); + testMailDebug(instance, 2); + } finally { + instance.close(); + } + } + + private void testMailDebug(MailHandler instance, int records) throws Exception { + final Properties props = createInitProperties(""); + props.put("mail.debug", "true"); + props.put("verify", "local"); + + instance.setLevel(Level.ALL); + testMailDebugQuietLog(instance, props, records); + } + + private void testMailDebugQuietLog(MailHandler instance, Properties props, int records) throws Exception { + final Logger root = Logger.getLogger(""); + hardRef = root; + try { + final Handler[] handlers = root.getHandlers(); + for (Handler h : handlers) { + root.removeHandler(h); + } + try { + root.setLevel(Level.ALL); + root.addHandler(instance); + try { + testMailDebugQuietStreams(instance, props, records); + } finally { + root.setLevel(null); + root.removeHandler(instance); + } + } finally { + for (Handler h : handlers) { + root.addHandler(h); + } + } + } finally { + hardRef = null; + } + } + + @SuppressWarnings("UseOfSystemOutOrSystemErr") + private void testMailDebugQuietStreams(MailHandler instance, Properties props, int records) throws Exception { + final PrintStream out = System.out; + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + System.setOut(new PrintStream(baos, true, "ISO-8859-1")); + final PrintStream err = System.err; + try { + System.setErr(new PrintStream(baos, true, "ISO-8859-1")); + instance.setMailProperties(props); + + if (records > 0) { + for (int i = 0; i < records; i++) { + baos.reset(); + instance.publish(new LogRecord(Level.SEVERE, "")); + } + } else { + records = -records; + for (int i = 0; i < records; i++) { + baos.reset(); + instance.publish(new LogRecord(Level.SEVERE, "")); + } + + if (records == 1) { + instance.close(); + } else if (records == 2) { + instance.flush(); + } else if (records == 3) { + instance.push(); + } + } + } finally { + System.setErr(err); + } + } finally { + System.setOut(out); + } + } + + @Test + public void testReportError() { + MailHandler instance = new MailHandler(createInitProperties("")); + instance.setErrorManager(new ErrorManager() { + + @Override + public void error(String msg, Exception ex, int code) { + assertNull(msg); + } + }); + + instance.reportError(null, null, ErrorManager.GENERIC_FAILURE); + + instance.setErrorManager(new ErrorManager() { + + @Override + public void error(String msg, Exception ex, int code) { + assertEquals(msg.indexOf(Level.SEVERE.getName()), 0); + } + }); + + instance.reportError("simple message.", null, ErrorManager.GENERIC_FAILURE); + instance.close(); + + //Test for valid message. + instance = createHandlerWithRecords(); + instance.setErrorManager(new MessageErrorManager(instance.getMailProperties()) { + + @Override + protected void error(MimeMessage message, Throwable t, int code) { + try { + assertTrue(message.getHeader("X-Mailer")[0].startsWith(MailHandler.class.getName())); + assertTrue(null != message.getSentDate()); + message.saveChanges(); + } catch (MessagingException ME) { + fail(ME.toString()); + } + } + }); + instance.close(); + } + + @Test + public void testReportErrorUtf8Addresses() throws Exception { + final String test = "test\u03b1"; + final String saddr = test + '@' + UNKNOWN_HOST; + final String sender = test + saddr; + Properties props = createInitProperties(""); + props.put("mail.to", saddr); + props.put("mail.cc", saddr); + props.put("mail.bcc", saddr); + props.put("mail.from", saddr); + props.put("mail.sender", sender); + props.put("mail.mime.allowutf8", "true"); + MailHandler instance = new MailHandler(props); + instance.setEncoding("UTF-8"); + instance.setFormatter(new SimpleFormatter()); + instance.setErrorManager(new MessageErrorManager(instance.getMailProperties()) { + + @Override + protected void error(MimeMessage message, Throwable t, int code) { + try { + assertEquals(saddr, toString(message.getFrom()[0])); + assertEquals(saddr, toString(message.getRecipients(Message.RecipientType.TO)[0])); + assertEquals(saddr, toString(message.getRecipients(Message.RecipientType.CC)[0])); + assertEquals(saddr, toString(message.getRecipients(Message.RecipientType.BCC)[0])); + + assertEquals(sender, toString(message.getSender())); + assertTrue(String.valueOf(message.getContent()).contains(sender)); + + message.saveChanges(); + } catch (MessagingException | IOException E) { + fail(E.toString()); + } + } + + private String toString(Address o) { + return ((InternetAddress)o).toUnicodeString(); + } + }); + instance.publish(new LogRecord(Level.SEVERE, sender)); + instance.close(); + } + + @Test + public void testReportErrorSuper() throws Exception { + assertNull(System.getSecurityManager()); + Field mhem = MailHandler.class.getDeclaredField("errorManager"); + mhem.setAccessible(true); + + InternalErrorManager superEm = new InternalErrorManager(); + InternalErrorManager em = new InternalErrorManager(); + MailHandler h = new MailHandler(); + try { + Exception tester = new Exception(); + synchronized (h) { + assertSame(h.getErrorManager(), getSuperErrorManager(h)); + + h.setErrorManager(superEm); + assertSame(superEm, getSuperErrorManager(h)); + assertSame(superEm, mhem.get(h)); + + mhem.set(h, em); + assertSame(em, h.getErrorManager()); + assertSame(superEm, getSuperErrorManager(h)); + assertNotSame(h.getErrorManager(), getSuperErrorManager(h)); + h.reportError("", tester, ErrorManager.GENERIC_FAILURE); + } + + assertEquals(1, em.exceptions.size()); + assertSame(tester, em.exceptions.get(0)); + assertTrue(superEm.exceptions.toString(), + superEm.exceptions.isEmpty()); + } finally { + h.close(); + } + } + + @Test + public void testGaeReportErrorSuper() throws Exception { + Field mhem = MailHandler.class.getDeclaredField("errorManager"); + mhem.setAccessible(true); + + InternalErrorManager em = new InternalErrorManager(); + GaeSecurityManager sm = new GaeSecurityManager(); + System.setSecurityManager(sm); + sm.secure = true; + try { + MailHandler h = new MailHandler(); + try { + Exception tester = new Exception(); + synchronized (h) { + sm.secure = false; + final Object def = getSuperErrorManager(h); + assertSame(def, getSuperErrorManager(h)); + sm.secure = true; + + assertEquals(h.getErrorManager().getClass(), def.getClass()); + assertNotSame(h.getErrorManager(), def); + + h.setErrorManager(em); + sm.secure = false; + final Object sem = getSuperErrorManager(h); + sm.secure = true; + assertSame(def, sem); + assertNotSame(h.getErrorManager(), def); + assertNotSame(h.getErrorManager(), sem); + assertSame(h.getErrorManager(), em); + + h.reportError("", tester, ErrorManager.GENERIC_FAILURE); + } + + assertEquals(1, em.exceptions.size()); + assertSame(tester, em.exceptions.get(0)); + } finally { + h.close(); + } + } finally { + sm.secure = false; + System.setSecurityManager((SecurityManager) null); + } + } + + @Test + public void testReportErrorLinkageWithStack() throws Exception { + testReportErrorLinkageWithStack(new LinkageErrorStream()); + } + + @Test + public void testReportErrorRuntimeWithStack() throws Exception { + testReportErrorLinkageWithStack(new RuntimeErrorStream()); + } + + private void testReportErrorLinkageWithStack(PrintStream ps) throws Exception { + MailHandler instance = new MailHandler(createInitProperties("")); + + @SuppressWarnings("UseOfSystemOutOrSystemErr") + final PrintStream err = System.err; + System.setErr(ps); + try { + try { + instance.setErrorManager(new ErrorManager()); + instance.reportError(null, null, ErrorManager.CLOSE_FAILURE); + + instance.setErrorManager(new ErrorManager()); + instance.reportError(null, null, ErrorManager.FLUSH_FAILURE); + + instance.setErrorManager(new ErrorManager()); + instance.reportError(null, null, ErrorManager.GENERIC_FAILURE); + + instance.setErrorManager(new ErrorManager()); + instance.reportError(null, null, ErrorManager.OPEN_FAILURE); + + instance.setErrorManager(new ErrorManager()); + instance.reportError(null, null, ErrorManager.WRITE_FAILURE); + } finally { + System.setErr(err); + } + } catch (PrintThrowsRuntimeException unexpected) { + unexpected.dump(); + fail(unexpected.getMessage()); + } + } + + @Test + public void testReportErrorLinkageEmptyStack() throws Throwable { + testReportErrorLinkageEmptyStack( + new LinkageErrorStream(new StackTraceElement[0])); + } + + @Test + public void testReportErrorLinkageShortStack() throws Throwable { + testReportErrorLinkageEmptyStack( + new LinkageErrorStream(new StackTraceElement[]{ + new StackTraceElement("", "", "", -1)})); + } + + @Test + public void testReportErrorRuntimeEmptyStack() throws Throwable { + testReportErrorLinkageEmptyStack( + new RuntimeErrorStream(new StackTraceElement[0])); + } + + @Test + public void testReportErrorRuntimeShortStack() throws Throwable { + testReportErrorLinkageEmptyStack( + new RuntimeErrorStream(new StackTraceElement[]{ + new StackTraceElement("", "", "", -1)})); + } + + private void testReportErrorLinkageEmptyStack(PrintStream ps) throws Throwable { + MailHandler instance = new MailHandler(createInitProperties("")); + + @SuppressWarnings("UseOfSystemOutOrSystemErr") + final PrintStream err = System.err; + System.setErr(ps); + try { + try { + instance.setErrorManager(new ErrorManager()); + instance.reportError(null, null, ErrorManager.FLUSH_FAILURE); + + instance.setErrorManager(new ErrorManager()); + instance.reportError(null, null, ErrorManager.CLOSE_FAILURE); + + instance.setErrorManager(new ErrorManager()); + instance.reportError(null, null, ErrorManager.GENERIC_FAILURE); + + instance.setErrorManager(new ErrorManager()); + instance.reportError(null, null, ErrorManager.OPEN_FAILURE); + + instance.setErrorManager(new ErrorManager()); + instance.reportError(null, null, ErrorManager.WRITE_FAILURE); + } finally { + System.setErr(err); + } + } catch (PrintThrowsRuntimeException unexpected) { + unexpected.dump(); + fail(unexpected.getMessage()); + } + } + + @Ignore + public void testGaeForbiddenHeaders() throws Exception { + assumeNoJit(); + assertNull(System.getSecurityManager()); + assertTrue(LogManagerProperties.hasLogManager()); + final Class<?> k = LogManagerProperties.class; + final Field f = k.getDeclaredField("LOG_MANAGER"); + final Field mod = setAccessible(f); + try { + final Object lm = f.get(null); + f.set(null, new Properties()); + try { + fullFence(); + assertFalse(LogManagerProperties.hasLogManager()); + MailHandler instance = createHandlerWithRecords(); + instance.setErrorManager(new GaeErrorManager(instance)); + instance.close(); + } finally { + f.set(null, lm); + fullFence(); + } + } finally { + mod.setInt(f, f.getModifiers() | Modifier.FINAL); + } + assertTrue(LogManagerProperties.hasLogManager()); + } + + @Ignore + public void testGaeSecurityManager() throws Exception { + assumeNoJit(); + InternalErrorManager em; + MailHandler h = null; + final GaeSecurityManager manager = new GaeSecurityManager(); + System.setSecurityManager(manager); + try { + manager.secure = false; + h = new MailHandler(createInitProperties("")); + em = new InternalErrorManager(); + h.setErrorManager(em); + manager.secure = true; + assertEquals(manager, System.getSecurityManager()); + + //GAE allows access to loggers. + Logger global = Logger.getLogger("global"); + hardRef = global; + global.addHandler(h); + global.removeHandler(h); + global.removeHandler((Handler) null); + hardRef = null; + + h.postConstruct(); + h.setAttachmentFormatters(new Formatter[]{new ThrowFormatter()}); + assertEquals(1, h.getAttachmentFormatters().length); + + h.setAttachmentFilters(new Filter[]{new ThrowFilter()}); + assertEquals(1, h.getAttachmentFormatters().length); + + assertEquals(1, h.getAttachmentFormatters().length); + h.setAttachmentNames(new String[]{"error.txt"}); + + assertEquals(1, h.getAttachmentFormatters().length); + h.setAttachmentNames(new Formatter[]{new ThrowFormatter()}); + + h.setAuthenticator((Authenticator) null); + h.setComparator((Comparator<? super LogRecord>) null); + + h.setLevel(Level.ALL); + h.setFilter(BooleanFilter.FALSE); + h.setFilter((Filter) null); + h.setFormatter(new EmptyFormatter()); + + assertNotNull(h.getErrorManager()); + h.setErrorManager(new ErrorManager()); + + h.setEncoding((String) null); + + h.flush(); + h.push(); + + h.setMailProperties(new Properties()); + + h.setPushFilter((Filter) null); + h.setPushLevel(Level.OFF); + + h.setSubject(new ThrowFormatter()); + h.setSubject("test"); + + h.getAuthenticator(); + h.getMailProperties(); + + h.preDestroy(); + h.close(); + + //check for internal exceptions caused by security manager. + for (Exception e : em.exceptions) { + dump(e); + } + assertTrue(em.exceptions.isEmpty()); + + hardRef = h = new MailHandler(); + h.close(); + + hardRef = h = new MailHandler(100); + assertEquals(100, h.getCapacity()); + h.close(); + + Properties props = new Properties(); + props.put("test", "test"); + hardRef = h = new MailHandler(props); + assertEquals(props, h.getMailProperties()); + } finally { + hardRef = null; + manager.secure = false; + System.setSecurityManager((SecurityManager) null); + if (h != null) { + h.close(); + } + } + } + + /** + * Test logging permissions of the MailHandler. Must run by itself or run in + * isolated VM. Use system property java.security.debug=all to troubleshoot + * failures. + */ + @Test + public void testSecurityManager() { + InternalErrorManager em; + MailHandler h = null; + final ThrowSecurityManager manager = new ThrowSecurityManager(); + System.setSecurityManager(manager); + try { + manager.secure = false; + h = new MailHandler(createInitProperties("")); + em = new InternalErrorManager(); + h.setErrorManager(em); + manager.secure = true; + assertEquals(manager, System.getSecurityManager()); + + try { + assertEquals(0, h.getAttachmentFormatters().length); + h.setAttachmentNames(new String[]{"error.txt"}); + fail("Missing secure check."); + } catch (SecurityException pass) { + } catch (Exception fail) { + fail(fail.toString()); + } + + try { + assertEquals(0, h.getAttachmentFormatters().length); + h.setAttachmentNames(new Formatter[]{new ThrowFormatter()}); + fail("Missing secure check."); + } catch (SecurityException pass) { + } catch (Exception fail) { + fail(fail.toString()); + } + + try { + assertEquals(0, h.getAttachmentFormatters().length); + h.setAttachmentFilters(new Filter[]{new ThrowFilter()}); + fail("Missing secure check."); + } catch (SecurityException pass) { + } catch (Exception fail) { + fail(fail.toString()); + } + + try { + h.setAttachmentFormatters(new Formatter[]{new ThrowFormatter()}); + fail("Missing secure check."); + } catch (SecurityException pass) { + } catch (Exception fail) { + fail(fail.toString()); + } + + manager.secure = false; + try { + h.setAttachmentFormatters(new Formatter[]{new ThrowFormatter()}); + } catch (SecurityException fail) { + fail("Unexpected secure check."); + } catch (Exception fail) { + fail(fail.toString()); + } finally { + manager.secure = true; + } + + try { + assertEquals(1, h.getAttachmentFormatters().length); + h.setAttachmentFilters(new Filter[]{new ThrowFilter()}); + fail("Missing secure check."); + } catch (SecurityException pass) { + } catch (Exception fail) { + fail(fail.toString()); + } + + try { + assertEquals(1, h.getAttachmentFormatters().length); + h.setAttachmentFilters((Filter[]) null); + fail("Missing secure check."); + } catch (SecurityException | NullPointerException pass) { + } catch (Exception fail) { + fail(fail.toString()); + } + + try { + assertEquals(1, h.getAttachmentFormatters().length); + h.setAttachmentNames(new String[]{"error.txt"}); + fail("Missing secure check."); + } catch (SecurityException pass) { + } catch (Exception fail) { + fail(fail.toString()); + } + + try { + assertEquals(1, h.getAttachmentFormatters().length); + h.setAttachmentNames((String[]) null); + fail("Missing secure check."); + } catch (SecurityException | NullPointerException pass) { + } catch (Exception fail) { + fail(fail.toString()); + } + + try { + assertEquals(1, h.getAttachmentFormatters().length); + h.setAttachmentNames((Formatter[]) null); + fail("Missing secure check."); + } catch (SecurityException | NullPointerException pass) { + } catch (Exception fail) { + fail(fail.toString()); + } + + try { + assertEquals(1, h.getAttachmentFormatters().length); + h.setAttachmentNames(new Formatter[]{new ThrowFormatter()}); + fail("Missing secure check."); + } catch (SecurityException pass) { + } catch (Exception fail) { + fail(fail.toString()); + } + + manager.secure = false; + try { + assertEquals(1, h.getAttachmentFormatters().length); + h.setAttachmentFormatters(new Formatter[0]); + } catch (SecurityException fail) { + fail(fail.toString()); + } catch (Exception fail) { + fail(fail.toString()); + } finally { + manager.secure = true; + } + + try { + assertEquals(0, h.getAttachmentFormatters().length); + assertEquals(0, h.getAttachmentFilters().length); + assertEquals(0, h.getAttachmentNames().length); + } catch (SecurityException fail) { + fail(fail.toString()); + } catch (Exception fail) { + fail(fail.toString()); + } + + try { + h.setAuthenticator((Authenticator) null); + fail("Missing secure check."); + } catch (SecurityException | NullPointerException pass) { + } catch (Exception fail) { + fail(fail.toString()); + } + + try { + h.setComparator((Comparator<? super LogRecord>) null); + fail("Missing secure check."); + } catch (SecurityException | NullPointerException pass) { + } catch (Exception fail) { + fail(fail.toString()); + } + + try { + h.getComparator(); + } catch (SecurityException fail) { + fail(fail.toString()); + } catch (Exception fail) { + fail(fail.toString()); + } + + try { + h.setLevel(Level.ALL); + fail("Missing secure check."); + } catch (SecurityException pass) { + } catch (Exception fail) { + fail(fail.toString()); + } + + try { + h.setLevel((Level) null); + fail("Missing secure check."); + } catch (SecurityException | NullPointerException pass) { + } catch (Exception fail) { + fail(fail.toString()); + } + + try { + h.getLevel(); + } catch (SecurityException fail) { + fail(fail.toString()); + } catch (Exception fail) { + fail(fail.toString()); + } + + try { + h.setFilter(BooleanFilter.FALSE); + fail("Missing secure check."); + } catch (SecurityException pass) { + } catch (Exception fail) { + fail(fail.toString()); + } + + try { + h.setFilter((Filter) null); + fail("Missing secure check."); + } catch (SecurityException pass) { + } catch (Exception fail) { + fail(fail.toString()); + } + + try { + h.getFilter(); + } catch (SecurityException fail) { + fail(fail.toString()); + } catch (Exception fail) { + fail(fail.toString()); + } + + try { + h.setFormatter(new EmptyFormatter()); + fail("Missing secure check."); + } catch (SecurityException pass) { + } catch (Exception fail) { + fail(fail.toString()); + } + + try { + h.setFormatter((Formatter) null); + fail("Missing secure check."); + } catch (SecurityException | NullPointerException pass) { + } catch (Exception fail) { + fail(fail.toString()); + } + + try { + h.getFormatter(); + } catch (SecurityException fail) { + fail(fail.toString()); + } catch (Exception fail) { + fail(fail.toString()); + } + + try { + assertNotNull(h.getErrorManager()); + fail("Missing secure check."); + } catch (SecurityException pass) { + } catch (Exception fail) { + fail(fail.toString()); + } + + try { + h.setErrorManager(new ErrorManager()); + fail("Missing secure check."); + } catch (SecurityException pass) { + } catch (Exception fail) { + fail(fail.toString()); + } + + try { + h.setErrorManager((ErrorManager) null); + fail("Missing secure check."); + } catch (SecurityException | NullPointerException pass) { + } catch (Exception fail) { + fail(fail.toString()); + } + + try { + h.setEncoding((String) null); + fail("Missing secure check."); + } catch (SecurityException pass) { + } catch (Exception fail) { + fail(fail.toString()); + } + + try { + h.getEncoding(); + } catch (SecurityException fail) { + fail(fail.toString()); + } catch (Exception fail) { + fail(fail.toString()); + } + + try { + h.flush(); + } catch (SecurityException fail) { + fail(fail.toString()); + } catch (Exception fail) { + fail(fail.toString()); + } + + try { + h.push(); + } catch (SecurityException fail) { + fail(fail.toString()); + } catch (Exception fail) { + fail(fail.toString()); + } + + try { + h.setMailProperties(new Properties()); + fail("Missing secure check."); + } catch (SecurityException pass) { + } catch (Exception fail) { + fail(fail.toString()); + } + + try { + h.setMailProperties((Properties) null); + fail("Missing secure check."); + } catch (SecurityException | NullPointerException pass) { + } catch (Exception fail) { + fail(fail.toString()); + } + + try { + h.setPushFilter((Filter) null); + fail("Missing secure check."); + } catch (SecurityException | NullPointerException pass) { + } catch (Exception fail) { + fail(fail.toString()); + } + + try { + h.getPushFilter(); + } catch (SecurityException fail) { + fail(fail.toString()); + } catch (Exception fail) { + fail(fail.toString()); + } + + try { + h.setPushLevel(Level.OFF); + fail("Missing secure check."); + } catch (SecurityException pass) { + } catch (Exception fail) { + fail(fail.toString()); + } + + try { + h.setPushLevel((Level) null); + fail("Missing secure check."); + } catch (SecurityException | NullPointerException pass) { + } catch (Exception fail) { + fail(fail.toString()); + } + + try { + h.getPushLevel(); + } catch (SecurityException fail) { + fail(fail.toString()); + } catch (Exception fail) { + fail(fail.toString()); + } + + try { + h.setSubject((Formatter) null); + fail("Missing secure check."); + } catch (SecurityException | NullPointerException pass) { + } catch (Exception fail) { + fail(fail.toString()); + } + + try { + h.setSubject((String) null); + fail("Missing secure check."); + } catch (SecurityException | NullPointerException pass) { + } catch (Exception fail) { + fail(fail.toString()); + } + + try { + h.setSubject(new ThrowFormatter()); + fail("Missing secure check."); + } catch (SecurityException pass) { + } catch (Exception fail) { + fail(fail.toString()); + } + + try { + h.setSubject("test"); + fail("Missing secure check."); + } catch (SecurityException pass) { + } catch (Exception fail) { + fail(fail.toString()); + } + + try { + h.getSubject(); + } catch (SecurityException fail) { + fail(fail.toString()); + } catch (Exception fail) { + fail(fail.toString()); + } + + try { + assertTrue(h.getCapacity() > 0); + } catch (SecurityException fail) { + fail(fail.toString()); + } catch (Exception fail) { + fail(fail.toString()); + } + + try { + h.getAuthenticator(); + fail("Missing secure check."); + } catch (SecurityException pass) { + } catch (Exception fail) { + fail(fail.toString()); + } + + try { + h.getMailProperties(); + fail("Missing secure check."); + } catch (SecurityException pass) { + } catch (Exception fail) { + fail(fail.toString()); + } + + try { + h.close(); + fail("Missing secure check."); + } catch (SecurityException pass) { + } catch (Exception fail) { + fail(fail.toString()); + } + + try { + h.publish(new LogRecord(Level.SEVERE, "")); + h.flush(); + } catch (SecurityException fail) { + fail(fail.toString()); + } catch (Exception fail) { + fail(fail.toString()); + } + + //check for internal exceptions caused by security manager. + next: + for (Exception e : em.exceptions) { + for (Throwable t = e; t != null; t = t.getCause()) { + if (t instanceof SecurityException) { + continue next; //expected + } else if (t instanceof RuntimeException) { + throw (RuntimeException) t; //fail + } + } + } + em.exceptions.clear(); + + try { + hardRef = new MailHandler(); + fail("Missing secure check."); + } catch (SecurityException pass) { + } catch (Exception fail) { + fail(fail.toString()); + } + + try { + hardRef = new MailHandler(100); + fail("Missing secure check."); + } catch (SecurityException pass) { + } catch (Exception fail) { + fail(fail.toString()); + } + + try { + hardRef = new MailHandler(new Properties()); + fail("Missing secure check."); + } catch (SecurityException pass) { + } catch (Exception fail) { + fail(fail.toString()); + } + + try { + hardRef = new MailHandler(-100); + fail("Missing secure check."); + } catch (SecurityException | IllegalArgumentException pass) { + } catch (Exception fail) { + fail(fail.toString()); + } + + try { + hardRef = new MailHandler((Properties) null); + fail("Missing secure check."); + } catch (SecurityException | NullPointerException pass) { + } catch (Exception fail) { + fail(fail.toString()); + } + } finally { + hardRef = null; + manager.secure = false; + System.setSecurityManager((SecurityManager) null); + if (h != null) { + h.close(); + } + } + } + + @Test + public void testVerifyErrorManager() throws Exception { + LogManager manager = LogManager.getLogManager(); + try { + manager.reset(); + + final String p = MailHandler.class.getName(); + Properties props = createInitProperties(p); + props.put(p.concat(".encoding"), "us-ascii"); + props.put(p.concat(".mail.to"), "foo@bar.com"); + props.put(p.concat(".mail.cc"), "fizz@buzz.com"); + props.put(p.concat(".mail.bcc"), "baz@bar.com"); + props.put(p.concat(".subject"), p.concat(" test")); + props.put(p.concat(".mail.from"), "localhost@localdomain"); + props.put(p.concat(".mail.sender"), "mail@handler"); + props.put(p.concat(".errorManager"), VerifyErrorManager.class.getName()); + props.put(p.concat(".verify"), "remote"); + + read(manager, props); + + MailHandler instance = new MailHandler(); + InternalErrorManager em = internalErrorManagerFrom(instance); + + //ensure VerifyErrorManager was installed. + assertEquals(VerifyErrorManager.class, em.getClass()); + + for (Exception exception : em.exceptions) { + final Throwable t = exception; + if (!isConnectOrTimeout(t)) { + dump(t); + fail(t.toString()); + } + } + + assertFalse(em.exceptions.isEmpty()); + instance.close(); + } finally { + manager.reset(); + } + } + + @Test + public void testVerifyNoContent() throws Exception { + Properties props = createInitProperties(""); + Session session = Session.getInstance(props); + MimeMessage msg = new MimeMessage(session); + Address[] from = InternetAddress.parse("me@localhost", false); + msg.addFrom(from); + msg.setRecipients(Message.RecipientType.TO, from); + ByteArrayOutputStream out = new ByteArrayOutputStream(384); + msg.setHeader("Content-Transfer-Encoding", "base64"); + msg.saveChanges(); + try { + msg.writeTo(out); + fail("Verify type 'remote' may send a message with no content."); + } catch (MessagingException | IOException expect) { + msg.setContent("", "text/plain"); + msg.saveChanges(); + msg.writeTo(out); + } finally { + out.close(); + } + } + + @Test + public void testIsMissingContent() throws Exception { + Properties props = createInitProperties(""); + + MailHandler target = new MailHandler(props); + Session session = Session.getInstance(props); + MimeMessage msg = new MimeMessage(session); + Address[] from = InternetAddress.parse("me@localhost", false); + msg.addFrom(from); + msg.setRecipients(Message.RecipientType.TO, from); + msg.setHeader("Content-Transfer-Encoding", "base64"); + msg.saveChanges(); + try { + msg.writeTo(new ByteArrayOutputStream(384)); + fail("Verify type 'remote' may hide remote exceptions."); + } catch (RuntimeException re) { + throw re; //Avoid catch all. + } catch (Exception expect) { + assertNotNull(expect.getMessage()); + assertTrue(expect.getMessage().length() != 0); + assertTrue(target.isMissingContent(msg, expect)); + assertTrue(target.isMissingContent(msg, new Exception(expect))); + assertTrue(target.isMissingContent(msg, new MessagingException("", expect))); + assertFalse(target.isMissingContent(msg, new Exception())); + assertFalse(target.isMissingContent(msg, new RuntimeException())); + } + } + + @Test + public void testIntern() throws Exception { + assertNull(System.getSecurityManager()); + final String p = MailHandler.class.getName(); + Properties props = createInitProperties(p); + props.put(p.concat(".errorManager"), + InternFilterErrorManager.class.getName()); + props.put(p.concat(".comparator"), + InternFilterFormatterComparator.class.getName()); + props.put(p.concat(".filter"), InternFilter.class.getName()); + props.put(p.concat(".pushFilter"), + InternFilterErrorManager.class.getName()); + + props.put(p.concat(".attachment.formatters"), + SimpleFormatter.class.getName() + ", " + + InternFilterFormatter.class.getName() + ", " + + InternFormatter.class.getName() + ", " + + XMLFormatter.class.getName() + ", " + + InternFormatter.class.getName() + ", " + + SimpleFormatter.class.getName() + ", " + + SimpleFormatter.class.getName()); + + props.put(p.concat(".attachment.filters"), + null + ", " + + InternFilterFormatter.class.getName() + ", " + + InternFilterFormatter.class.getName() + ", " + + InternFilter.class.getName() + ", " + + InternFilter.class.getName() + ", " + + InternBadSubFilter.class.getName() + ", " + + InternBadFilter.class.getName()); + + final String txt = "Intern test"; + props.put(p.concat(".attachment.names"), + txt + ", " + + InternFilterFormatter.class.getName() + ", " + + InternFilterFormatter.class.getName() + ", " + + txt + ", " + + InternFormatter.class.getName() + ", " + + InternFilterFormatterComparator.class.getName() + ", " + + InternFilterFormatterComparator.class.getName()); + props.put(p.concat(".subject"), txt); + + MailHandler instance = testIntern(p, props); + instance.close(); + + Formatter[] formatter = instance.getAttachmentFormatters(); + Filter[] filter = instance.getAttachmentFilters(); + Formatter[] names = instance.getAttachmentNames(); + + assertSame(instance.getErrorManager(), instance.getPushFilter()); + assertNull(filter[0]); + assertSame(filter[1], filter[2]); + assertSame(filter[1], formatter[1]); + assertSame(filter[2], formatter[1]); + assertSame(names[1], filter[1]); + assertSame(names[1], formatter[1]); + assertSame(names[2], filter[2]); + assertSame(names[2], formatter[1]); + assertSame(names[1], filter[2]); + assertNotNull(instance.getSubject()); + assertSame(instance.getSubject(), names[0]); + assertSame(instance.getSubject(), names[3]); + assertSame(names[0], names[3]); + assertNotNull(instance.getFilter()); + assertSame(instance.getFilter(), filter[3]); + assertSame(instance.getFilter(), filter[4]); + assertSame(filter[3], filter[4]); + assertNotSame(filter[5], filter[6]); //Bad equals method. + assertNotNull(instance.getComparator()); //Comparator is not interned. + assertNotSame(instance.getComparator(), names[5]); + assertNotSame(instance.getComparator(), names[6]); + assertSame(names[5], names[6]); + + InternalErrorManager em = internalErrorManagerFrom(instance); + for (Exception exception : em.exceptions) { + final Throwable t = exception; + if (t instanceof IllegalArgumentException + && String.valueOf(t.getMessage()).contains("equal")) { + continue; + } + dump(t); + } + assertFalse(em.exceptions.isEmpty()); + } + + private MailHandler testIntern(String p, Properties props) throws Exception { + props.put(p.concat(".mail.to"), "badAddress"); + props.put(p.concat(".mail.cc"), "badAddress"); + props.put(p.concat(".mail.from"), "badAddress"); + + final LogManager manager = LogManager.getLogManager(); + read(manager, props); + try { + return new MailHandler(); + } finally { + manager.reset(); + } + } + + @Test + public void testComparatorReverse() throws Exception { + final String p = MailHandler.class.getName(); + Properties props = createInitProperties(p); + props.put(p.concat(".comparator"), SequenceComparator.class.getName()); + props.put(p.concat(".comparator.reverse"), "false"); + props.put(p.concat(".errorManager"), InternalErrorManager.class.getName()); + + LogRecord low = new LogRecord(Level.INFO, ""); + LogRecord high = new LogRecord(Level.INFO, ""); + final LogManager manager = LogManager.getLogManager(); + try { + read(manager, props); + MailHandler instance = new MailHandler(); + instance.close(); + InternalErrorManager em = internalErrorManagerFrom(instance); + for (Exception exception : em.exceptions) { + final Throwable t = exception; + dump(t); + fail(t.toString()); + } + assertTrue(em.exceptions.isEmpty()); + + assertEquals(SequenceComparator.class, + instance.getComparator().getClass()); + + assertTrue(instance.getComparator().compare(low, high) < 0); + assertTrue(instance.getComparator().compare(high, low) > 0); + + props.put(p.concat(".comparator.reverse"), "true"); + read(manager, props); + instance = new MailHandler(); + instance.close(); + em = internalErrorManagerFrom(instance); + for (Exception exception : em.exceptions) { + final Throwable t = exception; + dump(t); + fail(t.toString()); + } + assertTrue(em.exceptions.isEmpty()); + + Comparator<? super LogRecord> c = instance.getComparator(); + assertTrue(SequenceComparator.class != c.getClass()); + assertFalse(instance.getComparator().compare(low, high) < 0); + assertFalse(instance.getComparator().compare(high, low) > 0); + + props.put(p.concat(".comparator"), + SequenceComparatorWithReverse.class.getName()); + read(manager, props); + instance = new MailHandler(); + instance.close(); + em = internalErrorManagerFrom(instance); + for (Exception exception : em.exceptions) { + final Throwable t = exception; + dump(t); + fail(t.toString()); + } + assertTrue(em.exceptions.isEmpty()); + + c = instance.getComparator(); + assertTrue(SequenceDescComparator.class == c.getClass()); + assertFalse(instance.getComparator().compare(low, high) < 0); + assertFalse(instance.getComparator().compare(high, low) > 0); + + props.put(p.concat(".comparator"), ""); + read(manager, props); + instance = new MailHandler(); + instance.close(); + em = internalErrorManagerFrom(instance); + for (Exception exception : em.exceptions) { + final Throwable t = exception; + if (t instanceof IllegalArgumentException) { + continue; + } + dump(t); + fail(t.toString()); + } + + assertFalse(IllegalArgumentException.class.getName(), + em.exceptions.isEmpty()); + + props.put(p.concat(".comparator"), "null"); + read(manager, props); + instance = new MailHandler(); + instance.close(); + em = internalErrorManagerFrom(instance); + for (Exception exception : em.exceptions) { + final Throwable t = exception; + if (t instanceof IllegalArgumentException) { + continue; + } + dump(t); + fail(t.toString()); + } + + assertFalse(IllegalArgumentException.class.getName(), + em.exceptions.isEmpty()); + + props.remove(p.concat(".comparator")); + read(manager, props); + instance = new MailHandler(); + instance.close(); + em = internalErrorManagerFrom(instance); + for (Exception exception : em.exceptions) { + final Throwable t = exception; + if (t instanceof IllegalArgumentException) { + continue; + } + dump(t); + fail(t.toString()); + } + + assertFalse(IllegalArgumentException.class.getName(), + em.exceptions.isEmpty()); + } finally { + manager.reset(); + } + } + + @Test + public void testVerifyLogManager() throws Exception { + LogManager manager = LogManager.getLogManager(); + try { + manager.reset(); + + final String p = MailHandler.class.getName(); + Properties props = createInitProperties(p); + props.put(p.concat(".subject"), p.concat(" test")); + props.put(p.concat(".errorManager"), InternalErrorManager.class.getName()); + props.put(p.concat(".verify"), "limited"); + + read(manager, props); + + MailHandler instance = new MailHandler(); + InternalErrorManager em = internalErrorManagerFrom(instance); + + assertEquals(InternalErrorManager.class, em.getClass()); + + for (Exception exception : em.exceptions) { + final Throwable t = exception; + if (t instanceof AddressException == false) { + dump(t); + fail(t.toString()); + } + } + assertFalse(em.exceptions.isEmpty()); + + instance.close(); + + props.put(p.concat(".verify"), "local"); + + read(manager, props); + + instance = new MailHandler(); + em = internalErrorManagerFrom(instance); + + assertEquals(InternalErrorManager.class, em.getClass()); + + for (Exception exception : em.exceptions) { + final Throwable t = exception; + if (t instanceof AddressException == false) { + dump(t); + fail(t.toString()); + } + } + assertFalse(em.exceptions.isEmpty()); + + instance.close(); + + props.put(p.concat(".verify"), "resolve"); + + read(manager, props); + + instance = new MailHandler(); + em = internalErrorManagerFrom(instance); + + assertEquals(InternalErrorManager.class, em.getClass()); + + for (Exception exception : em.exceptions) { + final Throwable t = exception; + if (isConnectOrTimeout(t)) { + continue; + } + if (t instanceof AddressException == false) { + dump(t); + fail(t.toString()); + } + } + assertFalse(em.exceptions.isEmpty()); + + instance.close(); + + props.put(p.concat(".verify"), "remote"); + read(manager, props); + + instance = new MailHandler(); + em = internalErrorManagerFrom(instance); + + assertEquals(InternalErrorManager.class, em.getClass()); + + for (Exception exception : em.exceptions) { + final Throwable t = exception; + if (t instanceof AddressException) { + continue; + } else if (isConnectOrTimeout(t)) { + continue; + } else { + dump(t); + fail(t.toString()); + } + } + assertFalse(em.exceptions.isEmpty()); + } finally { + manager.reset(); + } + } + + @Test + public void testVerifyProperties() throws Exception { + Properties props = createInitProperties(""); + props.put("subject", "test"); + props.put("verify", "limited"); + + InternalErrorManager em = new InternalErrorManager(); + MailHandler instance = new MailHandler(); + try { + instance.setErrorManager(em); + instance.setMailProperties(props); + + for (Exception exception : em.exceptions) { + final Throwable t = exception; + if (t instanceof AddressException == false) { + dump(t); + fail(t.toString()); + } + } + } finally { + instance.close(); + } + + props.put("verify", "local"); + instance = new MailHandler(); + try { + em = new InternalErrorManager(); + instance.setErrorManager(em); + instance.setMailProperties(props); + + for (Exception exception : em.exceptions) { + final Throwable t = exception; + if (t instanceof AddressException == false) { + dump(t); + fail(t.toString()); + } + } + } finally { + instance.close(); + } + + props.put("verify", "resolve"); + instance = new MailHandler(); + try { + em = new InternalErrorManager(); + instance.setErrorManager(em); + instance.setMailProperties(props); + + for (Exception exception : em.exceptions) { + final Throwable t = exception; + if (isConnectOrTimeout(t)) { + continue; + } + if (t instanceof AddressException == false) { + dump(t); + fail(t.toString()); + } + } + } finally { + instance.close(); + } + + props.put("verify", "login"); + instance = new MailHandler(); + try { + em = new InternalErrorManager(); + instance.setErrorManager(em); + instance.setMailProperties(props); + + for (Exception exception : em.exceptions) { + final Throwable t = exception; + if (isConnectOrTimeout(t)) { + continue; + } + if (t instanceof AddressException == false) { + dump(t); + fail(t.toString()); + } + } + } finally { + instance.close(); + } + + props.put("verify", "remote"); + instance = new MailHandler(); + try { + em = new InternalErrorManager(); + instance.setErrorManager(em); + instance.setMailProperties(props); + + for (Exception exception : em.exceptions) { + final Throwable t = exception; + if (t instanceof AddressException) { + continue; + } else if (isConnectOrTimeout(t)) { + continue; + } else { + dump(t); + fail(t.toString()); + } + } + } finally { + instance.close(); + } + } + + @Test + public void testVerifyPropertiesConstructor() throws Exception { + LogManager manager = LogManager.getLogManager(); + try { + manager.reset(); + + final String p = MailHandler.class.getName(); + Properties props = createInitProperties(p); + props.put(p.concat(".subject"), p.concat(" test")); + props.put(p.concat(".errorManager"), InternalErrorManager.class.getName()); + props.put(p.concat(".formatter"), XMLFormatter.class.getName()); + read(manager, props); + + props = createInitProperties(""); + props.put("subject", "test"); + props.put("verify", "limited"); + + MailHandler instance = new MailHandler(props); + try { + InternalErrorManager em = internalErrorManagerFrom(instance); + + for (Exception exception : em.exceptions) { + final Throwable t = exception; + if (t instanceof AddressException == false) { + dump(t); + fail(t.toString()); + } + } + assertFalse(em.exceptions.isEmpty()); + } finally { + instance.close(); + } + + props = createInitProperties(""); + props.put("mail.to", "badAddress"); + props.put("mail.cc", "badAddress"); + props.put("subject", "test"); + props.put("mail.from", "badAddress"); + props.put("verify", "local"); + + instance = new MailHandler(props); + try { + InternalErrorManager em = internalErrorManagerFrom(instance); + + for (Exception exception : em.exceptions) { + final Throwable t = exception; + if (t instanceof AddressException == false) { + dump(t); + fail(t.toString()); + } + } + assertFalse(em.exceptions.isEmpty()); + } finally { + instance.close(); + } + + props.put("verify", "resolve"); + + instance = new MailHandler(props); + try { + InternalErrorManager em = internalErrorManagerFrom(instance); + + for (Exception exception : em.exceptions) { + final Throwable t = exception; + if (isConnectOrTimeout(t)) { + continue; + } + if (t instanceof AddressException == false) { + dump(t); + fail(t.toString()); + } + } + assertFalse(em.exceptions.isEmpty()); + } finally { + instance.close(); + } + + props.put("verify", "remote"); + instance = new MailHandler(props); + try { + InternalErrorManager em = internalErrorManagerFrom(instance); + + for (Exception exception : em.exceptions) { + final Throwable t = exception; + if (t instanceof AddressException) { + continue; + } else if (isConnectOrTimeout(t)) { + continue; + } else { + dump(t); + fail(t.toString()); + } + } + assertFalse(em.exceptions.isEmpty()); + } finally { + instance.close(); + } + } finally { + manager.reset(); + } + } + + @Test + public void testNoVerifyReplacedProperties() throws Exception { + LogManager manager = LogManager.getLogManager(); + try { + manager.reset(); + + final String p = MailHandler.class.getName(); + Properties props = createInitProperties(p); + props.put(p.concat(".subject"), p.concat(" test")); + props.put(p.concat(".errorManager"), InternalErrorManager.class.getName()); + props.put(p.concat(".verify"), "remote"); + + read(manager, props); + + //Use empty properties to prove fallback to LogManager. + MailHandler instance = new MailHandler(new Properties()); + InternalErrorManager em = internalErrorManagerFrom(instance); + assertEquals(InternalErrorManager.class, em.getClass()); + instance.close(); + + for (Exception exception : em.exceptions) { + final Throwable t = exception; + dump(t); + fail(t.toString()); + } + assertTrue(em.exceptions.isEmpty()); + } finally { + manager.reset(); + } + } + + @Test + public void testInitSubject() throws Exception { + InternalErrorManager em; + MailHandler target; + final String p = MailHandler.class.getName(); + final LogManager manager = LogManager.getLogManager(); + Properties props = createInitProperties(p); + props.put(p.concat(".errorManager"), InternalErrorManager.class.getName()); + + //test class cast. + props.put(p.concat(".subject"), Properties.class.getName()); + + read(manager, props); + + try { + target = new MailHandler(); + try { + em = internalErrorManagerFrom(target); + for (Exception exception : em.exceptions) { + dump(exception); + } + assertTrue(em.exceptions.isEmpty()); + } finally { + target.close(); + } + + //test linkage error. + props.put(p.concat(".subject"), ThrowFormatter.class.getName().toUpperCase(Locale.US)); + read(manager, props); + + target = new MailHandler(); + try { + em = internalErrorManagerFrom(target); + for (Exception exception : em.exceptions) { + dump(exception); + } + assertTrue(em.exceptions.isEmpty()); + } finally { + target.close(); + } + + //test mixed linkage error. + props.put(p.concat(".subject"), Properties.class.getName().toUpperCase(Locale.US)); + read(manager, props); + + target = new MailHandler(); + try { + em = internalErrorManagerFrom(target); + for (Exception exception : em.exceptions) { + dump(exception); + } + assertTrue(em.exceptions.isEmpty()); + } finally { + target.close(); + } + } finally { + manager.reset(); + } + } + + @Test + public void testInitAuthenticator() throws Exception { + //Liternal null and null reference mean no Authenticator + //when dealing with a properties file. + testInitAuthenticator("user", null); + testInitAuthenticator("user", "null"); + testInitAuthenticator("user", "NULL"); + testInitAuthenticator("user", "Null"); + testInitAuthenticator("user", ""); + testInitAuthenticator("user", "somepassword"); + } + + private void testInitAuthenticator(String user, String pass) throws Exception { + InternalErrorManager em; + MailHandler target; + final String p = MailHandler.class.getName(); + final LogManager manager = LogManager.getLogManager(); + final Properties props = createInitProperties(p); + props.put(p.concat(".errorManager"), InternalErrorManager.class.getName()); + if (pass != null) { + props.put(p.concat(".authenticator"), pass); + } + props.put(p.concat(".mail.transport.protocol"), "smtp"); + + read(manager, props); + try { + target = new MailHandler(); + try { + em = internalErrorManagerFrom(target); + for (Exception exception : em.exceptions) { + dump(exception); + } + assertTrue(em.exceptions.isEmpty()); + } finally { + target.close(); + } + + if (pass != null && !"null".equalsIgnoreCase(pass)) { + assertNotNull(target.getAuthenticator()); + PasswordAuthentication initPa = passwordAuthentication( + target.getAuthenticator(), user); + assertEquals(user, initPa.getUserName()); + assertEquals(pass, initPa.getPassword()); + + target.setAuthenticator(pass.toCharArray()); + PasswordAuthentication setPa = passwordAuthentication( + target.getAuthenticator(), user); + assertEquals(setPa.getUserName(), initPa.getUserName()); + assertEquals(setPa.getPassword(), initPa.getPassword()); + } else { + assertNull(target.getAuthenticator()); + target.setAuthenticator((char[]) null); + assertNull(target.getAuthenticator()); + } + } finally { + manager.reset(); + } + } + + @Test + public void testInitAttachmentFilters() throws Exception { + InternalErrorManager em; + MailHandler target; + final String p = MailHandler.class.getName(); + final LogManager manager = LogManager.getLogManager(); + final Properties props = createInitProperties(p); + props.put(p.concat(".errorManager"), InternalErrorManager.class.getName()); + props.put(p.concat(".filter"), ErrorFilter.class.getName()); + props.put(p.concat(".attachment.formatters"), SimpleFormatter.class.getName()); + props.put(p.concat(".attachment.names"), Properties.class.getName()); + assertNull(props.getProperty(p.concat(".attachment.filters"))); + + read(manager, props); + try { + target = new MailHandler(); + try { + em = internalErrorManagerFrom(target); + for (Exception exception : em.exceptions) { + dump(exception); + } + assertTrue(em.exceptions.isEmpty()); + } finally { + target.close(); + } + } finally { + manager.reset(); + } + + assertEquals(ErrorFilter.class, target.getFilter().getClass()); + assertEquals(target.getFilter(), target.getAttachmentFilters()[0]); + + props.put(p.concat(".attachment.formatters"), + SimpleFormatter.class.getName() + ", " + + SimpleFormatter.class.getName() + ", " + + SimpleFormatter.class.getName() + ", " + + SimpleFormatter.class.getName()); + props.put(p.concat(".attachment.names"), "a.txt, b.txt, c.txt, d.txt"); + props.put(p.concat(".attachment.filters"), "null, " + + ThrowFilter.class.getName()); + + read(manager, props); + try { + target = new MailHandler(); + try { + em = internalErrorManagerFrom(target); + for (Exception exception : em.exceptions) { + final Throwable t = exception; + if (t instanceof IndexOutOfBoundsException) { + continue; + } + dump(t); + } + assertFalse(em.exceptions.isEmpty()); + } finally { + target.close(); + } + } finally { + manager.reset(); + } + + assertEquals(ErrorFilter.class, target.getFilter().getClass()); + assertNull(target.getAttachmentFilters()[0]); + assertEquals(ThrowFilter.class, + target.getAttachmentFilters()[1].getClass()); + assertEquals(target.getFilter(), target.getAttachmentFilters()[2]); + assertEquals(target.getFilter(), target.getAttachmentFilters()[3]); + } + + @Test + public void testInitAttachmentNames() throws Exception { + InternalErrorManager em; + MailHandler target; + final String p = MailHandler.class.getName(); + final LogManager manager = LogManager.getLogManager(); + Properties props = createInitProperties(p); + props.put(p.concat(".errorManager"), InternalErrorManager.class.getName()); + + //test class cast. + props.put(p.concat(".attachment.formatters"), SimpleFormatter.class.getName()); + props.put(p.concat(".attachment.names"), Properties.class.getName()); + + read(manager, props); + + try { + target = new MailHandler(); + try { + em = internalErrorManagerFrom(target); + for (Exception exception : em.exceptions) { + dump(exception); + } + assertTrue(em.exceptions.isEmpty()); + } finally { + target.close(); + } + + //test linkage error. + props.put(p.concat(".attachment.formatters"), SimpleFormatter.class.getName()); + props.put(p.concat(".attachment.names"), SimpleFormatter.class.getName().toUpperCase(Locale.US)); + + read(manager, props); + + target = new MailHandler(); + try { + em = internalErrorManagerFrom(target); + for (Exception exception : em.exceptions) { + dump(exception); + } + assertTrue(em.exceptions.isEmpty()); + } finally { + target.close(); + } + + //test mixed linkage error. + props.put(p.concat(".attachment.formatters"), SimpleFormatter.class.getName()); + props.put(p.concat(".attachment.names"), Properties.class.getName().toUpperCase(Locale.US)); + read(manager, props); + + target = new MailHandler(); + try { + em = internalErrorManagerFrom(target); + for (Exception exception : em.exceptions) { + dump(exception); + } + assertTrue(em.exceptions.isEmpty()); + } finally { + target.close(); + } + } finally { + manager.reset(); + } + } + + @Test + public void testInitErrorManagerException() throws Exception { + final String encoding = System.getProperty("file.encoding", "8859_1"); + final String p = MailHandler.class.getName(); + final Properties props = createInitProperties(p); + String key; + + setPending(new RuntimeException()); + try { + key = p.concat(".errorManager"); + props.put(key, InitErrorManager.class.getName()); + + final LogManager manager = LogManager.getLogManager(); + try { + read(manager, props); + ByteArrayOutputStream oldErrors = new ByteArrayOutputStream(); + PrintStream newErr = new PrintStream(oldErrors, false, encoding); + @SuppressWarnings("UseOfSystemOutOrSystemErr") + final PrintStream err = System.err; + System.setErr(newErr); + try { + final MailHandler target = new MailHandler(); + try { + System.setErr(err); + target.setErrorManager(new ErrorManager()); + } finally { + target.close(); + } + } finally { + System.setErr(err); + } + + //java.util.logging.ErrorManager: 4 + //java.lang.reflect.InvocationTargetException + // at... + //Caused by: java.lang.RuntimeException + final String data = oldErrors.toString(encoding); + assertTrue(data, data.contains(ErrorManager.class.getName())); + int ite, re; + ite = data.indexOf(InvocationTargetException.class.getName()); + re = data.indexOf(RuntimeException.class.getName(), ite); + assertTrue(data, ite < re); + } finally { + manager.reset(); + } + } finally { + setPending((Throwable) null); + } + } + + @Test + public void testInitErrorManagerError() throws Exception { + final String encoding = System.getProperty("file.encoding", "8859_1"); + final String p = MailHandler.class.getName(); + final Properties props = createInitProperties(p); + String key; + + setPending(new Error()); + try { + key = p.concat(".errorManager"); + props.put(key, InitErrorManager.class.getName()); + + final LogManager manager = LogManager.getLogManager(); + try { + read(manager, props); + ByteArrayOutputStream oldErrors = new ByteArrayOutputStream(); + PrintStream newErr = new PrintStream(oldErrors, false, encoding); + @SuppressWarnings("UseOfSystemOutOrSystemErr") + final PrintStream err = System.err; + System.setErr(newErr); + try { + final MailHandler target = new MailHandler(); + try { + System.setErr(err); + target.setErrorManager(new ErrorManager()); + } finally { + target.close(); + } + } finally { + System.setErr(err); + } + + //java.util.logging.ErrorManager: 4 + //java.lang.reflect.InvocationTargetException + // at... + //Caused by: java.lang.Error + final String data = oldErrors.toString(encoding); + assertTrue(data, data.contains(ErrorManager.class.getName())); + int ite, re; + ite = data.indexOf(InvocationTargetException.class.getName()); + re = data.indexOf(Error.class.getName(), ite); + assertTrue(data, ite < re); + } finally { + manager.reset(); + } + } finally { + setPending((Throwable) null); + } + } + + @Test + public void testInitError() throws Exception { + final String p = MailHandler.class.getName(); + final Properties props = createInitProperties(p); + String filter; + String name; + String key; + + setPending(new Error()); + try { + key = p.concat(".authenticator"); + props.put(key, InitAuthenticator.class.getName()); + testInitError(props); + assertNotNull(props.remove(key)); + + key = p.concat(".comparator"); + props.put(key, InitComparator.class.getName()); + testInitError(props); + assertNotNull(props.remove(key)); + + key = p.concat(".filter"); + props.put(key, InitFilter.class.getName()); + testInitError(props); + assertNotNull(props.remove(key)); + + key = p.concat(".formatter"); + props.put(key, InitFormatter.class.getName()); + testInitError(props); + assertNotNull(props.remove(key)); + + key = p.concat(".subject"); + props.put(key, InitFormatter.class.getName()); + testInitError(props); + assertNotNull(props.remove(key)); + + key = p.concat(".attachment.formatters"); + props.put(key, InitFormatter.class.getName()); + testInitError(props); + assertNotNull(props.remove(key)); + + key = p.concat(".attachment.formatters"); + props.put(key, ThrowFormatter.class.getName()); + filter = p.concat(".attachment.filters"); + props.put(filter, InitFilter.class.getName()); + name = p.concat(".attachment.names"); + props.put(name, "test.txt"); + testInitError(props); + assertNotNull(props.remove(key)); + assertNotNull(props.remove(name)); + assertNotNull(props.remove(filter)); + + key = p.concat(".attachment.formatters"); + props.put(key, ThrowFormatter.class.getName()); + name = p.concat(".attachment.names"); + props.put(name, InitFormatter.class.getName()); + testInitError(props); + assertNotNull(props.remove(key)); + assertNotNull(props.remove(name)); + } finally { + setPending((Throwable) null); + } + } + + private void testInitError(Properties props) throws Exception { + testInitException(props); + } + + @Test + public void testInitException() throws Exception { + final String p = MailHandler.class.getName(); + final Properties props = createInitProperties(p); + String filter; + String name; + String key; + + setPending(new RuntimeException()); + try { + key = p.concat(".authenticator"); + props.put(key, InitAuthenticator.class.getName()); + testInitException(props); + assertNotNull(props.remove(key)); + + key = p.concat(".comparator"); + props.put(key, InitComparator.class.getName()); + testInitException(props); + assertNotNull(props.remove(key)); + + key = p.concat(".filter"); + props.put(key, InitFilter.class.getName()); + testInitException(props); + assertNotNull(props.remove(key)); + + key = p.concat(".formatter"); + props.put(key, InitFormatter.class.getName()); + testInitException(props); + assertNotNull(props.remove(key)); + + key = p.concat(".subject"); + props.put(key, InitFormatter.class.getName()); + testInitException(props); + assertNotNull(props.remove(key)); + + key = p.concat(".attachment.formatters"); + props.put(key, InitFormatter.class.getName()); + testInitException(props); + assertNotNull(props.remove(key)); + + key = p.concat(".attachment.formatters"); + props.put(key, ThrowFormatter.class.getName()); + filter = p.concat(".attachment.filters"); + props.put(filter, InitFilter.class.getName()); + name = p.concat(".attachment.names"); + props.put(name, "test.txt"); + testInitException(props); + assertNotNull(props.remove(key)); + assertNotNull(props.remove(name)); + assertNotNull(props.remove(filter)); + + key = p.concat(".attachment.formatters"); + props.put(key, ThrowFormatter.class.getName()); + name = p.concat(".attachment.names"); + props.put(name, InitFormatter.class.getName()); + testInitException(props); + assertNotNull(props.remove(key)); + assertNotNull(props.remove(name)); + } finally { + setPending((Throwable) null); + } + } + + private void testInitException(Properties props) throws Exception { + final LogManager manager = LogManager.getLogManager(); + try { + read(manager, props); + final MailHandler target = new MailHandler(); + try { + InternalErrorManager em = internalErrorManagerFrom(target); + next: + for (Exception t : em.exceptions) { + for (Throwable cause = t; cause != null; cause = cause.getCause()) { + if (cause == getPending()) { + continue next; + } + } + dump(t); + fail(t.toString()); + } + assertFalse(em.exceptions.isEmpty()); + } finally { + target.close(); + } + } finally { + manager.reset(); + } + } + + @Test + public void testStaticInitErrorManagerException() throws Exception { + final String encoding = System.getProperty("file.encoding", "8859_1"); + final String test = MailHandlerTest.class.getName(); + final String p = MailHandler.class.getName(); + final Properties props = createInitProperties(p); + String key; + + setPending(new RuntimeException()); + try { + key = p.concat(".errorManager"); + props.put(key, test.concat("$StaticInitReErrorManager")); + + final LogManager manager = LogManager.getLogManager(); + try { + read(manager, props); + ByteArrayOutputStream oldErrors = new ByteArrayOutputStream(); + PrintStream newErr = new PrintStream(oldErrors, false, encoding); + @SuppressWarnings("UseOfSystemOutOrSystemErr") + final PrintStream err = System.err; + System.setErr(newErr); + try { + final MailHandler target = new MailHandler(); + try { + System.setErr(err); + target.setErrorManager(new ErrorManager()); + } finally { + target.close(); + } + } finally { + System.setErr(err); + } + + //java.util.logging.ErrorManager: 4 + //java.lang.reflect.InvocationTargetException + // at .... + //Caused by: java.lang.ExceptionInInitializerError + // at... + //Caused by: java.lang.RuntimeException + final String data = oldErrors.toString(encoding); + assertTrue(data, data.contains(ErrorManager.class.getName())); + int ite, eiie, re; + ite = data.indexOf(InvocationTargetException.class.getName()); + eiie = data.indexOf(ExceptionInInitializerError.class.getName(), ite); + if (eiie < 0) { + re = data.indexOf(RuntimeException.class.getName(), ite); + assertTrue(data, ite < re); + } else { + re = data.indexOf(RuntimeException.class.getName(), eiie); + assertTrue(data, ite < eiie); + assertTrue(data, eiie < re); + } + } finally { + manager.reset(); + } + assertNotNull(props.remove(key)); + } finally { + setPending((Throwable) null); + } + } + + @Test + public void testStaticInitException() throws Exception { + final String test = MailHandlerTest.class.getName(); + final String p = MailHandler.class.getName(); + final Properties props = createInitProperties(p); + String filter; + String name; + String key; + + setPending(new RuntimeException()); + try { + key = p.concat(".authenticator"); + props.put(key, test.concat("$StaticInitReAuthenticator")); + testStaticInitException(props); + assertNotNull(props.remove(key)); + + key = p.concat(".comparator"); + props.put(key, test.concat("$StaticInitReComparator")); + testStaticInitException(props); + assertNotNull(props.remove(key)); + + key = p.concat(".filter"); + props.put(key, test.concat("$StaticInitReFilter")); + testStaticInitException(props); + assertNotNull(props.remove(key)); + + key = p.concat(".formatter"); + props.put(key, test.concat("$StaticInitReFormatter")); + testStaticInitException(props); + assertNotNull(props.remove(key)); + + key = p.concat(".subject"); + props.put(key, test.concat("$StaticInitReSubjectFormatter")); + testStaticInitException(props); + assertNotNull(props.remove(key)); + + key = p.concat(".attachment.formatters"); + props.put(key, test.concat("$StaticInitReAttachFormatter")); + testStaticInitException(props); + assertNotNull(props.remove(key)); + + key = p.concat(".attachment.formatters"); + props.put(key, ThrowFormatter.class.getName()); + filter = p.concat(".attachment.filters"); + props.put(filter, test.concat("$StaticInitReAttachFilter")); + name = p.concat(".attachment.names"); + props.put(name, "test.txt"); + testStaticInitException(props); + assertNotNull(props.remove(key)); + assertNotNull(props.remove(name)); + assertNotNull(props.remove(filter)); + + key = p.concat(".attachment.formatters"); + props.put(key, ThrowFormatter.class.getName()); + name = p.concat(".attachment.names"); + props.put(name, test.concat("$StaticInitReNameFormatter")); + testStaticInitException(props); + assertNotNull(props.remove(key)); + assertNotNull(props.remove(name)); + } finally { + setPending((Throwable) null); + } + } + + private void testStaticInitException(Properties props) throws Exception { + testInitException(props); + } + + @Test + public void testStaticInitError() throws Exception { + final String test = MailHandlerTest.class.getName(); + final String p = MailHandler.class.getName(); + final Properties props = createInitProperties(p); + String filter; + String name; + String key; + + setPending(new Error()); + try { + key = p.concat(".authenticator"); + props.put(key, test.concat("$StaticInitErAuthenticator")); + testStaticInitError(props); + assertNotNull(props.remove(key)); + + key = p.concat(".comparator"); + props.put(key, test.concat("$StaticInitErComparator")); + testStaticInitError(props); + assertNotNull(props.remove(key)); + + key = p.concat(".errorManager"); + props.put(key, test.concat("$StaticInitErErrorManager")); + testStaticInitError(props); + assertNotNull(props.remove(key)); + + key = p.concat(".filter"); + props.put(key, test.concat("$StaticInitErFilter")); + testStaticInitError(props); + assertNotNull(props.remove(key)); + + key = p.concat(".formatter"); + props.put(key, test.concat("$StaticInitErFormatter")); + testStaticInitError(props); + assertNotNull(props.remove(key)); + + key = p.concat(".subject"); + props.put(key, test.concat("$StaticInitErSubjectFormatter")); + testStaticInitError(props); + assertNotNull(props.remove(key)); + + key = p.concat(".attachment.formatters"); + props.put(key, test.concat("$StaticInitErAttachFormatter")); + testStaticInitError(props); + assertNotNull(props.remove(key)); + + key = p.concat(".attachment.formatters"); + props.put(key, ThrowFormatter.class.getName()); + filter = p.concat(".attachment.filters"); + props.put(filter, test.concat("$StaticInitErAttachFilter")); + name = p.concat(".attachment.names"); + props.put(name, "test.txt"); + testStaticInitError(props); + assertNotNull(props.remove(key)); + assertNotNull(props.remove(name)); + assertNotNull(props.remove(filter)); + + key = p.concat(".attachment.formatters"); + props.put(key, ThrowFormatter.class.getName()); + name = p.concat(".attachment.names"); + props.put(name, test.concat("$StaticInitErNameFormatter")); + testStaticInitError(props); + assertNotNull(props.remove(key)); + assertNotNull(props.remove(name)); + } finally { + setPending((Throwable) null); + } + } + + @SuppressWarnings("ThrowableInitCause") + private void testStaticInitError(Properties props) throws Exception { + final LogManager manager = LogManager.getLogManager(); + try { + read(manager, props); + MailHandler target = null; + try { + target = new MailHandler(); + AssertionError AE = new AssertionError(props.toString()); + AE.initCause(getPending()); + throw AE; + } catch (AssertionError e) { + throw e; //avoid catch all. + } catch (Error expect) { + assertEquals(Error.class, expect.getClass()); + } finally { + if (target != null) { + target.close(); + } + } + } finally { + manager.reset(); + } + } + + static Properties createInitProperties(String p) { + final Properties props = new Properties(); + if (p.length() != 0) { + p = p.concat("."); + } + props.put(p.concat("mail.host"), UNKNOWN_HOST); + props.put(p.concat("mail.smtp.host"), UNKNOWN_HOST); + props.put(p.concat("mail.smtp.port"), Integer.toString(OPEN_PORT)); + props.put(p.concat("mail.to"), ""); + props.put(p.concat("mail.cc"), "badAddress"); + props.put(p.concat("mail.from"), ""); + props.put(p.concat("mail.smtp.connectiontimeout"), "1"); + props.put(p.concat("mail.smtp.timeout"), "1"); + props.put(p.concat("errorManager"), InternalErrorManager.class.getName()); + return props; + } + + @Test + public void testInitFromLogManager() throws Exception { + final LogManager manager = LogManager.getLogManager(); + synchronized (manager) { + try { + initGoodTest(MailHandler.class, new Class<?>[0], new Object[0]); + initBadTest(MailHandler.class, new Class<?>[0], new Object[0]); + initGoodTest(MailHandler.class, + new Class<?>[]{Integer.TYPE}, new Object[]{10}); + initBadTest(MailHandler.class, + new Class<?>[]{Integer.TYPE}, new Object[]{100}); + initGoodTest(MailHandler.class, + new Class<?>[]{Properties.class}, + new Object[]{new Properties()}); + initBadTest(MailHandler.class, + new Class<?>[]{Properties.class}, + new Object[]{new Properties()}); + + //Test subclass properties. + initGoodTest(MailHandlerExt.class, + new Class<?>[0], new Object[0]); + initBadTest(MailHandlerExt.class, + new Class<?>[0], new Object[0]); + + initGoodTest(MailHandlerExt.class, + new Class<?>[]{Integer.TYPE}, new Object[]{10}); + initBadTest(MailHandlerExt.class, + new Class<?>[]{Integer.TYPE}, new Object[]{100}); + + initGoodTest(MailHandlerExt.class, + new Class<?>[]{Properties.class}, + new Object[]{new Properties()}); + initBadTest(MailHandlerExt.class, + new Class<?>[]{Properties.class}, + new Object[]{new Properties()}); + } finally { + manager.reset(); + } + } + } + + private static String freeTextSubject() { + String name = "Mail Handler test subject"; + try { + Class.forName(name); //ensure this can't be loaded. + fail("Invalid subject: " + name); + } catch (AssertionError fail) { + throw fail; + } catch (Throwable expected) { + } + return name; + } + + private void initGoodTest(Class<? extends MailHandler> type, + Class<?>[] types, Object[] params) throws Exception { + + final String p = type.getName(); + Properties props = createInitProperties(p); + props.put(p.concat(".errorManager"), InternalErrorManager.class.getName()); + props.put(p.concat(".capacity"), "10"); + props.put(p.concat(".level"), "ALL"); + props.put(p.concat(".formatter"), XMLFormatter.class.getName()); + props.put(p.concat(".filter"), ThrowFilter.class.getName()); + props.put(p.concat(".authenticator"), EmptyAuthenticator.class.getName()); + props.put(p.concat(".pushLevel"), "WARNING"); + props.put(p.concat(".pushFilter"), ThrowFilter.class.getName()); + props.put(p.concat(".comparator"), ThrowComparator.class.getName()); + props.put(p.concat(".encoding"), "UTF-8"); + props.put(p.concat(".subject"), EmptyFormatter.class.getName()); + + props.put(p.concat(".attachment.filters"), + "null, " + ThrowFilter.class.getName() + ", " + + ThrowFilter.class.getName()); + + props.put(p.concat(".attachment.formatters"), + SimpleFormatter.class.getName() + ", " + + XMLFormatter.class.getName() + ", " + + SimpleFormatter.class.getName()); + + props.put(p.concat(".attachment.names"), "msg.txt, " + + SimpleFormatter.class.getName() + ", error.txt"); + + read(LogManager.getLogManager(), props); + + MailHandler h = type.getConstructor(types).newInstance(params); + assertEquals(10, h.getCapacity()); + assertEquals(Level.ALL, h.getLevel()); + assertEquals(ThrowFilter.class, h.getFilter().getClass()); + assertEquals(XMLFormatter.class, h.getFormatter().getClass()); + assertEquals(Level.WARNING, h.getPushLevel()); + assertEquals(ThrowFilter.class, h.getPushFilter().getClass()); + assertEquals(ThrowComparator.class, h.getComparator().getClass()); + assertEquals("UTF-8", h.getEncoding()); + assertEquals(EmptyFormatter.class, h.getSubject().getClass()); + assertEquals(EmptyAuthenticator.class, h.getAuthenticator().getClass()); + assertEquals(3, h.getAttachmentFormatters().length); + assertTrue(null != h.getAttachmentFormatters()[0]); + assertTrue(null != h.getAttachmentFormatters()[1]); + assertTrue(null != h.getAttachmentFormatters()[2]); + assertEquals(3, h.getAttachmentFilters().length); + assertEquals(null, h.getAttachmentFilters()[0]); + assertEquals(ThrowFilter.class, h.getAttachmentFilters()[1].getClass()); + assertEquals(ThrowFilter.class, h.getAttachmentFilters()[2].getClass()); + assertEquals(3, h.getAttachmentNames().length); + assertTrue(null != h.getAttachmentNames()[0]); + assertTrue(null != h.getAttachmentNames()[1]); + assertTrue(null != h.getAttachmentNames()[2]); + + InternalErrorManager em = internalErrorManagerFrom(h); + for (Exception exception : em.exceptions) { + fail(String.valueOf(exception)); + } + assertTrue(em.exceptions.isEmpty()); + + h.setComparator(null); + h.close(); + assertEquals(em.exceptions.isEmpty(), true); + + props.put(p.concat(".subject"), freeTextSubject()); + + read(LogManager.getLogManager(), props); + + h = type.getConstructor(types).newInstance(params); + em = internalErrorManagerFrom(h); + assertTrue(em.exceptions.isEmpty()); + assertEquals(freeTextSubject(), h.getSubject().toString()); + + props.remove(p.concat(".attachment.filters")); + read(LogManager.getLogManager(), props); + + h = type.getConstructor(types).newInstance(params); + em = internalErrorManagerFrom(h); + assertTrue(em.exceptions.isEmpty()); + assertEquals(3, h.getAttachmentFormatters().length); + h.close(); + + props.remove(p.concat(".attachment.names")); + read(LogManager.getLogManager(), props); + + h = type.getConstructor(types).newInstance(params); + em = internalErrorManagerFrom(h); + assertTrue(em.exceptions.isEmpty()); + assertEquals(h.getAttachmentFormatters().length, 3); + h.close(); + } + + private PasswordAuthentication passwordAuthentication(javax.mail.Authenticator auth, String user) { + final Session s = Session.getInstance(new Properties(), auth); + return s.requestPasswordAuthentication(null, 25, "SMTP", "", user); + } + + @SuppressWarnings("UseOfSystemOutOrSystemErr") + private void initBadTest(Class<? extends MailHandler> type, + Class<?>[] types, Object[] params) throws Exception { + final String encoding = System.getProperty("file.encoding", "8859_1"); + final PrintStream err = System.err; + ByteArrayOutputStream oldErrors = new ByteArrayOutputStream(); + + final String p = type.getName(); + Properties props = createInitProperties(p); + + props.put(p.concat(".errorManager"), "InvalidErrorManager"); + props.put(p.concat(".capacity"), "-10"); + props.put(p.concat(".level"), "BAD"); + props.put(p.concat(".formatter"), "InvalidFormatter"); + props.put(p.concat(".filter"), "InvalidFilter"); + props.put(p.concat(".authenticator"), "password"); + props.put(p.concat(".pushLevel"), "PUSHBAD"); + props.put(p.concat(".pushFilter"), "InvalidPushFilter"); + props.put(p.concat(".comparator"), "InvalidComparator"); + props.put(p.concat(".encoding"), "MailHandler-ENC"); + props.put(p.concat(".subject"), ThrowFilter.class.getName()); + props.put(p.concat(".attachment.filters"), "null, " + + "InvalidAttachFilter1, " + ThrowFilter.class.getName()); + + props.put(p.concat(".attachment.formatters"), + "InvalidAttachFormatter0, " + + ThrowComparator.class.getName() + ", " + + XMLFormatter.class.getName()); + + props.put(p.concat(".attachment.names"), "msg.txt, " + + ThrowComparator.class.getName() + ", " + + XMLFormatter.class.getName()); + + MailHandler h = null; + oldErrors.reset(); + System.setErr(new PrintStream(oldErrors, false, encoding)); + try { + /** + * Bad level value for property: + * com.sun.mail.util.logging.MailHandler.level The + * LogManager.setLevelsOnExistingLoggers triggers an error. This + * code swallows that error message. + */ + read(LogManager.getLogManager(), props); + System.err.print(""); //flushBuffer. + System.err.flush(); + String result = oldErrors.toString(encoding).trim(); + oldErrors.reset(); + if (result.length() > 0) { + final String expect = "Bad level value for property: " + p + ".level"; + //if (result.length() > expect.length()) { + // result = result.substring(0, expect.length()); + //} + assertEquals(expect, result); + } + + /** + * The default error manager writes to System.err. Since this test + * is trying to install an invalid ErrorManager we can only capture + * the error by capturing System.err. + */ + h = type.getConstructor(types).newInstance(params); + System.err.flush(); + result = oldErrors.toString(encoding).trim(); + int index = result.indexOf(ErrorManager.class.getName() + ": " + + ErrorManager.OPEN_FAILURE + ": " + Level.SEVERE.getName() + + ": InvalidErrorManager"); + assertTrue(index > -1); + assertTrue(result.indexOf( + "java.lang.ClassNotFoundException: InvalidErrorManager") > index); + oldErrors.reset(); + } finally { + System.setErr(err); + } + + assert h != null; + assertEquals(ErrorManager.class, h.getErrorManager().getClass()); + assertTrue(h.getCapacity() != 10); + assertTrue(h.getCapacity() != -10); + assertEquals(Level.WARNING, h.getLevel()); + assertEquals(null, h.getFilter()); + assertEquals(SimpleFormatter.class, h.getFormatter().getClass()); + assertEquals(Level.OFF, h.getPushLevel()); + assertEquals(null, h.getPushFilter()); + assertNull(h.getComparator()); + assertEquals(null, h.getEncoding()); + assertEquals(ThrowFilter.class.getName(), h.getSubject().toString()); + PasswordAuthentication pa = passwordAuthentication(h.getAuthenticator(), "user"); + assertEquals("user", pa.getUserName()); + assertEquals("password", pa.getPassword()); + assertEquals(3, h.getAttachmentFormatters().length); + assertTrue(null != h.getAttachmentFormatters()[0]); + assertTrue(null != h.getAttachmentFormatters()[1]); + assertTrue(null != h.getAttachmentFormatters()[2]); + assertEquals(3, h.getAttachmentFilters().length); + assertTrue(null == h.getAttachmentFilters()[0]); + assertTrue(null == h.getAttachmentFilters()[1]); + assertTrue(null != h.getAttachmentFilters()[2]); + assertEquals(ThrowFilter.class, h.getAttachmentFilters()[2].getClass()); + assertEquals(3, h.getAttachmentNames().length); + assertTrue(null != h.getAttachmentNames()[0]); + assertTrue(null != h.getAttachmentNames()[1]); + assertTrue(null != h.getAttachmentNames()[2]); + assertEquals(XMLFormatter.class, h.getAttachmentNames()[2].getClass()); + h.close(); + } + + private static boolean isConnectOrTimeout(Throwable t) { + if (t instanceof MessagingException) { + Throwable cause = t.getCause(); + if (cause == null) { //GNU JavaMail doesn't support 1.4 chaining. + cause = ((MessagingException) t).getNextException(); + } + return isConnectOrTimeout(cause); + } else if (isInstanceof(t, "com.sun.mail.util.SocketConnectException")) { + return isConnectOrTimeout(t.getCause()); + } else { + return t instanceof java.net.ConnectException + || t instanceof java.net.UnknownHostException + || t instanceof java.net.SocketTimeoutException; + } + } + + private static boolean isInstanceof(Object o, String s) { + if (s == null) { + throw new NullPointerException(); + } + + if (o != null) { + for (Class<?> k = o.getClass(); k != null; k = k.getSuperclass()) { + if (s.equals(k.getClass().getName())) { + return true; + } + } + } + return false; + } + + private static boolean isNoRecipientAddress(Throwable t) { + if (t instanceof MessagingException) { + return String.valueOf(t).contains("No recipient addresses"); + } + return false; + } + + private static InternalErrorManager internalErrorManagerFrom(Handler h) { + final ErrorManager em = h.getErrorManager(); + if (em instanceof InternalErrorManager) { + return (InternalErrorManager) em; + } + throw new ClassCastException(String.valueOf(em)); + } + + /** + * http://www.iana.org/assignments/port-numbers + * + * @return a open dynamic port. + */ + private static int findOpenPort() { + final int MAX_PORT = 65535; + for (int i = 49152; i <= MAX_PORT; ++i) { + if (checkUnusedPort(i)) { + return i; + } + } + + try { + close(new Socket("localhost", MAX_PORT)); + return MAX_PORT; + } catch (Throwable t) { //Config error or fix isConnectOrTimeout method. + throw new Error("Can't find open port.", t); + } + } + + private static boolean checkUnusedPort(int port) { + try { + close(new Socket("localhost", port)); + } catch (UnknownHostException UHE) { + throw new AssertionError(UHE); + } catch (IOException IOE) { + return isConnectOrTimeout(IOE); + } + return false; //listening. + } + + private static void close(Socket s) { + try { + s.close(); + } catch (IOException ignore) { + } + } + + private static abstract class MessageErrorManager extends InternalErrorManager { + + private final Properties props; + + protected MessageErrorManager(final Properties props) { + if (props == null) { + throw new NullPointerException(); + } + this.props = props; + } + + @Override + public final void error(String msg, Exception ex, int code) { + super.error(msg, ex, code); + if (msg != null && msg.length() > 0 + && !msg.startsWith(Level.SEVERE.getName())) { + MimeMessage message; + try { //Headers can be UTF-8 or US-ASCII. + byte[] b = msg.getBytes("UTF-8"); + assertTrue(b.length > 0); + + ByteArrayInputStream in = new ByteArrayInputStream(b); + Session session = Session.getInstance(props); + message = new MimeMessage(session, in); + error(message, ex, code); + } catch (Error e) { + throw e; + } catch (Throwable T) { + fail(T.toString()); + } + } else { + new ErrorManager().error(msg, ex, code); + fail("Message.writeTo failed."); + } + } + + protected abstract void error(MimeMessage msg, Throwable t, int code); + } + + public static class PushErrorManager extends MessageErrorManager { + + public PushErrorManager(MailHandler h) { + super(h.getMailProperties()); + } + + protected PushErrorManager(Properties p) { + super(p); + } + + @Override + protected void error(MimeMessage message, Throwable t, int code) { + try { + assertNotNull(message.getSentDate()); + assertNotNull(message.getDescription()); + assertNotNull(message.getHeader("X-Priority")); + assertEquals("2", message.getHeader("X-Priority")[0]); + assertNotNull(message.getHeader("Importance")); + assertEquals("High", message.getHeader("Importance")[0]); + assertNotNull(message.getHeader("Priority")); + assertEquals("urgent", message.getHeader("Priority")[0]); + assertEquals("auto-generated", message.getHeader("auto-submitted")[0]); + message.saveChanges(); + } catch (RuntimeException | MessagingException RE) { + dump(RE); + fail(RE.toString()); + } + } + } + + public static final class VerifyErrorManager extends PushErrorManager { + + public VerifyErrorManager() { + super(new Properties()); + } + + @Override + protected void error(MimeMessage message, Throwable t, int code) { + super.error(message, t, code); + try { + final Locale locale = Locale.getDefault(); + String lang = LogManagerProperties.toLanguageTag(locale); + if (lang.length() != 0) { + assertEquals(lang, message.getHeader("Accept-Language", null)); + } else { + assertEquals("", locale.getLanguage()); + } + + Address[] a = message.getRecipients(Message.RecipientType.TO); + assertEquals(InternetAddress.parse("foo@bar.com")[0], a[0]); + assertEquals(1, a.length); + + a = message.getRecipients(Message.RecipientType.CC); + assertEquals(InternetAddress.parse("fizz@buzz.com")[0], a[0]); + assertEquals(1, a.length); + + a = message.getRecipients(Message.RecipientType.BCC); + assertEquals(InternetAddress.parse("baz@bar.com")[0], a[0]); + assertEquals(1, a.length); + + a = message.getFrom(); + assertEquals(InternetAddress.parse("localhost@localdomain")[0], a[0]); + assertEquals(1, a.length); + + a = new Address[]{message.getSender()}; + assertEquals(InternetAddress.parse("mail@handler")[0], a[0]); + + assertEquals(MailHandler.class.getName() + " test", message.getSubject()); + + assertNotNull(message.getHeader("Incomplete-Copy", null)); + + assertTrue(message.getContentType(), message.isMimeType("multipart/mixed")); + Multipart multipart = (Multipart) message.getContent(); + MimePart body = (MimePart) multipart.getBodyPart(0); + ContentType type = new ContentType(body.getContentType()); + assertEquals("text/plain", type.getBaseType()); + assertEquals("us-ascii", type.getParameter("charset").toLowerCase(Locale.US)); + assertEquals("auto-generated", message.getHeader("auto-submitted")[0]); + + if (lang.length() != 0) { + assertEquals(lang, body.getHeader("Accept-Language", null)); + } else { + assertEquals("", locale.getLanguage()); + } + } catch (MessagingException | IOException me) { + dump(me); + fail(me.toString()); + } + } + } + + public static final class FlushErrorManager extends MessageErrorManager { + + public FlushErrorManager(MailHandler h) { + super(h.getMailProperties()); + } + + @Override + protected void error(MimeMessage message, Throwable t, int code) { + try { + assertTrue(null != message.getSentDate()); + assertNotNull(message.getDescription()); + assertNull(message.getHeader("X-Priority")); + assertNull(message.getHeader("Importance")); + assertNull(message.getHeader("Priority")); + assertEquals("auto-generated", message.getHeader("auto-submitted")[0]); + message.saveChanges(); + } catch (RuntimeException | MessagingException RE) { + dump(RE); + fail(RE.toString()); + } + } + } + + public static class ThrowFilter implements Filter { + + @SuppressWarnings("override") //JDK-6954234 + public boolean isLoggable(LogRecord record) { + throw new RuntimeException(record.toString()); + } + } + + public static final class ThrowComparator + implements Comparator<LogRecord>, Serializable { + + private static final long serialVersionUID = 8493707928829966353L; + + @SuppressWarnings("override") //JDK-6954234 + public int compare(LogRecord o1, LogRecord o2) { + throw new RuntimeException(); + } + } + + public static final class ThrowFormatter extends Formatter { + + @Override + public String format(LogRecord record) { + throw new RuntimeException("format"); + } + + @Override + public String getHead(Handler h) { + throw new RuntimeException("head"); + } + + @Override + public String getTail(Handler h) { + throw new RuntimeException("tail"); + } + } + + public static class UselessComparator + implements Comparator<LogRecord>, Serializable { + + private static final long serialVersionUID = 7973575043680596722L; + + @SuppressWarnings("override") //JDK-6954234 + public int compare(LogRecord o1, LogRecord o2) { + return o1.toString().compareTo(o2.toString()); + } + }; + + public static class SequenceComparator + implements Comparator<LogRecord>, Serializable { + + private static final long serialVersionUID = 1L; + + @SuppressWarnings("override") //JDK-6954234 + public int compare(LogRecord o1, LogRecord o2) { + long s1 = o1.getSequenceNumber(); + long s2 = o2.getSequenceNumber(); + return s1 < s2 ? -1 : s1 > s2 ? 1 : 0; + } + } + + public static class SequenceDescComparator + implements Comparator<LogRecord>, Serializable { + + private static final long serialVersionUID = 1L; + + @SuppressWarnings("override") //JDK-6954234 + public int compare(LogRecord o1, LogRecord o2) { + long s1 = o1.getSequenceNumber(); + long s2 = o2.getSequenceNumber(); + return s1 < s2 ? 1 : s1 > s2 ? -1 : 0; + } + } + + public static final class SequenceComparatorWithReverse + implements Comparator<LogRecord>, Serializable { + + private static final long serialVersionUID = 1L; + + @SuppressWarnings("override") //JDK-6954234 + public int compare(LogRecord o1, LogRecord o2) { + long s1 = o1.getSequenceNumber(); + long s2 = o2.getSequenceNumber(); + return s1 < s2 ? -1 : s1 > s2 ? 1 : 0; + } + + @SuppressWarnings("override") + public Comparator<LogRecord> reversed() { + return new SequenceDescComparator(); + } + } + + public static class RawTypeComparator + implements Comparator<Object>, Serializable { + + private static final long serialVersionUID = -6539179106541617400L; + + @SuppressWarnings("override") //JDK-6954234 + public int compare(Object o1, Object o2) { + long s1 = LogRecord.class.cast(o1).getSequenceNumber(); + long s2 = LogRecord.class.cast(o2).getSequenceNumber(); + return s1 < s2 ? -1 : s1 > s2 ? 1 : 0; + } + } + + public static final class ActivationErrorManager extends InternalErrorManager { + + @Override + public void error(String msg, Exception ex, int code) { + if (isDataTypeError(msg)) { + Exception e = new UnsupportedDataTypeException(msg); + super.error(msg, e, code); + } else { + if (!isConnectOrTimeout(ex)) { + super.error(msg, ex, code); + } else { + for (Throwable t = ex; t != null; t = t.getCause()) { + if (isDataTypeError(t.getMessage()) + || isDataTypeError(t.getClass().getName())) { + super.error(msg, ex, code); + } + } + } + } + } + + private boolean isDataTypeError(String m) { + if (m != null) { + return m.contains("javax.activation.UnsupportedDataTypeException"); + } + return false; + } + } + + public static final class BooleanFilter implements Filter { + + static final BooleanFilter TRUE = new BooleanFilter(true); + static final BooleanFilter FALSE = new BooleanFilter(false); + private final boolean value; + + public BooleanFilter() { + this(false); + } + + private BooleanFilter(boolean v) { + this.value = v; + } + + @SuppressWarnings("override") //JDK-6954234 + public boolean isLoggable(LogRecord r) { + return value; + } + } + + public static final class CountingFilter implements Filter { + + private final Filter result; + int count; + + public CountingFilter() { + this.result = BooleanFilter.TRUE; + } + + public CountingFilter(Filter f) { + this.result = f; + } + + @SuppressWarnings("override") //JDK-6954234 + public boolean isLoggable(LogRecord r) { + ++count; + return result.isLoggable(r); + } + } + + public static final class CountingFormatter extends Formatter { + + int head; + int tail; + int format; + + @Override + public String getHead(Handler h) { + ++head; + return ""; + } + + @Override + public String format(LogRecord record) { + ++format; + return String.valueOf(record.getMessage()); + } + + @Override + public String getTail(Handler h) { + ++tail; + return ""; + } + } + + public static final class HeadFormatter extends Formatter { + + private final String name; + + public HeadFormatter() { + this((String) null); + } + + public HeadFormatter(final String name) { + this.name = name; + } + + @Override + public String getHead(Handler h) { + return name; + } + + @Override + public String format(LogRecord record) { + return ""; + } + } + + public static class InternalErrorManager extends ErrorManager { + + protected final List<Exception> exceptions = new ArrayList<>(); + + @Override + public void error(String msg, Exception ex, int code) { + exceptions.add(ex); + } + } + + public static final class ThrowAuthenticator extends javax.mail.Authenticator { + + @Override + protected PasswordAuthentication getPasswordAuthentication() { + throw new RuntimeException(); + } + } + + public static final class EmptyAuthenticator extends javax.mail.Authenticator { + + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication("", ""); + } + } + + public static final class EmptyFormatter extends Formatter { + + @Override + public String format(LogRecord r) { + return ""; + } + } + + public static final class ThrowSecurityManager extends SecurityManager { + + boolean secure = false; + private final boolean debug; + + public ThrowSecurityManager() { + debug = isSecurityDebug(); + } + + @Override + public void checkPermission(java.security.Permission perm) { + try { //Call super class always for java.security.debug tracing. + super.checkPermission(perm); + checkPermission(perm, new SecurityException(perm.toString())); + } catch (SecurityException se) { + checkPermission(perm, se); + } + } + + @Override + public void checkPermission(java.security.Permission perm, Object context) { + try { //Call super class always for java.security.debug tracing. + super.checkPermission(perm, context); + checkPermission(perm, new SecurityException(perm.toString())); + } catch (SecurityException se) { + checkPermission(perm, se); + } + } + + private void checkPermission(java.security.Permission perm, SecurityException se) { + if (secure && perm instanceof LoggingPermission) { + throw se; + } else { + if (debug) { + securityDebugPrint(se); + } + } + } + } + + public static final class GaeErrorManager extends MessageErrorManager { + + public GaeErrorManager(MailHandler h) { + super(h.getMailProperties()); + } + + @Override + protected void error(MimeMessage message, Throwable t, int code) { + try { + assertFalse(LogManagerProperties.hasLogManager()); + String[] a = message.getHeader("auto-submitted"); + assertTrue(Arrays.toString(a), a == null || a.length == 0); + message.saveChanges(); + } catch (RuntimeException RE) { + dump(RE); + fail(RE.toString()); + } catch (Exception ME) { + dump(ME); + fail(ME.toString()); + } + } + } + + public static final class GaeSecurityManager extends SecurityManager { + + boolean secure = false; + private final boolean debug; + + public GaeSecurityManager() { + debug = isSecurityDebug(); + } + + @Override + public void checkPermission(java.security.Permission perm) { + try { //Call super class always for java.security.debug tracing. + super.checkPermission(perm); + checkPermission(perm, new SecurityException(perm.toString())); + } catch (SecurityException se) { + checkPermission(perm, se); + } + } + + @Override + public void checkPermission(java.security.Permission perm, Object context) { + try { //Call super class always for java.security.debug tracing. + super.checkPermission(perm, context); + checkPermission(perm, new SecurityException(perm.toString())); + } catch (SecurityException se) { + checkPermission(perm, se); + } + } + + private void checkPermission(java.security.Permission perm, SecurityException se) { + if (secure && perm instanceof LoggingPermission) { + final StackTraceElement[] stack = se.getStackTrace(); + if (stack.length == 0) { + Assume.assumeNoException(se); + } + for (StackTraceElement e : stack) { + if (Handler.class.getName().equals(e.getClassName())) { + throw se; + } + } + } + if (debug) { + securityDebugPrint(se); + } + } + } + + public static class ErrorFormatter extends Formatter { + + @Override + public String format(LogRecord record) { + throw new Error("format"); + } + + @Override + public String getHead(Handler h) { + throw new Error("head"); + } + + @Override + public String getTail(Handler h) { + throw new Error("tail"); + } + } + + public static class ErrorComparator implements Comparator<LogRecord>, Serializable { + + private static final long serialVersionUID = 1L; + + @SuppressWarnings("override") //JDK-6954234 + public int compare(LogRecord r1, LogRecord r2) { + throw new Error(""); + } + } + + public static class ReentranceFilter implements Filter { + + @SuppressWarnings("override") //JDK-6954234 + public boolean isLoggable(LogRecord record) { + if (!getClass().getName().equals(record.getSourceClassName())) { + final Logger logger = Logger.getLogger(record.getLoggerName()); + logger.logp(Level.SEVERE, getClass().getName(), "isLoggable", toString()); + } + return true; + } + } + + public static class ErrorFilter implements Filter { + + @SuppressWarnings("override") //JDK-6954234 + public boolean isLoggable(LogRecord record) { + throw new Error(""); + } + } + + public static class FlipFlopFilter implements Filter { + + volatile boolean value; + + @SuppressWarnings("override") //JDK-6954234 + public boolean isLoggable(LogRecord record) { + return value; + } + } + + public static final class InitAuthenticator extends javax.mail.Authenticator { + + public InitAuthenticator() { + throwPending(); + } + + @Override + protected PasswordAuthentication getPasswordAuthentication() { + throw new NoSuchMethodError(); + } + } + + public final static class InitFilter implements Filter { + + public InitFilter() { + throwPending(); + } + + @SuppressWarnings("override") //JDK-6954234 + public boolean isLoggable(LogRecord record) { + throw new NoSuchMethodError(); + } + } + + public final static class InitFormatter extends Formatter { + + public InitFormatter() { + throwPending(); + } + + @Override + public String format(LogRecord record) { + throw new NoSuchMethodError(); + } + } + + public final static class InitComparator + implements Comparator<LogRecord>, Serializable { + + private static final long serialVersionUID = 1L; + + public InitComparator() { + throwPending(); + } + + @SuppressWarnings("override") //JDK-6954234 + public int compare(LogRecord o1, LogRecord o2) { + throw new NoSuchMethodError(); + } + } + + public final static class InitErrorManager extends ErrorManager { + + public InitErrorManager() { + throwPending(); + } + } + + public final static class InternFilterFormatterComparator extends Formatter + implements Comparator<LogRecord>, Filter, + Serializable { + + private static final long serialVersionUID = -7282673499043066003L; + + @SuppressWarnings("override") //JDK-6954234 + public int compare(LogRecord o1, LogRecord o2) { + throw new UnsupportedOperationException(); + } + + @SuppressWarnings("override") //JDK-6954234 + public boolean isLoggable(LogRecord lr) { + return true; + } + + @Override + public String format(LogRecord lr) { + return ""; + } + + @Override + public boolean equals(Object o) { + return o == null ? false : getClass().equals(o.getClass()); + } + + @Override + public int hashCode() { + return 31 * getClass().hashCode(); + } + } + + public static class InternBadFilter implements Filter { + + @SuppressWarnings("override") //JDK-6954234 + public boolean isLoggable(LogRecord record) { + return true; + } + + @Override + public boolean equals(Object o) { + return o instanceof InternBadFilter; //Not safe. + } + + @Override + public int hashCode() { + return 31 * InternBadFilter.class.hashCode(); //Not safe. + } + } + + public final static class InternBadSubFilter extends InternBadFilter { + + @Override + public boolean isLoggable(LogRecord record) { + return false; + } + } + + public final static class InternFilter implements Filter { + + @SuppressWarnings("override") //JDK-6954234 + public boolean isLoggable(LogRecord lr) { + return true; + } + + @Override + public boolean equals(Object o) { + return o == null ? false : getClass().equals(o.getClass()); + } + + @Override + public int hashCode() { + return 31 * getClass().hashCode(); + } + } + + public final static class InternFilterErrorManager + extends InternalErrorManager implements Filter { + + @SuppressWarnings("override") //JDK-6954234 + public boolean isLoggable(LogRecord lr) { + return true; + } + + @Override + public boolean equals(Object o) { + return o == null ? false : getClass().equals(o.getClass()); + } + + @Override + public int hashCode() { + return 31 * getClass().hashCode(); + } + } + + public final static class InternFilterFormatter + extends Formatter implements Filter { + + @Override + public String format(LogRecord lr) { + return ""; + } + + @SuppressWarnings("override") //JDK-6954234 + public boolean isLoggable(LogRecord lr) { + return true; + } + + @Override + public boolean equals(Object o) { + return o == null ? false : getClass().equals(o.getClass()); + } + + @Override + public int hashCode() { + return 31 * getClass().hashCode(); + } + } + + public final static class InternFormatter extends Formatter { + + @Override + public boolean equals(Object o) { + return o == null ? false : getClass().equals(o.getClass()); + } + + @Override + public int hashCode() { + return 31 * getClass().hashCode(); + } + + @Override + public String format(LogRecord lr) { + return ""; + } + } + + public static final class StaticInitReAuthenticator extends javax.mail.Authenticator { + + static { + throwPending(); + } + + @Override + protected PasswordAuthentication getPasswordAuthentication() { + throw new NoSuchMethodError(); + } + } + + public final static class StaticInitReFilter implements Filter { + + static { + throwPending(); + } + + @SuppressWarnings("override") //JDK-6954234 + public boolean isLoggable(LogRecord record) { + throw new NoSuchMethodError(); + } + } + + public final static class StaticInitReAttachFilter implements Filter { + + static { + throwPending(); + } + + @SuppressWarnings("override") //JDK-6954234 + public boolean isLoggable(LogRecord record) { + throw new NoSuchMethodError(); + } + } + + public final static class StaticInitReFormatter extends Formatter { + + static { + throwPending(); + } + + @Override + public String format(LogRecord record) { + throw new NoSuchMethodError(); + } + } + + public final static class StaticInitReSubjectFormatter extends Formatter { + + static { + throwPending(); + } + + @Override + public String format(LogRecord record) { + throw new NoSuchMethodError(); + } + } + + public final static class StaticInitReAttachFormatter extends Formatter { + + static { + throwPending(); + } + + @Override + public String format(LogRecord record) { + throw new NoSuchMethodError(); + } + } + + public final static class StaticInitReNameFormatter extends Formatter { + + static { + throwPending(); + } + + @Override + public String format(LogRecord record) { + throw new NoSuchMethodError(); + } + } + + public final static class StaticInitReComparator + implements Comparator<LogRecord>, Serializable { + + private static final long serialVersionUID = 1L; + + static { + throwPending(); + } + + @SuppressWarnings("override") //JDK-6954234 + public int compare(LogRecord o1, LogRecord o2) { + throw new NoSuchMethodError(); + } + } + + public final static class StaticInitReErrorManager extends ErrorManager { + + static { + throwPending(); + } + } + + public static final class StaticInitErAuthenticator extends javax.mail.Authenticator { + + static { + throwPending(); + } + + @Override + protected PasswordAuthentication getPasswordAuthentication() { + throw new NoSuchMethodError(); + } + } + + public final static class StaticInitErFilter implements Filter { + + static { + throwPending(); + } + + @SuppressWarnings("override") //JDK-6954234 + public boolean isLoggable(LogRecord record) { + throw new NoSuchMethodError(); + } + } + + public final static class StaticInitErAttachFilter implements Filter { + + static { + throwPending(); + } + + @SuppressWarnings("override") //JDK-6954234 + public boolean isLoggable(LogRecord record) { + throw new NoSuchMethodError(); + } + } + + public final static class StaticInitErFormatter extends Formatter { + + static { + throwPending(); + } + + @Override + public String format(LogRecord record) { + throw new NoSuchMethodError(); + } + } + + public final static class StaticInitErSubjectFormatter extends Formatter { + + static { + throwPending(); + } + + @Override + public String format(LogRecord record) { + throw new NoSuchMethodError(); + } + } + + public final static class StaticInitErAttachFormatter extends Formatter { + + static { + throwPending(); + } + + @Override + public String format(LogRecord record) { + throw new NoSuchMethodError(); + } + } + + public final static class StaticInitErNameFormatter extends Formatter { + + static { + throwPending(); + } + + @Override + public String format(LogRecord record) { + throw new NoSuchMethodError(); + } + } + + public final static class StaticInitErComparator + implements Comparator<LogRecord>, Serializable { + + private static final long serialVersionUID = 1L; + + static { + throwPending(); + } + + @SuppressWarnings("override") //JDK-6954234 + public int compare(LogRecord o1, LogRecord o2) { + throw new NoSuchMethodError(); + } + } + + public final static class StaticInitErErrorManager extends ErrorManager { + + static { + throwPending(); + } + } + + private final static class LevelCheckingFormatter extends Formatter { + + private final Level expect; + + public LevelCheckingFormatter(final Level expect) { + this.expect = expect; + } + + @Override + public String format(LogRecord lr) { + return ""; + } + + @Override + public String getHead(Handler h) { + assertEquals(expect, h.getLevel()); + return ""; + } + + @Override + public String getTail(Handler h) { + return getHead(h); + } + } + + private static class PrintThrowsRuntimeException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + PrintThrowsRuntimeException(Throwable t) { + super(t); + } + + @Override + public void printStackTrace(PrintWriter s) { + throw this; + } + + @Override + public void printStackTrace(PrintStream s) { + throw this; + } + + @Override + public void printStackTrace() { + throw this; + } + + @SuppressWarnings("CallToPrintStackTrace") + public void dump() { + Throwable t = new Throwable(getClass().getName(), getCause()); + t.setStackTrace(getStackTrace()); + t.printStackTrace(); + } + } + + private static class CountingUncaughtExceptionHandler + implements Thread.UncaughtExceptionHandler { + + int count; + + @SuppressWarnings("override") //JDK-6954234 + public void uncaughtException(Thread t, Throwable e) { + count++; + } + } + + private static class LinkageErrorStream extends PrintStream { + + private final StackTraceElement[] stack; + + LinkageErrorStream() throws IOException { + this((StackTraceElement[]) null); + } + + LinkageErrorStream(final StackTraceElement[] stack) throws IOException { + super(new ByteArrayOutputStream(), false, "UTF-8"); + if (stack != null) { + this.stack = stack.clone(); + } else { + this.stack = null; + } + } + + @Override + public void println(String x) { + setError(); + LinkageError le = new LinkageError(x); + if (stack != null) { + le.setStackTrace(stack); + } + throw le; + } + } + + private static class RuntimeErrorStream extends PrintStream { + + private final StackTraceElement[] stack; + + RuntimeErrorStream() throws IOException { + this((StackTraceElement[]) null); + } + + RuntimeErrorStream(final StackTraceElement[] stack) throws IOException { + super(new ByteArrayOutputStream(), false, "UTF-8"); + if (stack != null) { + this.stack = stack.clone(); + } else { + this.stack = null; + } + } + + @Override + public void println(String x) { + setError(); + PrintThrowsRuntimeException re + = new PrintThrowsRuntimeException(new Throwable()); + if (stack != null) { + re.setStackTrace(stack); + } + throw re; + } + } + + private final static class LocaleFilter implements Filter { + + private final Locale locale; + private final boolean allow; + + LocaleFilter(final Locale l, final boolean allow) { + if (l == null) { + throw new NullPointerException(); + } + this.locale = l; + this.allow = allow; + } + + @SuppressWarnings("override") //JDK-6954234 + public boolean isLoggable(LogRecord record) { + final ResourceBundle rb = record.getResourceBundle(); + return rb == null ? allow : locale.equals(rb.getLocale()); + } + } + + public static class MailDebugErrorManager extends ErrorManager { + + @Override + public void error(String msg, Exception ex, int code) { + try { + Session session = Session.getInstance(createInitProperties("")); + session.setDebug(true); + Message m = new MimeMessage(session); + m.setFrom(); + m.setRecipient(Message.RecipientType.TO, m.getFrom()[0]); + m.setText(MailDebugErrorManager.class.getName()); + Transport.send(m); + } catch (Exception e) { + super.error(msg, e, code); + } + } + } + + public final static class MailHandlerExt extends MailHandler { + + public MailHandlerExt() { + super(); + } + + public MailHandlerExt(Properties props) { + super(props); + } + + public MailHandlerExt(int capacity) { + super(capacity); + } + } + + public final static class MailHandlerOverride extends MailHandler { + + public MailHandlerOverride() { + super(); + } + + public MailHandlerOverride(Properties props) { + super(props); + } + + public MailHandlerOverride(int capacity) { + super(capacity); + } + + @Override + public boolean isLoggable(LogRecord record) { + int levelValue = getLevel().intValue(); + if (record.getLevel().intValue() < levelValue + || levelValue == Level.OFF.intValue()) { + return false; + } + + Filter body = getFilter(); + if (body == null || body.isLoggable(record)) { + return true; + } + + final Filter[] filters = this.getAttachmentFilters(); + for (int i = 0; i < filters.length; ++i) { + final Filter f = filters[i]; + if (f == null || f.isLoggable(record)) { + return true; + } + } + return false; + } + } + + private final static class ClassLoaderSecurityManager extends SecurityManager { + + volatile boolean secure = false; + private final boolean debug; + + public ClassLoaderSecurityManager() { + debug = isSecurityDebug(); + } + + @Override + public void checkPermission(java.security.Permission perm) { + try { //Call super class always for java.security.debug tracing. + super.checkPermission(perm); + checkPermission(perm, new SecurityException(perm.toString())); + } catch (SecurityException se) { + checkPermission(perm, se); + } + } + + @Override + public void checkPermission(java.security.Permission perm, Object context) { + try { //Call super class always for java.security.debug tracing. + super.checkPermission(perm, context); + checkPermission(perm, new SecurityException(perm.toString())); + } catch (SecurityException se) { + checkPermission(perm, se); + } + } + + @Override + public void checkRead(String file, Object context) { + } + + @Override + public void checkRead(String file) { + } + + private void checkPermission(java.security.Permission perm, SecurityException se) { + //Check for set and get context class loader. + String name = perm.getName(); + if (secure && name.contains("ContextClassLoader")) { + throw se; + } else { + if (debug) { + securityDebugPrint(se); + } + } + } + } + + public final static class ClassLoaderErrorManager extends InternalErrorManager { + + private final ClassLoader expect; + + public ClassLoaderErrorManager() { + this(LOADER.get()); + } + + public ClassLoaderErrorManager(final ClassLoader expect) { + this.expect = expect; + } + + @Override + public void error(String msg, Exception ex, int code) { + checkContextClassLoader(expect); + super.error(msg, ex, code); + } + + @SuppressWarnings("EqualsWhichDoesntCheckParameterClass") + @Override + public boolean equals(Object o) { + checkContextClassLoader(expect); + return super.equals(o); + } + + @Override + public int hashCode() { + checkContextClassLoader(expect); + return super.hashCode(); + } + + @Override + public String toString() { + checkContextClassLoader(expect); + return super.toString(); + } + } + + public static final class ClassLoaderAuthenticator + extends javax.mail.Authenticator { + + private final ClassLoader expect; + + public ClassLoaderAuthenticator() { + this(LOADER.get()); + } + + ClassLoaderAuthenticator(ClassLoader loader) { + this.expect = loader; + } + + @SuppressWarnings("override") + protected PasswordAuthentication getPasswordAuthentication() { + checkContextClassLoader(expect); + for (StackTraceElement se : new Throwable().getStackTrace()) { + if ("javax.mail.Transport".equals(se.getClassName()) + && "send".equals(se.getMethodName())) { + return null; + } + } + throw new AssertionError("Not calling Transport.send"); + } + } + + public final static class ClassLoaderComparator + implements Comparator<LogRecord>, Serializable { + + private static final long serialVersionUID = -1L; + + private final ClassLoader expect; + + public ClassLoaderComparator() { + this(LOADER.get()); + } + + ClassLoaderComparator(final ClassLoader expect) { + this.expect = expect; + } + + @SuppressWarnings("override") //JDK-6954234 + public int compare(LogRecord o1, LogRecord o2) { + checkContextClassLoader(expect); + return new SequenceComparator().compare(o1, o2); + } + + @SuppressWarnings({"EqualsWhichDoesntCheckParameterClass", "override"}) + public boolean equals(Object o) { + checkContextClassLoader(expect); + return super.equals(o); + } + + @SuppressWarnings("override") + public int hashCode() { + checkContextClassLoader(expect); + return super.hashCode(); + } + + @SuppressWarnings("override") + public String toString() { + checkContextClassLoader(expect); + return super.toString(); + } + } + + public final static class ClassLoaderFilterFormatter + extends Formatter implements Filter { + + private final ClassLoader expect; + private final String format; + + public ClassLoaderFilterFormatter() { + this(LOADER.get()); + } + + ClassLoaderFilterFormatter(final ClassLoader expect) { + this.expect = expect; + this.format = ""; + } + + ClassLoaderFilterFormatter(final ClassLoader expect, String format) { + this.expect = expect; + this.format = format; + } + + @Override + public String getHead(Handler h) { + checkContextClassLoader(expect); + return ""; + } + + @Override + public String getTail(Handler h) { + checkContextClassLoader(expect); + return ""; + } + + @Override + public String format(final LogRecord lr) { + checkContextClassLoader(expect); + return format; + } + + @SuppressWarnings("override") //JDK-6954234 + public boolean isLoggable(LogRecord record) { + checkContextClassLoader(expect); + return true; + } + + @SuppressWarnings("EqualsWhichDoesntCheckParameterClass") + @Override + public boolean equals(Object o) { + checkContextClassLoader(expect); + return super.equals(o); + } + + @Override + public int hashCode() { + checkContextClassLoader(expect); + return super.hashCode(); + } + + @Override + public String toString() { + checkContextClassLoader(expect); + return super.toString(); + } + } + + private final static class CloseLogRecord extends LogRecord { + + private static final long serialVersionUID = 1L; + private transient volatile Handler target; + + CloseLogRecord(Level level, String msg, final Handler target) { + super(level, msg); + this.target = target; + } + + @Override + public String getSourceMethodName() { + close(); + return super.getSourceMethodName(); + } + + @Override + public String getSourceClassName() { + close(); + return super.getSourceClassName(); + } + + public boolean isClosed() { + return this.target == null; + } + + private void close() { + final Handler h = this.target; + if (h != null) { + h.close(); + this.target = null; + } + } + + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + in.defaultReadObject(); + this.target = null; + } + } +}
diff --git a/mail/src/test/java/com/sun/mail/util/logging/SeverityComparatorTest.java b/mail/src/test/java/com/sun/mail/util/logging/SeverityComparatorTest.java new file mode 100644 index 0000000..2911986 --- /dev/null +++ b/mail/src/test/java/com/sun/mail/util/logging/SeverityComparatorTest.java
@@ -0,0 +1,1373 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2018 Jason Mehrens. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package com.sun.mail.util.logging; + +import java.io.*; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.UndeclaredThrowableException; +import java.nio.channels.ClosedByInterruptException; +import java.nio.channels.FileLockInterruptionException; +import java.util.*; +import java.util.concurrent.ExecutionException; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import static org.junit.Assert.*; +import org.junit.Test; + +/** + * + * @author Jason Mehrens + * @since JavaMail 1.5.2 + */ +public class SeverityComparatorTest extends AbstractLogging { + + @Test + public void testDeclaredClasses() throws Exception { + Class<?>[] declared = SeverityComparator.class.getDeclaredClasses(); + assertEquals(Arrays.toString(declared), 0, declared.length); + } + + @Test + public void testApplyNull() { + SeverityComparator a = new SeverityComparator(); + assertNull(a.apply(null)); + } + + @Test + public void testApplyErrorByNormal() { + SeverityComparator a = new SeverityComparator(); + Throwable next = this.headIeChain(new AssertionError()); + next = new Error(next); + next = new NoClassDefFoundError().initCause(next); + Throwable reduced = a.apply(next); + assertEquals(Error.class, reduced.getClass()); + } + + @Test(timeout = 30000) + public void testApplyEvil() { + SeverityComparator a = new SeverityComparator(); + final int len = 7; + final Class<? extends Throwable> type = Exception.class; + Throwable l = a.apply(createEvilThrowable(type, len)); + assertEquals(type, l.getClass()); + } + + @Test + public void testApplyProxyNormal() { + //UTE->IE->AE = IE + SeverityComparator a = new SeverityComparator(); + Throwable next = this.headIeChain(new AssertionError()); + next = new UndeclaredThrowableException(next); + Throwable reduced = a.apply(next); + assertEquals(InterruptedException.class, reduced.getClass()); + } + + @Test + public void testApplyReflectNormal() { + //ITE->IE->AE = IE + SeverityComparator a = new SeverityComparator(); + Throwable next = this.headIeChain(new AssertionError()); + next = new InvocationTargetException(next); + Throwable reduced = a.apply(next); + assertEquals(InterruptedException.class, reduced.getClass()); + } + + @Test + public void testApplyNormalByNormal() { + //EE->IIOE->E->IE->AE = IE + SeverityComparator a = new SeverityComparator(); + Throwable next = this.headIeChain(new AssertionError()); + next = new Error(next); + next = this.headIioeChain(next); + next = new ExecutionException(next); + Throwable reduced = a.apply(next); + assertEquals(InterruptedException.class, reduced.getClass()); + } + + @Test + public void testApplyFindsRootCause() { + /** + * Lots of frameworks wrap exceptions with runtime exceptions. The root + * cause is always more important. + */ + SeverityComparator a = new SeverityComparator(); + final Throwable root = new Exception(); + Throwable next = root; + for (int i = 0; i < 7; i++) { + next = new RuntimeException(next); + } + Throwable reduced = a.apply(next); + assertEquals(root, reduced); + } + + @Test + public void testCompareThrownDoesNotApply() { + SeverityComparator a = new SeverityComparator(); + Throwable tc1 = new Error(new Exception()); + Throwable tc2 = new Exception(new Exception()); + assertTrue(a.compareThrowable(tc1, tc2) > 0); + assertTrue(a.compareThrowable(tc2, tc1) < 0); + + tc1 = new RuntimeException(tc1); + assertTrue(a.compareThrowable(tc1, tc2) > 0); + assertTrue(a.compareThrowable(tc2, tc1) < 0); + + } + + @Test + public void testCompareThrownNull() { + SeverityComparator a = new SeverityComparator(); + assertEquals(0, a.compareThrowable((Throwable) null, (Throwable) null)); + assertTrue(a.compareThrowable(new Throwable(), (Throwable) null) > 0); + assertTrue(a.compareThrowable((Throwable) null, new Throwable()) < 0); + } + + @Test + public void testApplyThenCompareNull() { + SeverityComparator a = new SeverityComparator(); + assertEquals(0, a.applyThenCompare((Throwable) null, (Throwable) null)); + } + + @Test(timeout = 30000) + public void testApplyThenCompareThrownEvilError() { + testApplyThenCompareThrownEvil(Error.class); + } + + @Test(timeout = 30000) + public void testApplyThenCompareThrownEvilRuntimeException() { + testApplyThenCompareThrownEvil(RuntimeException.class); + } + + @Test(timeout = 30000) + public void testApplyThenCompareThrownEvilException() { + testApplyThenCompareThrownEvil(Exception.class); + } + + private void testApplyThenCompareThrownEvil(Class<? extends Throwable> t) { + SeverityComparator a = new SeverityComparator(); + final int len = 7; + Throwable l = createEvilThrowable(t, len); + Throwable r = createEvilThrowable(t, len); + assertEquals(0, a.applyThenCompare(l, r)); + } + + @Test(timeout = 30000) + public void testCompareThrownEvilError() { + testCompareThrownEvil(Error.class); + } + + @Test(timeout = 30000) + public void testCompareThrownEvilRuntimeException() { + testCompareThrownEvil(RuntimeException.class); + } + + @Test(timeout = 30000) + public void testCompareThrownEvilException() { + testCompareThrownEvil(Exception.class); + } + + private void testCompareThrownEvil(Class<? extends Throwable> t) { + SeverityComparator a = new SeverityComparator(); + final int len = 7; + Throwable l = createEvilThrowable(t, len); + Throwable r = createEvilThrowable(t, len); + assertEquals(0, a.compareThrowable(l, r)); + } + + private Throwable createEvilThrowable(Class<? extends Throwable> t, int len) { + Throwable tail = create(t, null); + Throwable head = tail; + for (int i = 1; i < len; i++) { + head = create(t, head); + } + tail.initCause(head); //Pure evil. + return head; + } + + @Test + public void testGetInstance() { + SeverityComparator a = new SeverityComparator(); + assertEquals(a, SeverityComparator.getInstance()); + assertEquals(a.getClass(), SeverityComparator.getInstance().getClass()); + assertEquals(a.hashCode(), SeverityComparator.getInstance().hashCode()); + } + + @Test + public void testIsNormalError() { + testIsNotNormal(Error.class, headIeChain(null)); + } + + @Test + public void testIsNormalRuntimeException() { + testIsNotNormal(RuntimeException.class, headIeChain(null)); + } + + @Test + public void testIsNormalException() { + testIsNotNormal(Exception.class, headIeChain(null)); + } + + private void testIsNotNormal(Class<? extends Throwable> t, Throwable root) { + SeverityComparator a = new SeverityComparator(); + assertFalse(a.isNormal(create(t, null))); + + Throwable next = root; + for (int i = 0; i < 5; i++) { + next = create(t, next); + assertFalse(a.isNormal(next)); + } + } + + @Test + public void testIsNormal() { + SeverityComparator a = new SeverityComparator(); + assertTrue(a.isNormal(this.headIeChain(null))); + assertTrue(a.isNormal(this.headIioeChain(null))); + assertTrue(a.isNormal(this.headCbieChain(null))); + assertTrue(a.isNormal(this.headFlieChain(null))); + assertTrue(a.isNormal(this.headSubIeChain(null))); + assertTrue(a.isNormal(this.headSubIioeChain(null))); + assertTrue(a.isNormal(this.headSubTdChain(null))); + assertTrue(a.isNormal(this.headSubTdChain(null))); + assertTrue(a.isNormal(this.headWidChain(null))); + assertTrue(a.isNormal(this.headWitChain(null))); + } + + @Test + public void testIsNormalEvil() { + SeverityComparator a = new SeverityComparator(); + assertFalse(a.isNormal(createEvilThrowable(Exception.class, 7))); + } + + @Test + public void testIsNormalDoesNotUseApply() { + SeverityComparator a = new SeverityComparator(); + Throwable next = this.headIeChain(new AssertionError()); + next = new Error(next); + next = this.headIioeChain(next); + next = new ExecutionException(next); + assertFalse(a.isNormal(next)); + } + + @Test + public void testIsNormalNull() { + SeverityComparator a = new SeverityComparator(); + assertFalse(a.isNormal((Throwable) null)); + } + + @Test(expected = NullPointerException.class) + public void testCompareNullAndNull() { + SeverityComparator a = new SeverityComparator(); + a.compare((LogRecord) null, (LogRecord) null); + } + + @Test(expected = NullPointerException.class) + public void testCompareNotNullAndNull() { + SeverityComparator a = new SeverityComparator(); + LogRecord r1 = new LogRecord(Level.INFO, Level.INFO.toString()); + a.compare(r1, (LogRecord) null); + } + + @Test(expected = NullPointerException.class) + public void testCompareNullAndNotNull() { + SeverityComparator a = new SeverityComparator(); + LogRecord r1 = new LogRecord(Level.INFO, Level.INFO.toString()); + a.compare((LogRecord) null, r1); + } + + @Test + public void testLeadingErrorAndInterrupted() { + LogRecord r1 = new LogRecord(Level.INFO, Level.INFO.toString()); + LogRecord r2 = new LogRecord(Level.INFO, Level.INFO.toString()); + swapSeq(r1, r2); + assertEquals(r1.getLevel(), r2.getLevel()); + assertTrue(r1.getSequenceNumber() > r2.getSequenceNumber()); + + r1.setThrown(headIeChain(new Error())); + r2.setThrown(new Error(headIeChain(null))); + assertRecordLessThan(r1, r2); + + r1.setThrown(headIeChain(new RuntimeException())); + r2.setThrown(new Error(headIeChain(null))); + assertRecordLessThan(r1, r2); + + r1.setThrown(headIeChain(new Exception())); + r2.setThrown(new Error(headIeChain(null))); + assertRecordLessThan(r1, r2); + } + + @Test + public void testByLevel() { + LogRecord r1 = new LogRecord(Level.INFO, Level.INFO.toString()); + LogRecord r2 = new LogRecord(Level.WARNING, Level.WARNING.toString()); + assertTrue(r1.getSequenceNumber() < r2.getSequenceNumber()); + + //Level test with null thrown. + assertRecordLessThan(r1, r2); + + //Ensure Error doesn't change primary order. + r1.setThrown(new Error()); + r2.setThrown(null); + assertRecordLessThan(r1, r2); + + assertEquals(Error.class, r1.getThrown().getClass()); + r2.setThrown(new Error()); + assertRecordLessThan(r1, r2); + + assertEquals(Error.class, r1.getThrown().getClass()); + r2.setThrown(new RuntimeException()); + assertRecordLessThan(r1, r2); + + assertEquals(Error.class, r1.getThrown().getClass()); + r2.setThrown(new Exception()); + assertRecordLessThan(r1, r2); + + assertEquals(Error.class, r1.getThrown().getClass()); + r2.setThrown(new InterruptedException()); + assertRecordLessThan(r1, r2); + + //Ensure RuntimeException doesn't change primary order. + r1.setThrown(new RuntimeException()); + r2.setThrown(null); + assertRecordLessThan(r1, r2); + + assertEquals(RuntimeException.class, r1.getThrown().getClass()); + r2.setThrown(new Error()); + assertRecordLessThan(r1, r2); + + assertEquals(RuntimeException.class, r1.getThrown().getClass()); + r2.setThrown(new RuntimeException()); + assertRecordLessThan(r1, r2); + + assertEquals(RuntimeException.class, r1.getThrown().getClass()); + r2.setThrown(new Exception()); + assertRecordLessThan(r1, r2); + + assertEquals(RuntimeException.class, r1.getThrown().getClass()); + r2.setThrown(new InterruptedException()); + assertRecordLessThan(r1, r2); + + //Ensure Exception doesn't change primary order. + r1.setThrown(new Exception()); + r2.setThrown(null); + assertRecordLessThan(r1, r2); + + assertEquals(Exception.class, r1.getThrown().getClass()); + r2.setThrown(new Error()); + assertRecordLessThan(r1, r2); + + assertEquals(Exception.class, r1.getThrown().getClass()); + r2.setThrown(new RuntimeException()); + assertRecordLessThan(r1, r2); + + assertEquals(Exception.class, r1.getThrown().getClass()); + r2.setThrown(new Exception()); + assertRecordLessThan(r1, r2); + + assertEquals(Exception.class, r1.getThrown().getClass()); + r2.setThrown(new InterruptedException()); + assertRecordLessThan(r1, r2); + + //Ensure thrown and sequence doesn't change primary order. + swapSeq(r1, r2); + assertTrue(r1.getSequenceNumber() > r2.getSequenceNumber()); + assertRecordLessThan(r1, r2); + + r1.setThrown(null); + r2.setThrown(null); + assertTrue(r1.getSequenceNumber() > r2.getSequenceNumber()); + assertRecordLessThan(r1, r2); + } + + @Test + public void testByNullAndFrameworkInterrupted() { + LogRecord r1 = new LogRecord(Level.INFO, Level.INFO.toString()); + LogRecord r2 = new LogRecord(Level.INFO, Level.INFO.toString()); + assertEquals(r1.getLevel(), r2.getLevel()); + + //Ensure Interrupted is less than null. + r1.setThrown(this.headWidChain(null)); + assertNull(r2.getThrown()); + assertRecordLessThan(r1, r2); + + r1.setThrown(this.headWidChain(new Error())); + assertNull(r2.getThrown()); + assertRecordLessThan(r1, r2); + + r1.setThrown(this.headWidChain(new RuntimeException())); + assertNull(r2.getThrown()); + assertRecordLessThan(r1, r2); + + r1.setThrown(this.headWidChain(new Exception())); + assertNull(r2.getThrown()); + assertRecordLessThan(r1, r2); + } + + @Test + public void testByNullAndInterrupted() { + LogRecord r1 = new LogRecord(Level.INFO, Level.INFO.toString()); + LogRecord r2 = new LogRecord(Level.INFO, Level.INFO.toString()); + assertEquals(r1.getLevel(), r2.getLevel()); + + //Ensure Interrupted is less than null. + r1.setThrown(headIeChain(null)); + assertNull(r2.getThrown()); + assertRecordLessThan(r1, r2); + + r1.setThrown(headIeChain(new Error())); + assertNull(r2.getThrown()); + assertRecordLessThan(r1, r2); + + r1.setThrown(headIeChain(new RuntimeException())); + assertNull(r2.getThrown()); + assertRecordLessThan(r1, r2); + + r1.setThrown(headIeChain(new Exception())); + assertNull(r2.getThrown()); + assertRecordLessThan(r1, r2); + } + + @Test + public void testByNullAndInterruptedSub() { + LogRecord r1 = new LogRecord(Level.INFO, Level.INFO.toString()); + LogRecord r2 = new LogRecord(Level.INFO, Level.INFO.toString()); + assertEquals(r1.getLevel(), r2.getLevel()); + //Ensure subclass of IE is less than null. + r1.setThrown(headSubIeChain(null)); + assertNull(r2.getThrown()); + assertRecordLessThan(r1, r2); + + r1.setThrown(headSubIeChain(new Error())); + assertNull(r2.getThrown()); + assertRecordLessThan(r1, r2); + + r1.setThrown(headSubIeChain(new RuntimeException())); + assertNull(r2.getThrown()); + assertRecordLessThan(r1, r2); + + r1.setThrown(headSubIeChain(new Exception())); + assertNull(r2.getThrown()); + assertRecordLessThan(r1, r2); + } + + @Test + public void testByNullAndInterruptedIo() { + LogRecord r1 = new LogRecord(Level.INFO, Level.INFO.toString()); + LogRecord r2 = new LogRecord(Level.INFO, Level.INFO.toString()); + assertEquals(r1.getLevel(), r2.getLevel()); + + //Esure subclass of IOE is less than null. + r1.setThrown(headIioeChain(null)); + assertNull(r2.getThrown()); + assertRecordLessThan(r1, r2); + + r1.setThrown(headIioeChain(new Error())); + assertNull(r2.getThrown()); + assertRecordLessThan(r1, r2); + + r1.setThrown(headIioeChain(new RuntimeException())); + assertNull(r2.getThrown()); + assertRecordLessThan(r1, r2); + + r1.setThrown(headIioeChain(new Exception())); + assertNull(r2.getThrown()); + assertRecordLessThan(r1, r2); + } + + @Test + public void testByNullAndInterruptedIoSub() { + LogRecord r1 = new LogRecord(Level.INFO, Level.INFO.toString()); + LogRecord r2 = new LogRecord(Level.INFO, Level.INFO.toString()); + assertEquals(r1.getLevel(), r2.getLevel()); + + //Esure subclass of IOE is less than null. + r1.setThrown(headSubIioeChain(null)); + assertNull(r2.getThrown()); + assertRecordLessThan(r1, r2); + + r1.setThrown(headSubIioeChain(new Error())); + assertNull(r2.getThrown()); + assertRecordLessThan(r1, r2); + + r1.setThrown(headSubIioeChain(new RuntimeException())); + assertNull(r2.getThrown()); + assertRecordLessThan(r1, r2); + + r1.setThrown(headSubIioeChain(new Exception())); + assertNull(r2.getThrown()); + assertRecordLessThan(r1, r2); + } + + @Test + public void testByNullAndThreadDeath() { + LogRecord r1 = new LogRecord(Level.INFO, Level.INFO.toString()); + LogRecord r2 = new LogRecord(Level.INFO, Level.INFO.toString()); + assertEquals(r1.getLevel(), r2.getLevel()); + + //Ensure ThreadDeath is less than null. + r1.setThrown(headTdChain(null)); + assertNull(r2.getThrown()); + assertRecordLessThan(r1, r2); + + r1.setThrown(headTdChain(new Error())); + assertNull(r2.getThrown()); + assertRecordLessThan(r1, r2); + + r1.setThrown(headTdChain(new RuntimeException())); + assertNull(r2.getThrown()); + assertRecordLessThan(r1, r2); + + r1.setThrown(headTdChain(new Exception())); + assertNull(r2.getThrown()); + assertRecordLessThan(r1, r2); + } + + @Test + public void testByNullAndThreadDeathSub() { + LogRecord r1 = new LogRecord(Level.INFO, Level.INFO.toString()); + LogRecord r2 = new LogRecord(Level.INFO, Level.INFO.toString()); + assertEquals(r1.getLevel(), r2.getLevel()); + + //Ensure subclass ThreadDeath is less than null. + r1.setThrown(headSubTdChain(null)); + assertNull(r2.getThrown()); + assertRecordLessThan(r1, r2); + + r1.setThrown(headSubTdChain(new Error())); + assertNull(r2.getThrown()); + assertRecordLessThan(r1, r2); + + r1.setThrown(headSubTdChain(new RuntimeException())); + assertNull(r2.getThrown()); + assertRecordLessThan(r1, r2); + + r1.setThrown(headSubTdChain(new Exception())); + assertNull(r2.getThrown()); + assertRecordLessThan(r1, r2); + } + + @Test + public void testByNullAndThrown() { + LogRecord r1 = new LogRecord(Level.INFO, Level.INFO.toString()); + LogRecord r2 = new LogRecord(Level.INFO, Level.INFO.toString()); + r1.setSequenceNumber(r2.getSequenceNumber()); + setEpochMilli(r2, System.currentTimeMillis()); //Truncate nanos. + setEpochMilli(r1, r2.getMillis()); + + assertEquals(r1.getLevel(), r2.getLevel()); + assertEquals(r1.getSequenceNumber(), r2.getSequenceNumber()); + assertEquals(r1.getMillis(), r2.getMillis()); + + //Ensure null is less than Error. + assertNull(r1.getThrown()); + r2.setThrown(new Error()); + assertRecordLessThan(r1, r2); + + //Ensure null is less than RuntimeException. + assertNull(r1.getThrown()); + r2.setThrown(new RuntimeException()); + assertRecordLessThan(r1, r2); + + //Ensure null is less than Exception. + assertNull(r1.getThrown()); + r2.setThrown(new Exception()); + assertRecordLessThan(r1, r2); + } + + @Test + public void testByNullAndNullSeq() { + LogRecord r1 = new LogRecord(Level.INFO, Level.INFO.toString()); + LogRecord r2 = new LogRecord(Level.INFO, Level.INFO.toString()); + + assertTrue(r1.getSequenceNumber() < r2.getSequenceNumber()); + assertNull(r1.getThrown()); + assertNull(r2.getThrown()); + assertRecordLessThan(r1, r2); + } + + @Test + public void testByThrownAndThrownSeq() { + LogRecord r1 = new LogRecord(Level.INFO, Level.INFO.toString()); + LogRecord r2 = new LogRecord(Level.INFO, Level.INFO.toString()); + + assertTrue(r1.getSequenceNumber() < r2.getSequenceNumber()); + assertTrue(r1.getSequenceNumber() < r2.getSequenceNumber()); + r1.setThrown(new Error()); + r2.setThrown(new Error()); + assertRecordLessThan(r1, r2); + + assertTrue(r1.getSequenceNumber() < r2.getSequenceNumber()); + r1.setThrown(new RuntimeException()); + r2.setThrown(new RuntimeException()); + assertRecordLessThan(r1, r2); + + assertTrue(r1.getSequenceNumber() < r2.getSequenceNumber()); + r1.setThrown(new Exception()); + r2.setThrown(new Exception()); + assertRecordLessThan(r1, r2); + } + + @Test + public void testByThrownLenSeq() { + final int MAX_RUNS = 10; + final int MAX_LEN = 20; + + for (int r = MAX_RUNS; r > 0; r--) { + for (int i = 0; i < MAX_LEN; i++) { + testByThrownLenSeq(Error.class, i, r); + } + + for (int i = 0; i < MAX_LEN; i++) { + testByThrownLenSeq(RuntimeException.class, i, r); + } + + for (int i = 0; i < MAX_LEN; i++) { + testByThrownLenSeq(Exception.class, i, r); + } + } + } + + @Test + public void testByNormalLenSeq() { + final int MAX_RUNS = 10; + final int MAX_LEN = 20; + + for (int r = MAX_RUNS; r > 0; r--) { + //Null is higher than normal so we have to start at one. + for (int i = 1; i <= MAX_LEN; i++) { + testByThrownLenSeq(InterruptedException.class, i, r); + } + + for (int i = 1; i <= MAX_LEN; i++) { + testByThrownLenSeq(InterruptedIOException.class, i, r); + } + + for (int i = 1; i <= MAX_LEN; i++) { + testByThrownLenSeq(ThreadDeath.class, i, r); + } + } + } + + private void testByThrownLenSeq( + Class<? extends Throwable> t, int c1, int c2) { + LogRecord r1 = new LogRecord(Level.INFO, Level.INFO.toString()); + LogRecord r2 = new LogRecord(Level.INFO, Level.INFO.toString()); + + assertTrue(r1.getSequenceNumber() < r2.getSequenceNumber()); + + setThrown(r1, t, c1); + setThrown(r2, t, c2); + assertRecordLessThan(r1, r2); + } + + private Throwable create(Class<? extends Throwable> type, Throwable cause) { + try { + if (cause == null) { + return type.getConstructor().newInstance(); + } + + try { + return type.getConstructor(Throwable.class).newInstance(cause); + } catch (NoSuchMethodException tryInitCause) { + Throwable next = type.getConstructor().newInstance(); + return next.initCause(cause); + } + } catch (InstantiationException ex) { + throw new AssertionError(ex); + } catch (IllegalAccessException ex) { + throw new AssertionError(ex); + } catch (IllegalArgumentException ex) { + throw new AssertionError(ex); + } catch (InvocationTargetException ex) { + throw new AssertionError(ex); + } catch (NoSuchMethodException ex) { + throw new AssertionError(ex); + } + } + + private void setThrown(LogRecord r, Class<? extends Throwable> t, int d) { + for (int i = 0; i < d; i++) { + r.setThrown(create(t, r.getThrown())); + } + } + + @Test + public void testByThrownAndInterrupted() { + LogRecord r1 = new LogRecord(Level.INFO, Level.INFO.toString()); + LogRecord r2 = new LogRecord(Level.INFO, Level.INFO.toString()); + swapSeq(r1, r2); + + assertTrue(r1.getSequenceNumber() > r2.getSequenceNumber()); + r1.setThrown(headIeChain(null)); + r2.setThrown(new Error()); + assertRecordLessThan(r1, r2); + + assertTrue(r1.getSequenceNumber() > r2.getSequenceNumber()); + r1.setThrown(headIeChain(null)); + r2.setThrown(new RuntimeException()); + assertRecordLessThan(r1, r2); + + assertTrue(r1.getSequenceNumber() > r2.getSequenceNumber()); + r1.setThrown(headIeChain(null)); + r2.setThrown(new Exception()); + assertRecordLessThan(r1, r2); + + assertTrue(r1.getSequenceNumber() > r2.getSequenceNumber()); + r1.setThrown(headIeChain(new Error())); + r2.setThrown(new Error()); + assertRecordLessThan(r1, r2); + + assertTrue(r1.getSequenceNumber() > r2.getSequenceNumber()); + r1.setThrown(headIeChain(new RuntimeException())); + r2.setThrown(new RuntimeException()); + assertRecordLessThan(r1, r2); + + assertTrue(r1.getSequenceNumber() > r2.getSequenceNumber()); + r1.setThrown(headIeChain(new Exception())); + r2.setThrown(new Exception()); + assertRecordLessThan(r1, r2); + } + + @Test + public void testByThrownAndInterruptedSub() { + LogRecord r1 = new LogRecord(Level.INFO, Level.INFO.toString()); + LogRecord r2 = new LogRecord(Level.INFO, Level.INFO.toString()); + swapSeq(r1, r2); + + assertTrue(r1.getSequenceNumber() > r2.getSequenceNumber()); + r1.setThrown(headSubIeChain(null)); + r2.setThrown(new Error()); + assertRecordLessThan(r1, r2); + + assertTrue(r1.getSequenceNumber() > r2.getSequenceNumber()); + r1.setThrown(headSubIeChain(null)); + r2.setThrown(new RuntimeException()); + assertRecordLessThan(r1, r2); + + assertTrue(r1.getSequenceNumber() > r2.getSequenceNumber()); + r1.setThrown(headSubIeChain(null)); + r2.setThrown(new Exception()); + assertRecordLessThan(r1, r2); + + assertTrue(r1.getSequenceNumber() > r2.getSequenceNumber()); + r1.setThrown(headSubIeChain(new Error())); + r2.setThrown(new Error()); + assertRecordLessThan(r1, r2); + + assertTrue(r1.getSequenceNumber() > r2.getSequenceNumber()); + r1.setThrown(headSubIeChain(new RuntimeException())); + r2.setThrown(new RuntimeException()); + assertRecordLessThan(r1, r2); + + assertTrue(r1.getSequenceNumber() > r2.getSequenceNumber()); + r1.setThrown(headSubIeChain(new Exception())); + r2.setThrown(new Exception()); + assertRecordLessThan(r1, r2); + } + + @Test + public void testByThrownAndInterruptedIoSub() { + LogRecord r1 = new LogRecord(Level.INFO, Level.INFO.toString()); + LogRecord r2 = new LogRecord(Level.INFO, Level.INFO.toString()); + swapSeq(r1, r2); + + assertTrue(r1.getSequenceNumber() > r2.getSequenceNumber()); + r1.setThrown(headSubIioeChain(null)); + r2.setThrown(new Error()); + assertRecordLessThan(r1, r2); + + assertTrue(r1.getSequenceNumber() > r2.getSequenceNumber()); + r1.setThrown(headSubIioeChain(null)); + r2.setThrown(new RuntimeException()); + assertRecordLessThan(r1, r2); + + assertTrue(r1.getSequenceNumber() > r2.getSequenceNumber()); + r1.setThrown(headSubIioeChain(null)); + r2.setThrown(new Exception()); + assertRecordLessThan(r1, r2); + } + + @Test + public void testByThrownAndThreadDeath() { + LogRecord r1 = new LogRecord(Level.INFO, Level.INFO.toString()); + LogRecord r2 = new LogRecord(Level.INFO, Level.INFO.toString()); + swapSeq(r1, r2); + + assertTrue(r1.getSequenceNumber() > r2.getSequenceNumber()); + r1.setThrown(headTdChain(null)); + r2.setThrown(new Error()); + assertRecordLessThan(r1, r2); + + assertTrue(r1.getSequenceNumber() > r2.getSequenceNumber()); + r1.setThrown(headTdChain(null)); + r2.setThrown(new RuntimeException()); + assertRecordLessThan(r1, r2); + + assertTrue(r1.getSequenceNumber() > r2.getSequenceNumber()); + r1.setThrown(headTdChain(null)); + r2.setThrown(new Exception()); + assertRecordLessThan(r1, r2); + + assertTrue(r1.getSequenceNumber() > r2.getSequenceNumber()); + r1.setThrown(headTdChain(new Error())); + r2.setThrown(new Error()); + assertRecordLessThan(r1, r2); + + assertTrue(r1.getSequenceNumber() > r2.getSequenceNumber()); + r1.setThrown(headTdChain(new RuntimeException())); + r2.setThrown(new RuntimeException()); + assertRecordLessThan(r1, r2); + + assertTrue(r1.getSequenceNumber() > r2.getSequenceNumber()); + r1.setThrown(headTdChain(new Exception())); + r2.setThrown(new Exception()); + assertRecordLessThan(r1, r2); + } + + @Test + public void testByThrownAndThreadDeathSub() { + LogRecord r1 = new LogRecord(Level.INFO, Level.INFO.toString()); + LogRecord r2 = new LogRecord(Level.INFO, Level.INFO.toString()); + swapSeq(r1, r2); + + assertTrue(r1.getSequenceNumber() > r2.getSequenceNumber()); + r1.setThrown(headSubTdChain(null)); + r2.setThrown(new Error()); + assertRecordLessThan(r1, r2); + + assertTrue(r1.getSequenceNumber() > r2.getSequenceNumber()); + r1.setThrown(headSubTdChain(null)); + r2.setThrown(new RuntimeException()); + assertRecordLessThan(r1, r2); + + assertTrue(r1.getSequenceNumber() > r2.getSequenceNumber()); + r1.setThrown(headSubTdChain(null)); + r2.setThrown(new Exception()); + assertRecordLessThan(r1, r2); + + assertTrue(r1.getSequenceNumber() > r2.getSequenceNumber()); + r1.setThrown(headSubTdChain(new Error())); + r2.setThrown(new Error()); + assertRecordLessThan(r1, r2); + + assertTrue(r1.getSequenceNumber() > r2.getSequenceNumber()); + r1.setThrown(headSubTdChain(new RuntimeException())); + r2.setThrown(new RuntimeException()); + assertRecordLessThan(r1, r2); + + assertTrue(r1.getSequenceNumber() > r2.getSequenceNumber()); + r1.setThrown(headSubTdChain(new Exception())); + r2.setThrown(new Exception()); + assertRecordLessThan(r1, r2); + } + + @Test + public void testOfClassLoadReadFailed() { + /** + * When exceptions triggers errors that is more important than the root + * cause. + */ + Throwable clrf = new NoClassDefFoundError().initCause( + new ClassNotFoundException("", new IOException())); + + Throwable readFailed = new IOException(); + + LogRecord r1 = new LogRecord(Level.INFO, Level.INFO.toString()); + LogRecord r2 = new LogRecord(Level.INFO, Level.INFO.toString()); + + r1.setThrown(readFailed); + r2.setThrown(clrf); + assertRecordLessThan(r1, r2); + + r1.setThrown(clrf); + r2.setThrown(readFailed); + assertRecordLessThan(r2, r1); + } + + @Test + public void testOfClassLoadInterrupted() { + /** + * When exceptions triggers errors that is more important than the root + * cause. + */ + Throwable cli = new NoClassDefFoundError().initCause( + new ClassNotFoundException("", headIioeChain(null))); + + Throwable iioe = headIioeChain(null); + + LogRecord r1 = new LogRecord(Level.INFO, Level.INFO.toString()); + LogRecord r2 = new LogRecord(Level.INFO, Level.INFO.toString()); + + r1.setThrown(iioe); + r2.setThrown(cli); + assertRecordLessThan(r1, r2); + + r1.setThrown(cli); + r2.setThrown(iioe); + assertRecordLessThan(r2, r1); + } + + @Test + public void testOfNpeVsIoe() { + Throwable readFailed = new IOException(); + Throwable npe = new IOException().initCause(new NullPointerException()); + + LogRecord r1 = new LogRecord(Level.INFO, Level.INFO.toString()); + LogRecord r2 = new LogRecord(Level.INFO, Level.INFO.toString()); + + r1.setThrown(readFailed); + r2.setThrown(npe); + assertRecordLessThan(r1, r2); + + r1.setThrown(npe); + r2.setThrown(readFailed); + assertRecordLessThan(r2, r1); + } + + @Test + public void testArraySort() { + testArraySort(new SeverityComparator()); + } + + @Test + public void testReverseArraySort() { + testArraySort(Collections.reverseOrder(new SeverityComparator())); + } + + private void testArraySort(Comparator<LogRecord> sc) { + LogRecord[] r = createRecords(); + LogRecord[] copy = r.clone(); + Arrays.sort(copy, sc); + assertTrue(r.length > 1); + + //Ensure createRecords is not already sorted. + int orderChanged = 0; + for (int i = 0; i < r.length; i++) { + if (sc.compare(r[i], copy[i]) != 0) { + ++orderChanged; + } + } + assertTrue(String.valueOf(orderChanged), orderChanged > r.length / 2); + + //Ensure sorted. + for (int i = 0; i < r.length - 1; i++) { + if (sc.compare(copy[i], copy[i + 1]) > 0) { + fail(toString(copy[i], copy[i + 1])); + } + } + } + + @Test + public void testListSort() { + testListSort(new SeverityComparator()); + } + + @Test + public void testReverseListSort() { + testListSort(Collections.reverseOrder(new SeverityComparator())); + } + + private void testListSort(Comparator<LogRecord> sc) { + List<LogRecord> a = Arrays.asList(createRecords()); + Collections.sort(a, sc); + + List<LogRecord> b = serialClone(a); + Collections.sort(b, sc); + + assertEquals(a.size(), b.size()); + for (int i = 0; i < a.size(); i++) { + LogRecord r1 = a.get(i); + LogRecord r2 = b.get(i); + if (sc.compare(r1, r2) != 0) { + throw new AssertionError(toString(i, r1, r2)); + } + } + } + + @Test + public void testTreeMap() { + testMap(new TreeMap<LogRecord, Boolean>(new SeverityComparator())); + } + + @Test + public void testReverseTreeMap() { + testMap(new TreeMap<LogRecord, Boolean>(Collections + .reverseOrder(new SeverityComparator()))); + } + + private void testMap(SortedMap<LogRecord, Boolean> m) { + assertTrue(m.isEmpty()); + + for (LogRecord r : createRecords()) { + assertNull(m.put(r, Boolean.TRUE)); + } + + SortedMap<LogRecord, Boolean> copy = serialClone(m); + Iterator<LogRecord> keys = copy.keySet().iterator(); + assertTrue(keys.hasNext()); + int i = 0; + for (LogRecord r1 : m.keySet()) { + LogRecord r2 = keys.next(); + if (m.comparator().compare(r1, r2) != 0) { + throw new AssertionError(toString(i, r1, r2)); + } + ++i; + } + + //Don't use containsAll. + assertFalse(m.isEmpty()); + for (LogRecord r : m.keySet()) { + if (!copy.containsKey(r)) { + throw new AssertionError(toString(r)); + } + } + + //Don't use containsAll. + assertFalse(copy.isEmpty()); + for (LogRecord r : copy.keySet()) { + if (!m.containsKey(r)) { + throw new AssertionError(toString(r)); + } + } + } + + @Test + public void testPriorityQueue() { + SeverityComparator sc = new SeverityComparator(); + LogRecord[] rs = createRecords(); + PriorityQueue<LogRecord> q1 + = new PriorityQueue<>(rs.length, sc); + Collections.addAll(q1, rs); + PriorityQueue<LogRecord> q2 = serialClone(q1); + + assertFalse(q1.isEmpty()); + assertFalse(q2.isEmpty()); + + for (int i = 0; i < rs.length; i++) { + LogRecord r1 = q1.poll(); + LogRecord r2 = q2.poll(); + if (sc.compare(r1, r2) != 0) { + throw new AssertionError(toString(i, r1, r2)); + } + } + + assertTrue(q1.isEmpty()); + assertTrue(q2.isEmpty()); + } + + @Test + @SuppressWarnings("ObjectEqualsNull") + public void testEquals() { + final SeverityComparator a = new SeverityComparator(); + final SeverityComparator b = new SeverityComparator(); + assertNotSame(a, b); + + //NPE checks. + assertNotNull(a); + assertFalse(a.equals(null)); + + assertNotNull(b); + assertFalse(b.equals(null)); + + //Reflexive test. + assertTrue(a.equals(a)); + assertTrue(b.equals(b)); + + //Transitive test. + assertTrue(a.equals(b)); + assertTrue(b.equals(a)); + } + + @Test + public void testHashCode() { + final SeverityComparator a = new SeverityComparator(); + final SeverityComparator b = new SeverityComparator(); + assertNotSame(a, b); + + assertTrue(a.equals(b)); + assertTrue(b.equals(a)); + + assertEquals(a.hashCode(), b.hashCode()); + } + + @Test + public void testToString() { + final SeverityComparator a = new SeverityComparator(); + assertNotNull(a.toString()); + assertEquals(a.toString(), a.toString()); + } + + @Test + public void testJavaMailLinkage() throws Exception { + testJavaMailLinkage(SeverityComparator.class); + } + + @Test + public void testLogManagerModifiers() throws Exception { + testLogManagerModifiers(SeverityComparator.class); + } + + @Test + public void testWebappClassLoaderFieldNames() throws Exception { + testWebappClassLoaderFieldNames(SeverityComparator.class); + } + + @Test + public void testSerializable() throws Exception { + final SeverityComparator a = new SeverityComparator(); + final SeverityComparator b = serialClone(a); + + assertTrue(a.equals(b)); + assertTrue(b.equals(a)); + + assertEquals(a.hashCode(), b.hashCode()); + } + + @SuppressWarnings("unchecked") + private <T> T serialClone(T t) { + try { + final ByteArrayOutputStream os = new ByteArrayOutputStream(); + final ObjectOutputStream out = new ObjectOutputStream(os); + try { + out.writeObject(t); + out.flush(); + } finally { + out.close(); + } + + ByteArrayInputStream is = new ByteArrayInputStream(os.toByteArray()); + final ObjectInputStream in = new ObjectInputStream(is); + try { + return (T) in.readObject(); + } finally { + in.close(); + } + } catch (ClassNotFoundException CNFE) { + throw new AssertionError(CNFE); + } catch (IOException ioe) { + throw new AssertionError(ioe); + } + } + + @Test + public void testByMillis() { + testByMillis(new Error()); + testByMillis(new RuntimeException()); + testByMillis(new Exception()); + } + + private void testByMillis(Throwable t) { + LogRecord r1 = new LogRecord(Level.INFO, Level.INFO.toString()); + LogRecord r2 = new LogRecord(Level.INFO, Level.INFO.toString()); + r2.setSequenceNumber(r1.getSequenceNumber()); + setEpochMilli(r1, 10); + setEpochMilli(r2, 20); + + assertEquals(r1.getLevel(), r2.getLevel()); + assertEquals(r1.getThrown(), r2.getThrown()); + assertEquals(r1.getSequenceNumber(), r2.getSequenceNumber()); + assertEquals(r1.getThreadID(), r2.getThreadID()); + assertEquals(r1.getClass(), r2.getClass()); + assertTrue(r1.getMillis() < r2.getMillis()); + assertRecordLessThan(r1, r2); + + r1.setThrown(t); + r2.setThrown(r1.getThrown()); + assertEquals(r1.getLevel(), r2.getLevel()); + assertEquals(r1.getThrown(), r2.getThrown()); + assertEquals(r1.getSequenceNumber(), r2.getSequenceNumber()); + assertEquals(r1.getThreadID(), r2.getThreadID()); + assertEquals(r1.getClass(), r2.getClass()); + assertTrue(r1.getMillis() < r2.getMillis()); + assertRecordLessThan(r1, r2); + } + + private static void assertRecordLessThan(LogRecord r1, LogRecord r2) { + final SeverityComparator a = new SeverityComparator(); + if (a.compare(r1, r2) >= 0) { + throw new AssertionError(toString(r1, r2)); + } + + if (a.compare(r2, r1) <= 0) { + throw new AssertionError(toString(r1, r2)); + } + } + + private static String toString(int i, LogRecord r1, LogRecord r2) { + return "Index: " + i + " " + toString(r1, r2); + } + + private static String toString(LogRecord r1, LogRecord r2) { + return toString(r1) + '|' + toString(r2); + } + + private static String toString(LogRecord r) { + StringBuilder b = new StringBuilder(); + b.append(r.getLevel()).append(", "); + b.append(toString(r.getThrown())); + b.append(' ').append(r.getSequenceNumber()); + return b.toString(); + } + + private static String toString(Throwable chain) { + StringBuilder b = new StringBuilder(); + if (chain != null) { + String sep = ""; + for (Throwable t = chain; t != null; t = t.getCause()) { + b.append(sep).append(t.getClass().getSimpleName()); + sep = ", "; + } + } else { + b.append("null"); + } + return b.toString(); + } + + private static void swapSeq(LogRecord r1, LogRecord r2) { + final long seq = r1.getSequenceNumber(); + r1.setSequenceNumber(r2.getSequenceNumber()); + r2.setSequenceNumber(seq); + assertFalse(r1.getSequenceNumber() == r2.getSequenceNumber()); + } + + private LogRecord[] createRecords() { + LogRecord[] r = new LogRecord[8]; + r[0] = new LogRecord(Level.INFO, "Started {0}, {1}"); + r[0].setParameters(new Object[]{"10", new Object()}); + r[1] = new LogRecord(Level.CONFIG, "99%"); + r[2] = new LogRecord(Level.SEVERE, "1st failure."); + r[2].setThrown(new ClassNotFoundException()); + r[3] = new LogRecord(Level.SEVERE, "2nd failure."); + r[3].setThrown(new ExceptionInInitializerError(new NullPointerException())); + r[4] = new LogRecord(Level.SEVERE, "3rd failure."); + r[4].setThrown(new NoClassDefFoundError()); + r[5] = new LogRecord(Level.WARNING, "Restart required."); + r[6] = new LogRecord(Level.INFO, "Restarting...."); + r[7] = new LogRecord(Level.FINE, "Good bye."); + return r; + } + + private Throwable headIeChain(Throwable last) { + return new InterruptedException(String.valueOf(last)).initCause(last); + } + + private Throwable headIioeChain(Throwable last) { + return new InterruptedIOException(String.valueOf(last)).initCause(last); + } + + private Throwable headCbieChain(Throwable last) { + return new ClosedByInterruptException().initCause(last); + } + + private Throwable headFlieChain(Throwable last) { + return new FileLockInterruptionException().initCause(last); + } + + private Throwable headSubIeChain(Throwable last) { + return new SubOfIe().initCause(last); + } + + private Throwable headSubIioeChain(Throwable last) { + return new SubOfIioe().initCause(last); + } + + private Throwable headTdChain(Throwable last) { + return new ThreadDeath().initCause(last); + } + + private Throwable headSubTdChain(Throwable last) { + return new SubOfTd().initCause(last); + } + + private Throwable headWidChain(Throwable last) { + return new WorkInterruptedException().initCause(last); + } + + private Throwable headWitChain(Throwable last) { + return new WorkInterruptionException().initCause(last); + } + + /** + * Lots of framework designers dislike checked exceptions. + */ + private static class WorkInterruptionException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + /** + * Promote access. + */ + WorkInterruptionException() { + } + } + + /** + * Lots of framework designers dislike checked exceptions. + */ + private static class WorkInterruptedException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + /** + * Promote access. + */ + WorkInterruptedException() { + } + } + + /** + * Sub class of interrupted. + */ + private static class SubOfIe extends InterruptedException { + + private static final long serialVersionUID = 1L; + + /** + * Promote access. + */ + SubOfIe() { + } + } + + /** + * Sub class of interrupted IO. + */ + private static class SubOfIioe extends InterruptedIOException { + + private static final long serialVersionUID = 1L; + + /** + * Promote access. + */ + SubOfIioe() { + } + } + + /** + * Sub class of thread death. + */ + private static class SubOfTd extends ThreadDeath { + + private static final long serialVersionUID = 1L; + + /** + * Promote access. + */ + SubOfTd() { + } + } +}
diff --git a/mail/src/test/java/javax/mail/URLNameTest.java b/mail/src/test/java/javax/mail/URLNameTest.java new file mode 100644 index 0000000..26ef800 --- /dev/null +++ b/mail/src/test/java/javax/mail/URLNameTest.java
@@ -0,0 +1,79 @@ +/* + * Copyright (c) 2014, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +import java.net.URL; + +import org.junit.*; +import static org.junit.Assert.assertEquals; + +/** + * Test the URLName class. + * + * XXX - for now, just some simple regression tests for reported bugs. + */ +public class URLNameTest { + + @Test + public void testReflexiveEquality() throws Exception { + URLName u = new URLName("test"); + assertEquals(u, u); // bug 6365 + u = new URLName("imap://test.com/INBOX"); + assertEquals(u, u); + } + + /** + * Test that the getFile method returns the file part *without* + * the separator character. This behavior is different than the + * URL or URI classes but needs to be preserved for compatibility. + */ + @Test + public void testFile() throws Exception { + URLName u = new URLName("http://host/file"); + assertEquals("file", u.getFile()); + u = new URLName("http://host:123/file"); + assertEquals("file", u.getFile()); + u = new URLName("http://host/"); + assertEquals("", u.getFile()); + u = new URLName("http://host"); + assertEquals(null, u.getFile()); + u = new URLName("http://host:123"); + assertEquals(null, u.getFile()); + } + + /** + * Test that the getURL method returns a URL with the same value + * as the URLName. + */ + @Test + public void testURL() throws Exception { + // Note: must use a protocol supported by the URL class + URLName u = new URLName("http://host/file"); + assertEquals("file", u.getFile()); + URL url = u.getURL(); + assertEquals(u.toString(), url.toString()); + u = new URLName("http://host:123/file"); + url = u.getURL(); + assertEquals(u.toString(), url.toString()); + u = new URLName("http://host:123/"); + url = u.getURL(); + assertEquals(u.toString(), url.toString()); + u = new URLName("http://host:123"); + url = u.getURL(); + assertEquals(u.toString(), url.toString()); + } +}
diff --git a/mail/src/test/java/javax/mail/internet/AddAddressHeaderTest.java b/mail/src/test/java/javax/mail/internet/AddAddressHeaderTest.java new file mode 100644 index 0000000..b1d337c --- /dev/null +++ b/mail/src/test/java/javax/mail/internet/AddAddressHeaderTest.java
@@ -0,0 +1,81 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import java.util.Properties; + +import javax.mail.Session; +import javax.mail.MessagingException; +import javax.mail.Message; +import javax.mail.internet.MimeMessage; + +import org.junit.*; +import static org.junit.Assert.assertEquals; + +/** + * Test that "add" methods for address headers result in only a single + * address header, per RFC 2822. + */ +public class AddAddressHeaderTest { + + private static Session s = Session.getInstance(new Properties()); + private static InternetAddress[] setList = new InternetAddress[1]; + private static InternetAddress[] addList = new InternetAddress[1]; + + static { + try { + setList[0] = new InternetAddress("me@example.com"); + addList[0] = new InternetAddress("you@example.com"); + } catch (MessagingException ex) { + } + } + + @Test + public void testFrom() throws Exception { + MimeMessage m = new MimeMessage(s); + m.setFrom(setList[0]); + m.addFrom(addList); + m.saveChanges(); + String[] h = m.getHeader("From"); + assertEquals(1, h.length); + } + + @Test + public void testTo() throws Exception { + testRecipients(Message.RecipientType.TO); + } + + @Test + public void testCc() throws Exception { + testRecipients(Message.RecipientType.CC); + } + + @Test + public void testBcc() throws Exception { + testRecipients(Message.RecipientType.BCC); + } + + private void testRecipients(Message.RecipientType type) throws Exception { + MimeMessage m = new MimeMessage(s); + m.setRecipients(type, setList); + m.addRecipients(type, addList); + m.saveChanges(); + // XXX - depends on RecipientType.toString + String[] h = m.getHeader(type.toString()); + assertEquals(1, h.length); + } +}
diff --git a/mail/src/test/java/javax/mail/internet/AddFromTest.java b/mail/src/test/java/javax/mail/internet/AddFromTest.java new file mode 100644 index 0000000..a2a48ee --- /dev/null +++ b/mail/src/test/java/javax/mail/internet/AddFromTest.java
@@ -0,0 +1,66 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import java.util.Properties; + +import javax.mail.Session; +import javax.mail.internet.AddressException; +import javax.mail.internet.MimeMessage; + +import org.junit.*; +import static org.junit.Assert.assertEquals; + +/** + * Test the MimeMultipart.addFrom method, for bug 5057742. + */ +public class AddFromTest { + + private static final Session s = Session.getInstance(new Properties()); + private static final String ADDR = "a@example.com"; + private static final InternetAddress iaddr; + private static final InternetAddress[] addresses; + static { + InternetAddress ia = null; + try { + ia = new InternetAddress(ADDR); + } catch (AddressException ex) { + // can't happen + } finally { + iaddr = ia; + } + addresses = new InternetAddress[] { iaddr }; + } + + @Test + public void testNoFrom() throws Exception { + MimeMessage m = new MimeMessage(s); + m.addFrom(addresses); + assertEquals("Number of From headers", 1, m.getHeader("From").length); + assertEquals("From header", ADDR, m.getHeader("From", ",")); + } + + @Test + public void testOneFrom() throws Exception { + MimeMessage m = new MimeMessage(s); + m.setFrom(iaddr); + m.addFrom(addresses); + assertEquals("Number of From headers", 1, m.getHeader("From").length); + assertEquals("From header", ADDR + ", " + ADDR, + m.getHeader("From", ",")); + } +}
diff --git a/mail/src/test/java/javax/mail/internet/AllowEncodedMessages.java b/mail/src/test/java/javax/mail/internet/AllowEncodedMessages.java new file mode 100644 index 0000000..7b9f90a --- /dev/null +++ b/mail/src/test/java/javax/mail/internet/AllowEncodedMessages.java
@@ -0,0 +1,80 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import com.sun.mail.test.AsciiStringInputStream; +import java.io.ByteArrayInputStream; +import java.nio.charset.Charset; +import java.util.Properties; + +import javax.mail.Session; +import javax.mail.MessagingException; +import javax.mail.BodyPart; + +import org.junit.*; +import static org.junit.Assert.assertEquals; + +/** + * Test "mail.mime.allowencodedmessages" System property. + */ +public class AllowEncodedMessages { + + private static Session s = Session.getInstance(new Properties()); + + @BeforeClass + public static void before() { + System.out.println("AllowEncodedMessages"); + System.setProperty("mail.mime.allowencodedmessages", "true"); + } + + @Test + public void testEncodedMessages() throws Exception { + MimeMessage m = createMessage(); + MimeMultipart mp = (MimeMultipart)m.getContent(); + BodyPart bp = mp.getBodyPart(0); + assertEquals("message/rfc822", bp.getContentType()); + + MimeMessage m2 = (MimeMessage)bp.getContent(); + assertEquals("text/plain", m2.getContentType()); + assertEquals("test message\r\n", m2.getContent()); + } + + @AfterClass + public static void after() { + // should be unnecessary + System.clearProperty("mail.mime.allowencodedmessages"); + } + + private static MimeMessage createMessage() throws MessagingException { + String content = + "Mime-Version: 1.0\n" + + "Subject: Example\n" + + "Content-Type: multipart/mixed; boundary=\"-\"\n" + + "\n" + + "---\n" + + "Content-Type: message/rfc822\n" + + "Content-Transfer-Encoding: base64\n" + + "\n" + + "TWltZS1WZXJzaW9uOiAxLjANClN1YmplY3Q6IH" + + "Rlc3QNCkNvbnRlbnQtVHlwZTogdGV4dC9wbGFp\n" + + "bg0KDQp0ZXN0IG1lc3NhZ2UNCg==\n" + + "\n" + + "-----\n"; + + return new MimeMessage(s, new AsciiStringInputStream(content)); + } +}
diff --git a/mail/src/test/java/javax/mail/internet/AppleFileNames.java b/mail/src/test/java/javax/mail/internet/AppleFileNames.java new file mode 100644 index 0000000..5d6d477 --- /dev/null +++ b/mail/src/test/java/javax/mail/internet/AppleFileNames.java
@@ -0,0 +1,45 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import org.junit.*; +import static org.junit.Assert.assertEquals; + +/** + * Test that the "mail.mime.applefilenames" System property + * causes the filename to be returned properly. + */ +public class AppleFileNames { + + @BeforeClass + public static void before() { + System.out.println("AppleFileNames"); + System.setProperty("mail.mime.applefilenames", "true"); + } + + @Test + public void testProp() throws Exception { + ParameterList pl = new ParameterList("; filename=a b.txt"); + assertEquals(pl.get("filename"), "a b.txt"); + } + + @AfterClass + public static void after() { + // should be unnecessary + System.clearProperty("mail.mime.applefilenames"); + } +}
diff --git a/mail/src/test/java/javax/mail/internet/ContentDispositionNoStrict.java b/mail/src/test/java/javax/mail/internet/ContentDispositionNoStrict.java new file mode 100644 index 0000000..c920d3c --- /dev/null +++ b/mail/src/test/java/javax/mail/internet/ContentDispositionNoStrict.java
@@ -0,0 +1,58 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import org.junit.*; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; + +/** + * Test the property that contols ContentDisposition non-strict mode + */ +public class ContentDispositionNoStrict { + + @BeforeClass + public static void before() { + System.setProperty("mail.mime.contentdisposition.strict", "false"); + } + + @Test + public void testDecode() throws Exception { + try { + ContentDisposition cd = new ContentDisposition("\"/non/standard/stuff/here.csv\""); + assertNull("Content disposition must parse to null in non-strict mode", cd.getDisposition()); + } catch (ParseException px) { + fail("Exception must not be thrown in non-strict mode"); + } + } + + @Test + public void testDecodeWithParams() throws Exception { + try { + ContentDisposition cd = new ContentDisposition(" ; size=12345"); + assertNull("Content disposition must parse to null in non-strict mode", cd.getDisposition()); + } catch (ParseException px) { + fail("Exception must not be thrown in non-strict mode"); + } + } + + @AfterClass + public static void after() { + System.clearProperty("mail.mime.contentdisposition.strict"); + } +} +
diff --git a/mail/src/test/java/javax/mail/internet/ContentDispositionStrict.java b/mail/src/test/java/javax/mail/internet/ContentDispositionStrict.java new file mode 100644 index 0000000..a41efe5 --- /dev/null +++ b/mail/src/test/java/javax/mail/internet/ContentDispositionStrict.java
@@ -0,0 +1,48 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import org.junit.*; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; + +/** + * Test the property that contols ContentDisposition non-strict mode + */ +public class ContentDispositionStrict { + + @BeforeClass + public static void before() { + System.setProperty("mail.mime.contentdisposition.strict", "true"); + } + + @Test + public void testDecode() throws Exception { + try { + ContentDisposition cd = new ContentDisposition("\"/non/standard/stuff/here.csv\""); + fail("ParseException should be thrown on malformed content disposition"); + } catch (ParseException expected) { + // the exception is the expected outcome in this test + } + } + + @AfterClass + public static void after() { + System.clearProperty("mail.mime.contentdisposition.strict"); + } +} +
diff --git a/mail/src/test/java/javax/mail/internet/ContentDispositionTestSuite.java b/mail/src/test/java/javax/mail/internet/ContentDispositionTestSuite.java new file mode 100644 index 0000000..93ccf5b --- /dev/null +++ b/mail/src/test/java/javax/mail/internet/ContentDispositionTestSuite.java
@@ -0,0 +1,35 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import org.junit.runner.RunWith; +import org.junit.runners.Suite.SuiteClasses; + +import com.sun.mail.test.ClassLoaderSuite; +import com.sun.mail.test.ClassLoaderSuite.TestClass; + +/** + * Suite of ParameterList tests that need to be run in a separate class loader. + */ +@RunWith(ClassLoaderSuite.class) +@TestClass(ContentDisposition.class) +@SuiteClasses( { + ContentDispositionNoStrict.class, + ContentDispositionStrict.class +}) +public class ContentDispositionTestSuite { +}
diff --git a/mail/src/test/java/javax/mail/internet/ContentTypeTest.java b/mail/src/test/java/javax/mail/internet/ContentTypeTest.java new file mode 100644 index 0000000..34e8735 --- /dev/null +++ b/mail/src/test/java/javax/mail/internet/ContentTypeTest.java
@@ -0,0 +1,43 @@ +/* + * Copyright (c) 2014, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import org.junit.*; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertFalse; + +/** + * Test the ContentType class. + * + * XXX - for now, just some simple regression tests for reported bugs. + */ +public class ContentTypeTest { + + @Test + public void testMatch() throws Exception { + ContentType empty = new ContentType(); + assertFalse(empty.match("text/plain")); + ContentType plain = new ContentType("text/plain"); + assertTrue(plain.match("text/plain")); + assertFalse(empty.match(plain)); + assertFalse(plain.match(empty)); + assertTrue(plain.match("text/*")); + ContentType text = new ContentType("text/*"); + assertTrue(text.match(plain)); + assertTrue(plain.match(text)); + } +}
diff --git a/mail/src/test/java/javax/mail/internet/DecodeParameters.java b/mail/src/test/java/javax/mail/internet/DecodeParameters.java new file mode 100644 index 0000000..374d587 --- /dev/null +++ b/mail/src/test/java/javax/mail/internet/DecodeParameters.java
@@ -0,0 +1,44 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import org.junit.*; +import static org.junit.Assert.assertEquals; + +/** + * Test that the "mail.mime.decodeparameters" System property + * causes the parameters to be properly decoded. + */ +public class DecodeParameters extends ParameterListDecode { + + @BeforeClass + public static void before() { + System.out.println("DecodeParameters"); + System.setProperty("mail.mime.decodeparameters", "true"); + } + + @Test + public void testDecode() throws Exception { + testDecode("paramdata"); + } + + @AfterClass + public static void after() { + // should be unnecessary + System.clearProperty("mail.mime.decodeparameters"); + } +}
diff --git a/mail/src/test/java/javax/mail/internet/EncodeFileName.java b/mail/src/test/java/javax/mail/internet/EncodeFileName.java new file mode 100644 index 0000000..f331ba0 --- /dev/null +++ b/mail/src/test/java/javax/mail/internet/EncodeFileName.java
@@ -0,0 +1,69 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import javax.mail.MessagingException; +import javax.mail.internet.MimeBodyPart; + +import org.junit.*; +import static org.junit.Assert.assertTrue; + +/** + * Test "mail.mime.encodefilename" System property set. + */ +public class EncodeFileName extends NoEncodeFileName { + + // depends on exactly how MimeUtility.encodeText splits long words + private static String expected1 = + "=?utf-8?B?w4DDgcOFw4bDgMOBw4XDhsOHw4jDicOKw4vDjMONw47Dj8OQw4DDgcOF?="; + private static String expected2 = + "=?utf-8?B?w4bDh8OIw4nDisOLw4zDjcOOw4/DkMORw5LDk8OUw5XDlsOYw5nDmsObw5w=?="; + private static String expected3 = + "=?utf-8?B?w53DnsOfw6DDocOiw6PDpMOlw6bDp8Oow6nDqsOrw6zDrcOuw6/DsMOx?="; + private static String expected4 = + "=?utf-8?B?w7LDs8O0w7XDtsO4w7nDusO7w7zDvcO+w7/DgMOBw4XDhsOHLmRvYw==?="; + + @BeforeClass + public static void before() { + System.out.println("EncodeFileName"); + System.setProperty("mail.mime.charset", "utf-8"); + System.setProperty("mail.mime.encodefilename", "true"); + // assume mail.mime.encodeparamters defaults to true + System.clearProperty("mail.mime.encodeparamters"); + } + + @Test + @Override + public void test() throws Exception { + MimeBodyPart mbp = new MimeBodyPart(); + mbp.setText("test"); + mbp.setFileName(fileName); + mbp.updateHeaders(); + String h = mbp.getHeader("Content-Type", ""); + assertTrue(h.contains("name=")); + assertTrue(h.contains(expected1)); + assertTrue(h.contains(expected2)); + assertTrue(h.contains(expected3)); + assertTrue(h.contains(expected4)); + h = mbp.getHeader("Content-Disposition", ""); + assertTrue(h.contains("filename=")); + assertTrue(h.contains(expected1)); + assertTrue(h.contains(expected2)); + assertTrue(h.contains(expected3)); + assertTrue(h.contains(expected4)); + } +}
diff --git a/mail/src/test/java/javax/mail/internet/EncodeFileNameNoEncodeParameters.java b/mail/src/test/java/javax/mail/internet/EncodeFileNameNoEncodeParameters.java new file mode 100644 index 0000000..1db08af --- /dev/null +++ b/mail/src/test/java/javax/mail/internet/EncodeFileNameNoEncodeParameters.java
@@ -0,0 +1,34 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import org.junit.*; + +/** + * Test "mail.mime.encodefilename" System property set to "true" + * and "mail.mime.encodeparameters" set to "false". + */ +public class EncodeFileNameNoEncodeParameters extends EncodeFileName { + + @BeforeClass + public static void before() { + System.out.println("EncodeFileNameNoEncodeParameters"); + System.setProperty("mail.mime.charset", "utf-8"); + System.setProperty("mail.mime.encodefilename", "true"); + System.setProperty("mail.mime.encodeparamters", "false"); + } +}
diff --git a/mail/src/test/java/javax/mail/internet/FoldTest.java b/mail/src/test/java/javax/mail/internet/FoldTest.java new file mode 100644 index 0000000..39f49d8 --- /dev/null +++ b/mail/src/test/java/javax/mail/internet/FoldTest.java
@@ -0,0 +1,110 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import java.io.*; +import java.util.*; +import javax.mail.internet.MimeUtility; + +import org.junit.Test; +import org.junit.Assert; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +/** + * Test header folding. + * + * @author Bill Shannon + */ + +@RunWith(Parameterized.class) +public class FoldTest { + private String direction; + private String orig; + private String expect; + + private static List<Object[]> testData; + + public FoldTest(String direction, String orig, String expect) { + this.direction = direction; + this.orig = orig; + this.expect = expect; + } + + @Parameters + public static Collection<Object[]> data() throws IOException { + testData = new ArrayList<>(); + parse(new BufferedReader(new InputStreamReader( + FoldTest.class.getResourceAsStream("folddata")))); + return testData; + } + + /** + * Read the data from the test file. Format is multiple of any of + * the following: + * + * FOLD\nString$\nEXPECT\nString$\n + * UNFOLD\nString$\nEXPECT\nString$\n + * BOTH\nString$\n + */ + private static void parse(BufferedReader in) throws IOException { + String line; + while ((line = in.readLine()) != null) { + if (line.startsWith("#") || line.length() == 0) + continue; + String orig = readString(in); + if (line.equals("BOTH")) { + testData.add(new Object[] { line, orig, null }); + } else { + String e = in.readLine(); + if (!e.equals("EXPECT")) + throw new IOException("TEST DATA FORMAT ERROR"); + String expect = readString(in); + testData.add(new Object[] { line, orig, expect }); + } + } + } + + /** + * Read a string that ends with '$', preserving all characters, + * especially including CR and LF. + */ + private static String readString(BufferedReader in) throws IOException { + StringBuffer sb = new StringBuffer(); + int c; + while ((c = in.read()) != '$') + sb.append((char)c); + in.readLine(); // throw away rest of line + return sb.toString(); + } + + @Test + public void testFold() { + if (direction.equals("BOTH")) { + String fs = MimeUtility.fold(0, orig); + String us = MimeUtility.unfold(fs); + Assert.assertEquals(orig, us); + } else if (direction.equals("FOLD")) { + Assert.assertEquals("Fold", expect, MimeUtility.fold(0, orig)); + } else if (direction.equals("UNFOLD")) { + Assert.assertEquals("Unfold", expect, MimeUtility.unfold(orig)); + } else { + Assert.fail("Unknown direction: " + direction); + } + } +}
diff --git a/mail/src/test/java/javax/mail/internet/GetLocalAddressTest.java b/mail/src/test/java/javax/mail/internet/GetLocalAddressTest.java new file mode 100644 index 0000000..a4432e4 --- /dev/null +++ b/mail/src/test/java/javax/mail/internet/GetLocalAddressTest.java
@@ -0,0 +1,109 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Properties; + +import javax.mail.Session; +import javax.mail.MessagingException; +import javax.mail.internet.InternetAddress; + +import org.junit.*; +import static org.junit.Assert.assertEquals; + +/** + * Test the InternetAddress.getLocalAddress() method. + */ +public class GetLocalAddressTest { + + private static String localhost; + static { + try { + localhost = InetAddress.getLocalHost().getCanonicalHostName(); + } catch (UnknownHostException ex) { + localhost = "localhost"; + } + } + + @Test + public void testUserName() throws Exception { + System.setProperty("user.name", "Joe"); + InternetAddress ia = InternetAddress.getLocalAddress(null); + assertEquals("Joe@" + localhost, ia.getAddress()); + } + + @Test + public void testUserNameSession() throws Exception { + System.setProperty("user.name", "Joe"); + Session s = Session.getInstance(new Properties()); + InternetAddress ia = InternetAddress.getLocalAddress(s); + assertEquals("Joe@" + localhost, ia.getAddress()); + } + + @Test + public void testMailFrom() throws Exception { + System.setProperty("user.name", "Joe"); + Properties p = new Properties(); + p.setProperty("mail.from", "Bob@home"); + Session s = Session.getInstance(p); + InternetAddress ia = InternetAddress.getLocalAddress(s); + assertEquals("Bob@home", ia.getAddress()); + } + + @Test + public void testMailFromAddress() throws Exception { + System.setProperty("user.name", "Joe"); + Properties p = new Properties(); + p.setProperty("mail.from", "Bob <Bob@home>"); + Session s = Session.getInstance(p); + InternetAddress ia = InternetAddress.getLocalAddress(s); + assertEquals("Bob@home", ia.getAddress()); + } + + @Test + public void testMailUser() throws Exception { + System.setProperty("user.name", "Joe"); + Properties p = new Properties(); + p.setProperty("mail.user", "Bob"); + Session s = Session.getInstance(p); + InternetAddress ia = InternetAddress.getLocalAddress(s); + assertEquals("Bob@" + localhost, ia.getAddress()); + } + + @Test + public void testMailUserSpace() throws Exception { + System.setProperty("user.name", "Joe"); + Properties p = new Properties(); + p.setProperty("mail.user", "NETWORK SERVICE"); + Session s = Session.getInstance(p); + InternetAddress ia = InternetAddress.getLocalAddress(s); + assertEquals("\"NETWORK SERVICE\"@" + localhost, ia.getAddress()); + } + + @Test + public void testMailHost() throws Exception { + System.setProperty("user.name", "Joe"); + Properties p = new Properties(); + p.setProperty("mail.user", "Bob"); + p.setProperty("mail.host", "home"); + Session s = Session.getInstance(p); + InternetAddress ia = InternetAddress.getLocalAddress(s); + assertEquals("Bob@home", ia.getAddress()); + } +}
diff --git a/mail/src/test/java/javax/mail/internet/HeaderTokenizerTest.java b/mail/src/test/java/javax/mail/internet/HeaderTokenizerTest.java new file mode 100644 index 0000000..12c1086 --- /dev/null +++ b/mail/src/test/java/javax/mail/internet/HeaderTokenizerTest.java
@@ -0,0 +1,316 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import java.io.*; +import java.util.*; +import javax.mail.*; +import javax.mail.internet.HeaderTokenizer; +import javax.mail.internet.ParseException; + +import org.junit.Test; +import org.junit.Assert; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +/** + * Test MIME HeaderTokenizer. + * + * @author Bill Shannon + */ + +@RunWith(Parameterized.class) +public class HeaderTokenizerTest { + private String header; + private String value; + private String[] expect; + + static boolean gen_test_input = false; // output good for input to -p + static boolean parse_mail = false; // parse input in mail format + static boolean return_comments = false; // return comments as tokens + static boolean mime = false; // use MIME specials + static int errors = 0; // number of errors detected + + static boolean junit; + static List<Object[]> testData; + + public HeaderTokenizerTest(String heder, String value, String[] expect) { + this.header = header; + this.value = value; + this.expect = expect; + } + + @Parameters + public static Collection<Object[]> data() throws IOException { + junit = true; + testData = new ArrayList<>(); + parse(new BufferedReader(new InputStreamReader( + InternetAddressTest.class.getResourceAsStream("tokenlist")))); + return testData; + } + + @Test + public void test() { + test(header, value, expect); + } + + public static void main(String argv[]) throws Exception { + int optind; + for (optind = 0; optind < argv.length; optind++) { + if (argv[optind].equals("-")) { + // ignore + } else if (argv[optind].equals("-g")) { + gen_test_input = true; + } else if (argv[optind].equals("-p")) { + parse_mail = true; + } else if (argv[optind].equals("-c")) { + return_comments = true; + } else if (argv[optind].equals("-m")) { + mime = true; + } else if (argv[optind].equals("--")) { + optind++; + break; + } else if (argv[optind].startsWith("-")) { + System.out.println( + "Usage: tokenizertest [-g] [-p] [-c] [-m] [-] [header ...]"); + System.exit(1); + } else { + break; + } + } + + /* + * If there's any args left on the command line, + * concatenate them into a string and test that. + */ + if (optind < argv.length) { + StringBuffer sb = new StringBuffer(); + for (int i = optind; i < argv.length; i++) { + sb.append(argv[i]); + sb.append(" "); + } + test("To", sb.toString(), null); + } else { + // read from stdin + BufferedReader in = + new BufferedReader(new InputStreamReader(System.in)); + String s; + + if (parse_mail) + parse(in); + else { + while ((s = in.readLine()) != null) + test("To", s, null); + } + } + System.exit(errors); + + } + + /* + * Parse the input in "mail" format, extracting the From, To, and Cc + * headers and testing them. The parse is rather crude, but sufficient + * to test against most existing UNIX mailboxes. + */ + public static void parse(BufferedReader in) throws IOException { + String header = ""; + + for (;;) { + String s = in.readLine(); + if (s != null && s.length() > 0) { + char c = s.charAt(0); + if (c == ' ' || c == '\t') { + // a continuation line, add it to the current header + header += '\n' + s; + continue; + } + } + // "s" is the next header, "header" is the last complete header + if (header.startsWith("From: ") || + header.startsWith("To: ") || + header.startsWith("Cc: ")) { + int i; + String[] expect = null; + if (s != null && s.startsWith("Expect: ")) { + try { + int nexpect = Integer.parseInt(s.substring(8)); + expect = new String[nexpect]; + for (i = 0; i < nexpect; i++) + expect[i] = in.readLine().trim(); + } catch (NumberFormatException e) { + try { + if (s.substring(8, 17).equals("Exception")) { + expect = new String[1]; + expect[0] = "Exception"; + } + } catch (StringIndexOutOfBoundsException se) { + // ignore it + } + } + } + i = header.indexOf(':'); + try { + if (junit) + testData.add(new Object[] { + header.substring(0, i), + header.substring(i + 2), + expect }); + else + test(header.substring(0, i), header.substring(i + 2), + expect); + } catch (StringIndexOutOfBoundsException e) { + // ignore + } + } + if (s == null) + return; // EOF + if (s.length() == 0) { + while ((s = in.readLine()) != null) { + if (s.startsWith("From ")) + break; + } + if (s == null) + return; + } + header = s; + } + } + + /** + * Test the header's value to see if we can tokenize it as expected. + */ + public static void test(String header, String value, String expect[]) { + PrintStream out = System.out; + if (gen_test_input) + out.println(header + ": " + value); + else if (!junit) + out.println("Test: " + value); + + try { + HeaderTokenizer ht = new HeaderTokenizer(value, + mime ? HeaderTokenizer.MIME : HeaderTokenizer.RFC822, + !return_comments); + HeaderTokenizer.Token tok; + Vector<HeaderTokenizer.Token> toklist + = new Vector<>(); + while ((tok = ht.next()).getType() != HeaderTokenizer.Token.EOF) + toklist.addElement(tok); + if (gen_test_input) + out.println("Expect: " + toklist.size()); + else { + if (junit) { + Assert.assertEquals("Number of tokens", + expect.length, toklist.size()); + } else { + out.println("Got " + toklist.size() + " tokens:"); + if (expect != null && toklist.size() != expect.length) { + out.println("Expected " + expect.length + " tokens"); + errors++; + } + } + } + for (int i = 0; i < toklist.size(); i++) { + tok = toklist.elementAt(i); + if (gen_test_input) + out.println("\t" + type(tok.getType()) + + "\t" + tok.getValue()); + else { + if (!junit) + out.println("\t[" + (i+1) + "] " + type(tok.getType()) + + "\t" + tok.getValue()); + if (expect != null && i < expect.length) { + HeaderTokenizer.Token t = makeToken(expect[i]); + if (junit) { + Assert.assertEquals("Token type", + t.getType(), tok.getType()); + Assert.assertEquals("Token value", + t.getValue(), tok.getValue()); + } else { + if (t.getType() != tok.getType() || + !t.getValue().equals(tok.getValue())) { + out.println("\tExpected:\t" + + type(t.getType()) + "\t" + t.getValue()); + errors++; + } + } + } + } + } + } catch (ParseException e) { + if (gen_test_input) + out.println("Expect: Exception " + e); + else { + if (junit) { + Assert.assertTrue("Expected exception", + expect.length == 1 && expect[0].equals("Exception")); + } else { + out.println("Got Exception: " + e); + if (expect != null && + (expect.length != 1 || !expect[0].equals("Exception"))) { + out.println("Expected " + expect.length + " tokens"); + for (int i = 0; i < expect.length; i++) + out.println("\tExpected:\t" + expect[i]); + errors++; + } + } + } + } + } + + private static String type(int t) { + if (t == HeaderTokenizer.Token.ATOM) + return "ATOM"; + else if (t == HeaderTokenizer.Token.QUOTEDSTRING) + return "QUOTEDSTRING"; + else if (t == HeaderTokenizer.Token.COMMENT) + return "COMMENT"; + else if (t == HeaderTokenizer.Token.EOF) + return "EOF"; + else if (t < 0) + return "UNKNOWN"; + else + return "SPECIAL"; + } + + private static int type(String s) { + if (s.equals("ATOM")) + return HeaderTokenizer.Token.ATOM; + else if (s.equals("QUOTEDSTRING")) + return HeaderTokenizer.Token.QUOTEDSTRING; + else if (s.equals("COMMENT")) + return HeaderTokenizer.Token.COMMENT; + else if (s.equals("EOF")) + return HeaderTokenizer.Token.EOF; + else // if (s.equals("SPECIAL")) + return 0; + } + + private static HeaderTokenizer.Token makeToken(String line) { + int i = line.indexOf('\t'); + int t = type(line.substring(0, i)); + String value = line.substring(i + 1); + if (t == 0) + return new HeaderTokenizer.Token(value.charAt(0), value); + else + return new HeaderTokenizer.Token(t, value); + } + + private static final String n(String s) { + return s == null ? "<null>" : s; + } +}
diff --git a/mail/src/test/java/javax/mail/internet/InternetAddressExtraTest.java b/mail/src/test/java/javax/mail/internet/InternetAddressExtraTest.java new file mode 100644 index 0000000..0f2fa73 --- /dev/null +++ b/mail/src/test/java/javax/mail/internet/InternetAddressExtraTest.java
@@ -0,0 +1,63 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import javax.mail.internet.InternetAddress; +import javax.mail.internet.AddressException; + +import org.junit.Test; + +import static org.junit.Assert.fail; + +/** + * Test Internet address parsing that can't be tested using paramterized test. + * + * @author Bill Shannon + */ + +public class InternetAddressExtraTest { + @Test + public void testNewlineInDomainLiteral() throws Exception { + InternetAddress ia = new InternetAddress("test@[\r\nfoo]", "test"); + try { + ia.validate(); + fail("validation succeeded"); + } catch (AddressException ex) { + // success! + } + } + + @Test + public void testNewlineInLocal() throws Exception { + InternetAddress ia = + new InternetAddress("\"test\r\nfoo\"@example.com", "test"); + try { + ia.validate(); + fail("validation succeeded"); + } catch (AddressException ex) { + // success! + } + } + + @Test + public void testNewlineInLocalWithWhitespace() throws Exception { + InternetAddress ia = + new InternetAddress("\"test\r\n foo\"@example.com", "test"); + ia.validate(); + // success! + } +}
diff --git a/mail/src/test/java/javax/mail/internet/InternetAddressFoldTest.java b/mail/src/test/java/javax/mail/internet/InternetAddressFoldTest.java new file mode 100644 index 0000000..4270822 --- /dev/null +++ b/mail/src/test/java/javax/mail/internet/InternetAddressFoldTest.java
@@ -0,0 +1,101 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import java.io.*; +import java.util.*; +import javax.mail.internet.InternetAddress; + +import org.junit.Test; +import org.junit.Assert; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +/** + * Test InternetAddress folding. + * + * @author Bill Shannon + */ + +@RunWith(Parameterized.class) +public class InternetAddressFoldTest { + private InternetAddress[] orig; + private String expect; + + private static List<Object[]> testData; + + public InternetAddressFoldTest(InternetAddress[] orig, String expect) { + this.orig = orig; + this.expect = expect; + } + + @Parameters + public static Collection<Object[]> data() throws Exception { + testData = new ArrayList<>(); + parse(new BufferedReader(new InputStreamReader( + InternetAddressFoldTest.class.getResourceAsStream("addrfolddata")))); + return testData; + } + + /** + * Read the data from the test file. Format is: + * + * FOLD N + * address1$ + * ... + * addressN$ + * EXPECT + * address1, ..., addressN$ + */ + private static void parse(BufferedReader in) throws Exception { + String line; + while ((line = in.readLine()) != null) { + if (line.startsWith("#") || line.length() == 0) + continue; + if (!line.startsWith("FOLD")) + throw new IOException("TEST DATA FORMAT ERROR, MISSING FOLD"); + int count = Integer.parseInt(line.substring(5)); + InternetAddress[] orig = new InternetAddress[count]; + for (int i = 0; i < count; i++) + orig[i] = new InternetAddress(readString(in)); + String e = in.readLine(); + if (!e.equals("EXPECT")) + throw new IOException("TEST DATA FORMAT ERROR, MISSING EXPECT"); + String expect = readString(in); + testData.add(new Object[] { orig, expect }); + } + } + + /** + * Read a string that ends with '$', preserving all characters, + * especially including CR and LF. + */ + private static String readString(BufferedReader in) throws IOException { + StringBuffer sb = new StringBuffer(); + int c; + while ((c = in.read()) != '$') + sb.append((char)c); + in.readLine(); // throw away rest of line + return sb.toString(); + } + + @Test + public void testFold() { + Assert.assertEquals("Fold", expect, InternetAddress.toString(orig, 0)); + } +}
diff --git a/mail/src/test/java/javax/mail/internet/InternetAddressTest.java b/mail/src/test/java/javax/mail/internet/InternetAddressTest.java new file mode 100644 index 0000000..7c5f753 --- /dev/null +++ b/mail/src/test/java/javax/mail/internet/InternetAddressTest.java
@@ -0,0 +1,380 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import java.io.*; +import java.util.*; +import javax.mail.*; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.AddressException; + +import org.junit.Test; +import org.junit.Assert; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +/** + * Test Internet address parsing. + * + * @author Bill Shannon + */ + +@RunWith(Parameterized.class) +public class InternetAddressTest { + private String headerName; + private String headerValue; + private String[] expected; + private boolean doStrict; + private boolean doParseHeader; + + static boolean strict = false; // enforce strict RFC822 syntax + static boolean gen_test_input = false; // output good for input to -p + static boolean parse_mail = false; // parse input in mail format + static boolean parse_header = false; // use parseHeader method? + static boolean verbose; // print progress? + static int errors = 0; // number of errors detected + + static boolean junit; + static List<Object[]> testData; + + public InternetAddressTest(String headerName, String headerValue, + String[] expected, boolean doStrict, boolean doParseHeader) { + this.headerName = headerName; + this.headerValue = headerValue; + this.expected = expected; + this.doStrict = doStrict; + this.doParseHeader = doParseHeader; + } + + @Parameters + public static Collection<Object[]> data() throws IOException { + junit = true; + testData = new ArrayList<>(); + parse(new BufferedReader(new InputStreamReader( + InternetAddressTest.class.getResourceAsStream("addrlist")))); + return testData; + } + + public static void main(String argv[]) throws Exception { + verbose = true; // default for standalone + int optind; + for (optind = 0; optind < argv.length; optind++) { + if (argv[optind].equals("-")) { + // ignore + } else if (argv[optind].equals("-g")) { + gen_test_input = true; + } else if (argv[optind].equals("-h")) { + parse_header = true; + } else if (argv[optind].equals("-p")) { + parse_mail = true; + } else if (argv[optind].equals("-s")) { + strict = true; + } else if (argv[optind].equals("-q")) { + verbose = false; + } else if (argv[optind].equals("--")) { + optind++; + break; + } else if (argv[optind].startsWith("-")) { + System.out.println( + "Usage: addrtest [-g] [-h] [-p] [-s] [-q] [-] [address ...]"); + System.exit(1); + } else { + break; + } + } + + /* + * If there's any args left on the command line, + * concatenate them into a string and test that. + */ + if (optind < argv.length) { + StringBuffer sb = new StringBuffer(); + for (int i = optind; i < argv.length; i++) { + sb.append(argv[i]); + sb.append(" "); + } + test("To", sb.toString(), null, strict, parse_header); + } else { + // read from stdin + BufferedReader in = + new BufferedReader(new InputStreamReader(System.in)); + String s; + + if (parse_mail) + parse(in); + else { + while ((s = in.readLine()) != null) + test("To", s, null, strict, parse_header); + } + } + System.exit(errors); + + } + + /* + * Parse the input in "mail" format, extracting the From, To, and Cc + * headers and testing them. The parse is rather crude, but sufficient + * to test against most existing UNIX mailboxes. + */ + public static void parse(BufferedReader in) throws IOException { + String header = ""; + boolean doStrict = strict; + boolean doParseHeader = parse_header; + + for (;;) { + String s = in.readLine(); + if (s != null && s.length() > 0) { + char c = s.charAt(0); + if (c == ' ' || c == '\t') { + // a continuation line, add it to the current header + header += '\n' + s; + continue; + } + } + // "s" is the next header, "header" is the last complete header + if (header.startsWith("Strict: ")) { + doStrict = Boolean.parseBoolean(value(header)); + } else if (header.startsWith("Header: ")) { + doParseHeader = Boolean.parseBoolean(value(header)); + } else if (header.startsWith("From: ") || + header.startsWith("To: ") || + header.startsWith("Cc: ")) { + int i; + String[] expect = null; + if (s != null && s.startsWith("Expect: ")) { + try { + int nexpect = Integer.parseInt(s.substring(8)); + expect = new String[nexpect]; + for (i = 0; i < nexpect; i++) + expect[i] = readLine(in).trim(); + } catch (NumberFormatException e) { + try { + if (s.substring(8, 17).equals("Exception")) { + expect = new String[1]; + expect[0] = "Exception"; + } + } catch (StringIndexOutOfBoundsException se) { + // ignore it + } + } + } + i = header.indexOf(':'); + try { + if (junit) + testData.add(new Object[] { + header.substring(0, i), header.substring(i + 2), + expect, doStrict, doParseHeader }); + else + test(header.substring(0, i), header.substring(i + 2), + expect, doStrict, doParseHeader); + } catch (StringIndexOutOfBoundsException e) { + e.printStackTrace(System.out); + } + } + if (s == null) + return; // EOF + if (s.length() == 0) { + while ((s = in.readLine()) != null) { + if (s.startsWith("From ")) + break; + } + if (s == null) + return; + } + header = s; + } + } + + private static String value(String header) { + return header.substring(header.indexOf(':') + 1).trim(); + } + + /** + * Read an "expected" line, handling continuations + * (backslash at end of line). If line ends with + * two backslashes, it's not a continuation, just a + * line that ends with a single backslash. + */ + private static String readLine(BufferedReader in) throws IOException { + String line = in.readLine(); + if (!line.endsWith("\\")) + return line; + if (line.endsWith("\\\\")) + return line.substring(0, line.length() - 1); + StringBuilder sb = new StringBuilder(line); + sb.setCharAt(sb.length() - 1, '\n'); + for (;;) { + line = in.readLine(); + sb.append(line); + if (!line.endsWith("\\")) + break; + if (line.endsWith("\\\\")) { + sb.setLength(sb.length() - 1); + break; + } + sb.setCharAt(sb.length() - 1, '\n'); + } + return sb.toString(); + } + + @Test + public void testAddress() { + test(headerName, headerValue, expected, doStrict, doParseHeader); + } + + /** + * Test the header's value to see if we can parse it as expected. + */ + public static void test(String header, String value, String expect[], + boolean doStrict, boolean doParseHeader) { + PrintStream out = System.out; + if (gen_test_input) + pr(header + ": " + value); + else + pr("Test: " + value); + + try { + InternetAddress[] al; + if (doParseHeader) + al = InternetAddress.parseHeader(value, doStrict); + else + al = InternetAddress.parse(value, doStrict); + if (gen_test_input) + pr("Expect: " + al.length); + else { + pr("Got " + al.length + " addresses:"); + if (expect != null && al.length != expect.length) { + pr("Expected " + expect.length + " addresses"); + if (junit) + Assert.assertEquals("For " + value + + " number of addresses", + al.length, expect.length); + errors++; + } + } + for (int i = 0; i < al.length; i++) { + if (gen_test_input) + pr("\t" + al[i].getAddress()); // XXX - escape newlines + else { + pr("\t[" + (i+1) + "] " + al[i].getAddress() + + "\t\tPersonal: " + n(al[i].getPersonal())); + if (expect != null && i < expect.length && + !expect[i].equals(al[i].getAddress())) { + pr("\tExpected:\t" + expect[i]); + if (junit) + Assert.assertEquals("For " + value + + " address[" + i + "]", + expect[i], al[i].getAddress()); + errors++; + } + } + } + + if (al.length == 0) + return; + + /* + * Some of the really bad addresses fail the toString + * tests, but we don't want them to cause build failures. + */ + if (junit) + return; + + /* + * As a sanity test, convert the address array to a string and + * then parse it again, to see if we get the same thing back. + */ + try { + InternetAddress[] al2; + String ta = InternetAddress.toString(al); + if (doParseHeader) + al2 = InternetAddress.parseHeader(ta, doStrict); + else + al2 = InternetAddress.parse(ta, doStrict); + if (al.length != al2.length) { + pr("toString FAILED!!!"); + pr("Expected length " + al.length + + ", got " + al2.length); + if (junit) + Assert.assertEquals("For " + value + + " toString number of addresses", + al.length, al2.length); + errors++; + } else { + for (int i = 0; i < al.length; i++) { + if (!al[i].getAddress().equals(al2[i].getAddress())) { + pr("toString FAILED!!!"); + pr("Expected address " + + al[i].getAddress() + + ", got " + al2[i].getAddress()); + if (junit) + Assert.assertEquals("For " + value + + " toString " + ta + " address[" + i + "]", + al[i].getAddress(), al2[i].getAddress()); + errors++; + } + String p1 = al[i].getPersonal(); + String p2 = al2[i].getPersonal(); + if (!(p1 == p2 || (p1 != null && p1.equals(p2)))) { + pr("toString FAILED!!!"); + pr("Expected personal " + n(p1) + + ", got " + n(p2)); + if (junit) + Assert.assertEquals("For " + value + + " toString " + ta + " personal[" + i + "]", + p1, p2); + errors++; + } + } + } + } catch (AddressException e2) { + pr("toString FAILED!!!"); + pr("Got Exception: " + e2); + if (junit) + Assert.fail("For " + value + + " toString got Exception: " + e2); + errors++; + } + } catch (AddressException e) { + if (gen_test_input) + pr("Expect: Exception " + e); + else { + pr("Got Exception: " + e); + if (expect != null && + (expect.length != 1 || !expect[0].equals("Exception"))) { + pr("Expected " + expect.length + " addresses"); + for (int i = 0; i < expect.length; i++) + pr("\tExpected:\t" + expect[i]); + if (junit) + Assert.fail("For " + value + " expected " + + expect.length + "addresses, got Exception: " + e); + errors++; + } + } + } + } + + private static final void pr(String s) { + if (verbose) + System.out.println(s); + } + + private static final String n(String s) { + return s == null ? "<null>" : s; + } +}
diff --git a/mail/src/test/java/javax/mail/internet/InternetHeadersTest.java b/mail/src/test/java/javax/mail/internet/InternetHeadersTest.java new file mode 100644 index 0000000..a96688c --- /dev/null +++ b/mail/src/test/java/javax/mail/internet/InternetHeadersTest.java
@@ -0,0 +1,115 @@ +/* + * Copyright (c) 2011, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import com.sun.mail.test.AsciiStringInputStream; +import java.io.*; +import java.nio.charset.Charset; +import java.util.Enumeration; + +import javax.mail.*; + +import org.junit.*; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertEquals; + +/** + * Test the InternetHeaders class. + */ +public class InternetHeadersTest { + + private static final String initialWhitespaceHeader = + " \r\nSubject: test\r\n\r\n"; + private static final String initialContinuationHeader = + " Subject: test\r\n\r\n"; + + /** + * Test that a continuation line is handled properly. + */ + @Test + public void testContinuationLine() throws Exception { + String header = "Subject: a\r\n b\r\n\r\n"; + InternetHeaders ih = new InternetHeaders( + new AsciiStringInputStream(header)); + assertEquals(1, ih.getHeader("Subject").length); + assertEquals("a\r\n b", ih.getHeader("Subject")[0]); + } + + /** + * Test that a whitespace line at the beginning is ignored. + */ + @Test + public void testInitialWhitespaceLineConstructor() throws Exception { + InternetHeaders ih = new InternetHeaders( + new AsciiStringInputStream(initialWhitespaceHeader)); + testInitialWhitespaceLine(ih); + } + + /** + * Test that a whitespace line at the beginning is ignored. + */ + @Test + public void testInitialWhitespaceLineLoad() throws Exception { + InternetHeaders ih = new InternetHeaders(); + ih.load(new AsciiStringInputStream(initialWhitespaceHeader)); + testInitialWhitespaceLine(ih); + } + + private void testInitialWhitespaceLine(InternetHeaders ih) + throws Exception { + assertEquals(1, ih.getHeader("Subject").length); + assertEquals("test", ih.getHeader("Subject")[0]); + Enumeration<Header> e = ih.getAllHeaders(); + while (e.hasMoreElements()) { + Header h = e.nextElement(); + assertEquals("Subject", h.getName()); + assertEquals("test", h.getValue()); + } + } + + /** + * Test that a continuation line at the beginning is handled. + */ + @Test + public void testInitialContinuationLineConstructor() throws Exception { + InternetHeaders ih = new InternetHeaders( + new AsciiStringInputStream(initialContinuationHeader)); + testInitialContinuationLine(ih); + } + + /** + * Test that a continuation line at the beginning is handled. + */ + @Test + public void testInitialContinuationLineLoad() throws Exception { + InternetHeaders ih = new InternetHeaders(); + ih.load(new AsciiStringInputStream(initialContinuationHeader)); + testInitialContinuationLine(ih); + } + + private void testInitialContinuationLine(InternetHeaders ih) + throws Exception { + assertEquals(1, ih.getHeader("Subject").length); + assertEquals("test", ih.getHeader("Subject")[0]); + Enumeration<Header> e = ih.getAllHeaders(); + while (e.hasMoreElements()) { + Header h = e.nextElement(); + assertEquals("Subject", h.getName()); + assertEquals("test", h.getValue()); + } + } +}
diff --git a/mail/src/test/java/javax/mail/internet/MailDateFormatTest.java b/mail/src/test/java/javax/mail/internet/MailDateFormatTest.java new file mode 100644 index 0000000..c421609 --- /dev/null +++ b/mail/src/test/java/javax/mail/internet/MailDateFormatTest.java
@@ -0,0 +1,516 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.text.DateFormat; +import java.text.DateFormatSymbols; +import java.text.FieldPosition; +import java.text.NumberFormat; +import java.text.ParseException; +import java.text.ParsePosition; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.Locale; +import java.util.TimeZone; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import org.junit.Test; + +/** + * Test MailDateFormat: formatting and parsing of dates as specified by + * <a href="http://www.ietf.org/rfc/rfc2822.txt">RFC 2822</a>. + * + * @author Anthony Vanelverdinghe + */ +public class MailDateFormatTest { + + /* + * Serialization + */ + @Test + public void mustRetainBinaryCompatibility() + throws IOException, ClassNotFoundException, ParseException { + verifySerialization("MailDateFormat_old.ser"); + verifySerialization("MailDateFormat_new.ser"); + } + + private void verifySerialization(String resourceName) + throws IOException, ClassNotFoundException { + MailDateFormat fmt = readSerializedFormat(resourceName); + + assertThat(fmt.getTimeZone().getID(), is("America/Los_Angeles")); + + Date date = mustPass(fmt, "Thu, 1 Jan 2015 00:00:00 +0000"); + assertThatDate(date, "Thu, 1 Jan 2015 00:00:00 +0000 (UTC)"); + + assertThat(fmt.format(date), + is("Wed, 31 Dec 2014 16:00:00 -0800 (PST)")); + } + + private MailDateFormat readSerializedFormat(String resourceName) + throws ClassNotFoundException, IOException { + InputStream resource = null; + ObjectInputStream in = null; + try { + resource = this.getClass().getResourceAsStream(resourceName); + in = new ObjectInputStream(resource); + return (MailDateFormat) in.readObject(); + } finally { + if (in != null) { + in.close(); + } + } + } + + /* + * Formatting + parsing + */ + @Test + public void mustSucceedRoundTrip() throws ParseException { + Date date = new Date(1341100798000L); // milliseconds must be 0 + SimpleDateFormat fmt = getDefault(); + assertThat(fmt.parse(fmt.format(date)), is(date)); + } + + /* + * Formatting + */ + @Test() + public void formatMustThrowNpeForNullArgs() { + for (int mask = 0; mask < 7; mask++) { + try { + Date date = (mask & 1) == 1 + ? new Date() : null; + StringBuffer toAppendTo = (mask & 2) == 2 + ? new StringBuffer() : null; + FieldPosition pos = (mask & 4) == 4 + ? new FieldPosition(0) : null; + getDefault().format(date, toAppendTo, pos); + fail("No NullPointerException thrown for mask = " + mask); + } catch (NullPointerException e) { + } + } + } + + @Test + public void mustFormatRfc2822WithOptionalCfws() { + Date date = mustPass(getDefault(), "1 Jan 2015 00:00 +0000"); + assertThatDate(date, "Thu, 1 Jan 2015 00:00:00 +0000 (UTC)"); + } + + @Test + public void mustUseTimeZoneForFormatting() { + String input = "1 Jan 2015 00:00 +0100"; + SimpleDateFormat fmt = getDefault(); + Date date = mustPass(fmt, input); + fmt.setTimeZone(TimeZone.getTimeZone("Etc/GMT+8")); + assertThat(fmt.format(date), + is("Wed, 31 Dec 2014 15:00:00 -0800 (GMT-08:00)")); + } + + /* + * Parsing + */ + @Test + public void parseMustThrowNpeForNullArgs() { + for (int mask = 0; mask < 3; mask++) { + try { + String source = (mask & 1) == 1 + ? "1 Jan 2015 00:00 +0100" : null; + ParsePosition pos = (mask & 2) == 2 + ? new ParsePosition(0) : null; + getDefault().parse(source, pos); + fail("No NullPointerException thrown for mask = " + mask); + } catch (NullPointerException e) { + } + } + } + + @Test + public void mustReturnOnInvalidParsePosition() { + assertNull(getDefault().parse("", new ParsePosition(-1))); + assertNull(getDefault().parse("", new ParsePosition(0))); + assertNull(getDefault().parse("", new ParsePosition(1))); + } + + @Test + public void mustParseRfc2822() { + mustPass(getDefault(), "Thu, 1 Jan 2015 00:00:00 +0000"); + mustFail(getStrict(), "foo", 0); + mustFail(getLenient(), "foo", 3); + } + + @Test + public void mustRetainTimeZoneForBackwardCompatibility() { + // though java.text.DateFormat::parse(String, ParsePosition) specifies: + // "This parsing operation uses the calendar to produce a Date. As a + // result, the calendar's date-time fields and the TimeZone value + // may have been overwritten, depending on subclass implementations. + // Any TimeZone value that has previously been set by a call to + // setTimeZone may need to be restored for further operations." + // we must retain the TimeZone value for backward compatibility reasons + SimpleDateFormat fmt = getDefault(); + fmt.setTimeZone(TimeZone.getTimeZone("Europe/Brussels")); + mustPass(fmt, "1 Jan 2015 00:00 +0000"); + assertThat(fmt.getTimeZone(), + is(TimeZone.getTimeZone("Europe/Brussels"))); + } + + @Test + public void mustUseLeniencyForParsing() { + String lenientInput = "1-Jan-2015 00:00 +0000"; + String strictInput = "1 Jan 2015 00:00 +0000"; + + SimpleDateFormat fmt = getStrict(); + mustFail(fmt, lenientInput, 1); + mustPass(fmt, strictInput); + + fmt.setLenient(true); + mustPass(fmt, lenientInput); + mustPass(fmt, strictInput); + + fmt.setLenient(false); + mustFail(fmt, lenientInput, 1); + mustPass(fmt, strictInput); + } + + @Test + public void mustReportCorrectErrorIndex() { + mustFail(getDefault(), "1 Juk 2015 00:00 +0000", 2); + mustFail(getStrict(), "1\r\nJan 2015 00:00 +0000", 1); + mustFail(getStrict(), "1 Jan 201 00:00 +0000", 6); + } + + @Test + public void lenientMustBeBackwardCompatible() { + mustPass(getLenient(), "Thu, 1 Jan 2015 12:00 +0000"); + mustPass(getLenient(), "32 dEC 2015 12:00 +0099"); + mustPass(getLenient(), "fooFri,999-Jan-99999999 99:99:99+2399"); + mustPass(getLenient(), "fooFri,999-Jan-99999999 99:99:99+2399"); + mustPass(getLenient(), "fooFri ,999Jan99999999 99:99:99+2399"); + mustPass(getLenient(), "foo31-mAY-1\r3:3:3+"); + } + + @Test + public void strictMustRejectInvalidBegin() { + mustFail(getStrict(), "foo1 Jan 2015 00:00 +0000", 0); + } + + @Test + public void strictMustRejectSemanticallyInvalidDate() { + mustFail(getStrict(), "Fri, 1 Jan 2015 00:00 +0000", 27); + } + + /* + * Parsing - range constraints + */ + @Test + public void mustRejectFieldsWithTooManyDigits() { + mustFail(getDefault(), "1234 Jan 2015 00:00 +0000", 0); + mustFail(getDefault(), "1 Jan 123456789 00:00 +0000", 6); + mustFail(getDefault(), "1 Jan 2015 123:00 +0000", 11); + mustFail(getDefault(), "1 Jan 2015 00:123 +0000", 14); + mustFail(getDefault(), "1 Jan 2015 00:00:123 +0000", 17); + } + + @Test + public void mustCountLeadingZeroesForTooManyDigits() { + mustFail(getDefault(), "0000000000001 Jan 2015 00:00 +0000", 0); + } + + /* + * Parsing - overflow + */ + @Test + public void mustFailFastIfDayContainsTooManyDigits() { + mustFail(getLenient(), + "100000000000000000000000000000001 Jan 2015 00:00 +0000", 0); + } + + @Test + public void mustFailFastIfYearContainsTooManyDigits() { + mustFail(getLenient(), + "1 Jan 100000000000000000000000000000002015 00:00 +0000", 6); + } + + @Test + public void mustFailIfYearIsTooBigForGregorianCalendar() { + int tooBig = 999999999; + assertTrue(tooBig > new GregorianCalendar().getMaximum(Calendar.YEAR)); + mustFail(getLenient(), "1 Jan " + tooBig + " 00:00 +0000", 6); + } + + @Test + public void mustFailOnIntegerOverflow() { + mustFail(getLenient(), "456378941961 Jan 2015 00:00 +0000", 0); + } + + /* + * Parsing - FWS (folding white space) + */ + @Test + public void mustFailOrSkipCfws() { + String input = "(3 Jan 2015 00:00 +0000) 1 Jan 2015 00:00 +0000"; + try { + // NOTE this test fails with getLenient(), + // since lenient parsing must remain backward compatible + Date date = getStrict().parse(input); + assertThatDate(date, "Thu, 1 Jan 2015 00:00:00 +0000 (UTC)"); + } catch (ParseException ignored) { + assertTrue("Not supporting CFWS is allowed", true); + } + } + + @Test + public void lenientMustAcceptEmptyFwsIffUnambiguous() { + mustPass(getLenient(), "Thu,1Jan2015 12:00+0000"); + mustFail(getLenient(), "1 Jan 1015123456:00 +0000", 6); + } + + @Test + public void lenientMustRejectInconsistentMonthFws() { + mustFail(getLenient(), "1-Jan 2015 00:00 +0000", 5); + } + + @Test + public void strictMustRejectInvalidFws() { + mustFail(getStrict(), "\r\n\r\n 1 Jan 2015 00:00 +0000", 0); + mustFail(getStrict(), "\r\n1 Jan 2015 00:00 +0000", 0); + mustFail(getStrict(), "\n 1 Jan 2015 00:00 +0000", 0); + mustFail(getStrict(), " \n 1 Jan 2015 00:00 +0000", 1); + mustFail(getStrict(), " \r\n1 Jan 2015 00:00 +0000", 0); + } + + /* + * Parsing - day-name and month-name + */ + @Test + public void mustAcceptValidDayMonthNames() { + SimpleDateFormat fmt = getDefault(); + for (String dayMonth : new String[]{ + "Mon, 5 Jan", "Tue, 3 Feb", "Wed, 4 Mar", "Thu, 2 Apr", + "Fri, 1 May", "Sat, 6 Jun", "Sun, 5 Jul", "Sat, 1 Aug", + "Tue, 1 Sep", "Thu, 1 Oct", "Sun, 1 Nov", "Tue, 1 Dec" + }) { + mustPass(fmt, dayMonth + " 2015 00:00:00 +0000"); + } + } + + @Test + public void strictMustMatchDayNameCaseSensitive() { + mustFail(getStrict(), "tHU, 1 Jan 2015 00:00 +0000", 0); + } + + @Test + public void strictMustMatchMonthNameCaseSensitive() { + mustFail(getStrict(), "1 jAN 2015 00:00 +0000", 2); + } + + /* + * Parsing - year + */ + @Test + public void lenientMustAcceptSingleDigitYears() { + mustPass(getLenient(), "1 Jan 1 00:00 +0000"); + } + + @Test + public void lenientMustAcceptYearsBetween1000and1899() { + mustPass(getLenient(), "1 Jan 999 00:00 +0000"); + mustPass(getLenient(), "1 Jan 1000 00:00 +0000"); + mustPass(getLenient(), "1 Jan 1899 00:00 +0000"); + mustPass(getStrict(), "1 Jan 1900 00:00 +0000"); + } + + @Test + public void lenientMustAdd1900To3DigitYear() { + Date date = mustPass(getLenient(), "1 Jul 900 00:00 +0000"); + assertThatDate(date, "Sat, 1 Jul 2800 00:00:00 +0000 (UTC)"); + } + + @Test + public void strictMustRejectAllYearsLt1900() { + mustFail(getStrict(), "1 Jan 000001 00:00 +0000", 6); + mustFail(getStrict(), "1 Jan 999 00:00 +0000", 6); + mustFail(getStrict(), "1 Jan 1000 00:00 +0000", 6); + mustFail(getStrict(), "1 Jan 1899 00:00 +0000", 6); + mustPass(getStrict(), "1 Jan 1900 00:00 +0000"); + } + + /* + * Parsing - second + */ + @Test + public void mustParseLeapSecondAsJsr310() { + // JSR-310 replaces 60 with 59 + Date date = mustPass(getDefault(), "30 Jun 2012 23:59:60 +0000"); + // Date.from(ISO_INSTANT.parse("2012-06-30T23:59:60Z", Instant::from)) + assertThat(date, is(new Date(1341100799000L))); + } + + /* + * Parsing - zone + */ + @Test + public void mustParseNegativeZeroesZoneAsUtc() { + Date date = mustPass(getDefault(), "1 Jan 2015 00:00 -0000"); + assertThatDate(date, "Thu, 1 Jan 2015 00:00:00 +0000 (UTC)"); + } + + @Test + public void mustCorrectlyParseInputWithTrailingDigits() { + Date date = mustPass(getStrict(), "1 Jan 2015 00:00 -001530"); + assertThatDate(date, "Thu, 1 Jan 2015 00:15:00 +0000 (UTC)"); + } + + @Test + public void mustAcceptZoneWithExtremeOffset() { + Date date = mustPass(getStrict(), "1 Jan 2015 00:00 +9959"); + Date equivalentWithoutOffset = mustPass(getStrict(), + "27 Dec 2014 20:01 +0000"); + assertThat(date, is(equivalentWithoutOffset)); + + date = mustPass(getStrict(), "1 Jan 2015 00:00 -9959"); + equivalentWithoutOffset = mustPass(getStrict(), + "5 Jan 2015 03:59 +0000"); + assertThat(date, is(equivalentWithoutOffset)); + } + + @Test + public void lenientMustAcceptNonMilitaryZoneNames() { + SimpleDateFormat fmt = getLenient(); + for (String zoneName : new String[]{"GMT", "UT", + "EDT", "EST", "CDT", "CST", "MDT", "MST", "PDT", "PST"}) { + mustPass(fmt, "1 Jan 2015 00:00 " + zoneName); + } + } + + @Test + public void strictMustMatchZone() { + mustFail(getStrict(), "1 Jan 2015 00:00", 16); + } + + @Test + public void strictMustRejectZoneOffsetMinutesGreaterThan60() { + mustFail(getStrict(), "1 Jan 2015 00:00 +0060", 17); + } + + /* + * Unsupported methods. When possible, the test also demonstrates + * why invoking the method must be prohibited. + */ + @Test(expected = UnsupportedOperationException.class) + public void mustProhibitSetCalendar() { + getDefault().setCalendar(Calendar.getInstance()); + } + + @Test(expected = UnsupportedOperationException.class) + public void mustProhibitSetNumberFormat() { + getDefault().setNumberFormat(NumberFormat.getInstance()); + } + + @Test(expected = UnsupportedOperationException.class) + public void mustProhibitApplyLocalizedPattern() { + SimpleDateFormat fmt = getStrict(); + fmt.applyLocalizedPattern("yyyy"); + Date date = mustPass(fmt, "1 Jan 2015 00:00:00 +0000"); + assertThat(fmt.format(date), is("2015")); + } + + @Test(expected = UnsupportedOperationException.class) + public void mustProhibitApplyPattern() { + SimpleDateFormat fmt = getStrict(); + fmt.applyPattern("yyyy"); + Date date = mustPass(fmt, "1 Jan 2015 00:00:00 +0000"); + assertThat(fmt.format(date), is("2015")); + } + + @Test(expected = UnsupportedOperationException.class) + public void mustProhibitGet2DigitYearStart() { + getDefault().get2DigitYearStart(); + } + + @Test(expected = UnsupportedOperationException.class) + public void mustProhibitSet2DigitYearStart() { + getDefault().set2DigitYearStart(new Date()); + } + + @Test(expected = UnsupportedOperationException.class) + public void mustProhibitSetDateFormatSymbols() { + SimpleDateFormat fmt = getStrict(); + fmt.setDateFormatSymbols(new DateFormatSymbols(Locale.FRENCH)); + Date date = mustPass(fmt, "1 Jan 2015 00:00:00 +0000"); + assertThatDate(date, "jeu., 1 janv. 2015 00:00:00 +0000 (UTC)"); + } + + /* + * Helper methods + */ + private static SimpleDateFormat getDefault() { + return new MailDateFormat(); + } + + private static SimpleDateFormat getLenient() { + SimpleDateFormat fmt = getDefault(); + fmt.setLenient(true); + return fmt; + } + + private static SimpleDateFormat getStrict() { + SimpleDateFormat fmt = getDefault(); + fmt.setLenient(false); + return fmt; + } + + private void assertThatDate(Date date, String formattedDate) { + SimpleDateFormat fmt = getDefault(); + fmt.setTimeZone(TimeZone.getTimeZone("UTC")); + assertThat(fmt.format(date), is(formattedDate)); + } + + private Date mustPass(DateFormat fmt, String input) { + try { + Date date = fmt.parse(input); + assertNotNull(date); + return date; + } catch (ParseException e) { + fail(String.format("'%s' is a valid date in %s mode", input, + fmt.isLenient() ? "lenient" : "strict")); + throw new AssertionError(); + } + } + + private void mustFail(DateFormat fmt, String input, int errorOffset) { + try { + Date result = fmt.parse(input); + fail(String.format("'%s' is not a valid date in %s mode", input, + fmt.isLenient() ? "lenient" : "strict")); + } catch (ParseException e) { + assertThat(e.getErrorOffset(), is(errorOffset)); + } + } + +}
diff --git a/mail/src/test/java/javax/mail/internet/MimeBodyPartTest.java b/mail/src/test/java/javax/mail/internet/MimeBodyPartTest.java new file mode 100644 index 0000000..8c7875a --- /dev/null +++ b/mail/src/test/java/javax/mail/internet/MimeBodyPartTest.java
@@ -0,0 +1,204 @@ +/* + * Copyright (c) 2011, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import java.io.*; +import java.nio.charset.Charset; +import java.util.Properties; + +import javax.activation.DataHandler; + +import javax.mail.*; + +import com.sun.mail.test.AsciiStringInputStream; + +import org.junit.*; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertArrayEquals; + +/** + * Test the MimeBodyPart class. + */ +public class MimeBodyPartTest { + + private static String[] languages = new String[] { + "language1", "language2", "language3", "language4", "language5", + "language6", "language7", "language8", "language9", "language10", + "language11", "language12", "language13", "language14", "language15" + }; + + /** + * Test that the Content-Language header is properly folded + * if there are a lot of languages. + */ + @Test + public void testContentLanguageFold() throws Exception { + MimeBodyPart mbp = new MimeBodyPart(); + mbp.setContentLanguage(languages); + String header = mbp.getHeader("Content-Language", ","); + assertTrue(header.indexOf("\r\n") > 0); + + String[] langs = mbp.getContentLanguage(); + assertArrayEquals(languages, langs); + } + + /** + * Test that copying a DataHandler from one message to another + * has the desired effect. + */ + @Test + public void testCopyDataHandler() throws Exception { + Session s = Session.getInstance(new Properties()); + // create a message and extract the DataHandler for a part + MimeMessage orig = createMessage(s); + MimeMultipart omp = (MimeMultipart)orig.getContent(); + MimeBodyPart obp = (MimeBodyPart)omp.getBodyPart(0); + DataHandler dh = obp.getDataHandler(); + // create a new message and use the DataHandler + MimeMessage msg = new MimeMessage(s); + MimeMultipart mp = new MimeMultipart(); + MimeBodyPart mbp = new MimeBodyPart(); + mbp.setDataHandler(dh); + mp.addBodyPart(mbp); + msg.setContent(mp); + // depend on copy constructor streaming the data + msg = new MimeMessage(msg); + mp = (MimeMultipart)msg.getContent(); + mbp = (MimeBodyPart)mp.getBodyPart(0); + assertEquals("text/x-test", mbp.getContentType()); + assertEquals("quoted-printable", mbp.getEncoding()); + assertEquals("test part", getString(mbp.getInputStream())); + } + + /** + * Test that copying a DataHandler from one message to another + * by setting the "dh" field in a subclass has the desired effect. + */ + @Test + public void testSetDataHandler() throws Exception { + Session s = Session.getInstance(new Properties()); + // create a message and extract the DataHandler for a part + MimeMessage orig = createMessage(s); + MimeMultipart omp = (MimeMultipart)orig.getContent(); + MimeBodyPart obp = (MimeBodyPart)omp.getBodyPart(0); + final DataHandler odh = obp.getDataHandler(); + // create a new message and use the DataHandler + MimeMessage msg = new MimeMessage(s); + MimeMultipart mp = new MimeMultipart(); + MimeBodyPart mbp = new MimeBodyPart() { + { dh = odh; } + }; + mp.addBodyPart(mbp); + msg.setContent(mp); + // depend on copy constructor streaming the data + msg = new MimeMessage(msg); + mp = (MimeMultipart)msg.getContent(); + mbp = (MimeBodyPart)mp.getBodyPart(0); + assertEquals("text/x-test", mbp.getContentType()); + assertEquals("quoted-printable", mbp.getEncoding()); + assertEquals("test part", getString(mbp.getInputStream())); + } + + /** + * Test that a MimeBodyPart created from a stream with unencoded data + * will have the data be encoded when the data is copied to another + * MimeBodyPart by copying the DataHandler. + */ + @Test + public void testEncodingCopiedDataHandler() throws Exception { + String part = + "Content-Type: application/x-test\n" + + "\n" + + "\u0001\u0002\u0003" + + "\n"; + MimeBodyPart mbp = new MimeBodyPart(new AsciiStringInputStream(part)); + MimeBodyPart mbp2 = new MimeBodyPart() { + @Override + public void setDataHandler(DataHandler dh) + throws MessagingException { + super.setDataHandler(dh); + updateHeaders(); + } + }; + mbp2.setDataHandler(mbp.getDataHandler()); + assertEquals("base64", mbp2.getEncoding()); + // ensure the data is correct by reading the first byte + InputStream in = mbp2.getInputStream(); + assertEquals(1, in.read()); + in.close(); + } + + /** + * Test that isMimeType does something reasonable even if the + * Content-Type header can't be parsed because of a bad parameter. + */ + @Test + public void testIsMimeTypeBadParameter() throws Exception { + String part = + "Content-Type: application/x-test; type=a/b\n" + + "\n" + + "\n"; + MimeBodyPart mbp = new MimeBodyPart(new AsciiStringInputStream(part)); + assertTrue("complete MIME type", mbp.isMimeType("application/x-test")); + assertTrue("pattern MIME type", mbp.isMimeType("application/*")); + assertFalse("wrong MIME type", mbp.isMimeType("application/test")); + } + + /** + * Test that a Content-Transfer-Encoding header with no value doesn't + * cause an IOException. + */ + @Test + public void testEmptyContentTransferEncoding() throws Exception { + String part = + "Content-Type: text/plain; charset=\"us-ascii\"\n" + + "Content-Transfer-Encoding: \n" + + "\n" + + "test" + + "\n"; + MimeBodyPart mbp = new MimeBodyPart(new AsciiStringInputStream(part)); + assertEquals("empty C-T-E value", null, mbp.getEncoding()); + assertEquals("empty C-T-E data", "test\n", mbp.getContent()); + } + + + private static MimeMessage createMessage(Session s) + throws MessagingException { + String content = + "Mime-Version: 1.0\n" + + "Subject: Example\n" + + "Content-Type: multipart/mixed; boundary=\"-\"\n" + + "\n" + + "preamble\n" + + "---\n" + + "Content-Type: text/x-test\n" + + "Content-Transfer-Encoding: quoted-printable\n" + + "\n" + + "test part\n" + + "\n" + + "-----\n"; + + return new MimeMessage(s, new AsciiStringInputStream(content)); + } + + private static String getString(InputStream is) throws IOException { + BufferedReader r = new BufferedReader(new InputStreamReader(is)); + return r.readLine(); + } +}
diff --git a/mail/src/test/java/javax/mail/internet/MimeBodyPartTestSuite.java b/mail/src/test/java/javax/mail/internet/MimeBodyPartTestSuite.java new file mode 100644 index 0000000..4e5e871 --- /dev/null +++ b/mail/src/test/java/javax/mail/internet/MimeBodyPartTestSuite.java
@@ -0,0 +1,36 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import org.junit.runner.RunWith; +import org.junit.runners.Suite.SuiteClasses; + +import com.sun.mail.test.ClassLoaderSuite; +import com.sun.mail.test.ClassLoaderSuite.TestClass; + +/** + * Suite of MimeBodyPart tests that need to be run in a separate class loader. + */ +@RunWith(ClassLoaderSuite.class) +@TestClass(MimeBodyPart.class) +@SuiteClasses( { + NoEncodeFileName.class, + EncodeFileName.class, + EncodeFileNameNoEncodeParameters.class +}) +public class MimeBodyPartTestSuite { +}
diff --git a/mail/src/test/java/javax/mail/internet/MimeMessageTest.java b/mail/src/test/java/javax/mail/internet/MimeMessageTest.java new file mode 100644 index 0000000..c23f2bc --- /dev/null +++ b/mail/src/test/java/javax/mail/internet/MimeMessageTest.java
@@ -0,0 +1,218 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import com.sun.mail.test.AsciiStringInputStream; +import java.io.*; +import java.nio.charset.Charset; +import java.util.Properties; +import java.util.Enumeration; + +import javax.activation.DataHandler; + +import javax.mail.*; +import static javax.mail.Message.RecipientType.*; +import static javax.mail.internet.MimeMessage.RecipientType.*; + +import org.junit.*; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * Test MimeMessage methods. + * + * XXX - just a beginning... + * + * @author Bill Shannon + */ +public class MimeMessageTest { + + private static final Session s = Session.getInstance(new Properties()); + + /** + * Test that setRecipients with a null string address removes the header. + * (Bug 7021190) + */ + @Test + public void testSetRecipientsStringNull() throws Exception { + String addr = "joe@example.com"; + MimeMessage m = new MimeMessage(s); + m.setRecipients(TO, addr); + assertEquals("To: is set", addr, m.getRecipients(TO)[0].toString()); + m.setRecipients(TO, (String)null); + assertArrayEquals("To: is removed", null, m.getRecipients(TO)); + } + + /** + * Test that setRecipient with a null string address removes the header. + * (Bug 7536) + */ + @Test + public void testSetRecipientStringNull() throws Exception { + String addr = "joe@example.com"; + MimeMessage m = new MimeMessage(s); + m.setRecipient(TO, new InternetAddress(addr)); + assertEquals("To: is set", addr, m.getRecipients(TO)[0].toString()); + m.setRecipient(TO, (Address)null); + assertArrayEquals("To: is removed", null, m.getRecipients(TO)); + } + + /** + * Test that setFrom with an address containing a newline is folded + * properly. + * (Bug 7529) + */ + @Test + public void testSetFromFold() throws Exception { + InternetAddress addr = new InternetAddress("joe@bad.com", "Joe\r\nBad"); + MimeMessage m = new MimeMessage(s); + m.setFrom(addr); + assertEquals("Joe\r\n Bad <joe@bad.com>", m.getHeader("From", null)); + } + + /** + * Test that setSender with an address containing a newline is folded + * properly. + * (Bug 7529) + */ + @Test + public void testSetSenderFold() throws Exception { + InternetAddress addr = new InternetAddress("joe@bad.com", "Joe\r\nBad"); + MimeMessage m = new MimeMessage(s); + m.setSender(addr); + assertEquals("Joe\r\n Bad <joe@bad.com>", m.getHeader("Sender", null)); + } + + /** + * Test that setRecipient with a newsgroup address containing a newline is + * handled properly. + * (Bug 7529) + */ + @Test + public void testSetNewsgroupWhitespace() throws Exception { + NewsAddress addr = new NewsAddress("alt.\r\nbad"); + MimeMessage m = new MimeMessage(s); + m.setRecipient(NEWSGROUPS, addr); + assertEquals("alt.bad", m.getHeader("Newsgroups", null)); + } + + /** + * Test that setRecipients with many newsgroup addresses is folded properly. + * (Bug 7529) + */ + @Test + public void testSetNewsgroupFold() throws Exception { + NewsAddress[] longng = NewsAddress.parse( + "alt.loooooooooooooooooooooooooooooooooooooooooooooooooong," + + "alt.verylongggggggggggggggggggggggggggggggggggggggggggggg"); + MimeMessage m = new MimeMessage(s); + m.setRecipients(NEWSGROUPS, longng); + assertTrue(m.getHeader("Newsgroups", null).indexOf("\r\n\t") > 0); + } + + /** + * Test that newsgroups can be set and read back (even if folded). + */ + @Test + public void testSetGetNewsgroups() throws Exception { + NewsAddress[] longng = NewsAddress.parse( + "alt.loooooooooooooooooooooooooooooooooooooooooooooooooong," + + "alt.verylongggggggggggggggggggggggggggggggggggggggggggggg"); + MimeMessage m = new MimeMessage(s); + m.setRecipients(NEWSGROUPS, longng); + assertArrayEquals(longng, m.getRecipients(NEWSGROUPS)); + } + + /** + * Test that copying a DataHandler from one message to another + * has the desired effect. + */ + @Test + public void testCopyDataHandler() throws Exception { + Session s = Session.getInstance(new Properties()); + // create a message and extract the DataHandler + MimeMessage orig = createMessage(s); + DataHandler dh = orig.getDataHandler(); + // create a new message and use the DataHandler + MimeMessage msg = new MimeMessage(s); + msg.setDataHandler(dh); + // depend on copy constructor streaming the data + msg = new MimeMessage(msg); + assertEquals("text/x-test", msg.getContentType()); + assertEquals("quoted-printable", msg.getEncoding()); + assertEquals("test message", getString(msg.getInputStream())); + } + + /** + * Test that copying a DataHandler from one message to another + * by setting the "dh" field in a subclass has the desired effect. + */ + @Test + public void testSetDataHandler() throws Exception { + Session s = Session.getInstance(new Properties()); + // create a message and extract the DataHandler for a part + MimeMessage orig = createMessage(s); + final DataHandler odh = orig.getDataHandler(); + // create a new message and use the DataHandler + MimeMessage msg = new MimeMessage(s) { + { dh = odh; } + }; + // depend on copy constructor streaming the data + msg = new MimeMessage(msg); + assertEquals("text/x-test", msg.getContentType()); + assertEquals("quoted-printable", msg.getEncoding()); + assertEquals("test message", getString(msg.getInputStream())); + } + + /** + * Test that address headers account for the header length when folding. + */ + @Test + public void testAddressHeaderFolding() throws Exception { + Session s = Session.getInstance(new Properties()); + MimeMessage msg = new MimeMessage(s); + InternetAddress[] addrs = InternetAddress.parse( + "long-address1@example.com, long-address2@example.com, joe@foobar.com"); + msg.setReplyTo(addrs); // use Reply-To because it's a long header name + Enumeration<String> e + = msg.getMatchingHeaderLines(new String[] { "Reply-To" }); + String line = e.nextElement(); + int npos = line.indexOf("\r"); + // was the line folded where we expected? + assertTrue("Header folded", + npos > 9 && npos <= 77 && npos < line.length()); + } + + private static MimeMessage createMessage(Session s) + throws MessagingException { + String content = + "Mime-Version: 1.0\n" + + "Subject: Example\n" + + "Content-Type: text/x-test\n" + + "Content-Transfer-Encoding: quoted-printable\n" + + "\n" + + "test message\n"; + + return new MimeMessage(s, new AsciiStringInputStream(content)); + } + + private static String getString(InputStream is) throws IOException { + BufferedReader r = new BufferedReader(new InputStreamReader(is)); + return r.readLine(); + } +}
diff --git a/mail/src/test/java/javax/mail/internet/MimeMessageTestSuite.java b/mail/src/test/java/javax/mail/internet/MimeMessageTestSuite.java new file mode 100644 index 0000000..578918e --- /dev/null +++ b/mail/src/test/java/javax/mail/internet/MimeMessageTestSuite.java
@@ -0,0 +1,34 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import org.junit.runner.RunWith; +import org.junit.runners.Suite.SuiteClasses; + +import com.sun.mail.test.ClassLoaderSuite; +import com.sun.mail.test.ClassLoaderSuite.TestClass; + +/** + * Suite of MimeMessage tests that need to be run in a separate class loader. + */ +@RunWith(ClassLoaderSuite.class) +@TestClass(MimeMessage.class) +@SuiteClasses( { + AllowEncodedMessages.class +}) +public class MimeMessageTestSuite { +}
diff --git a/mail/src/test/java/javax/mail/internet/MimeMultipartParseTest.java b/mail/src/test/java/javax/mail/internet/MimeMultipartParseTest.java new file mode 100644 index 0000000..e8c21a9 --- /dev/null +++ b/mail/src/test/java/javax/mail/internet/MimeMultipartParseTest.java
@@ -0,0 +1,145 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import java.util.*; +import java.io.*; +import javax.mail.*; +import javax.mail.event.*; +import javax.mail.internet.*; +import javax.mail.util.*; +import javax.activation.*; + +import org.junit.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/* + * Test multipart parsing. + * + * @author Bill Shannon + */ + +public class MimeMultipartParseTest { + private static Session session = + Session.getInstance(new Properties(), null); + + private static final int maxsize = 10000; + private static final String data = + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + + @Test + public void testParse() throws Exception { + test(false); + } + + @Test + public void testParseShared() throws Exception { + test(true); + } + + /* + * Test a few potential boundary cases, then test a range. + * This is a compromise to make the run time of this test reasonable, + * although it still takes about 30 seconds, which is on the long side + * for a unit test. + */ + public void test(boolean shared) throws Exception { + testMessage(1, shared); + testMessage(2, shared); + testMessage(62, shared); + testMessage(63, shared); + testMessage(64, shared); + testMessage(65, shared); + testMessage(1023, shared); + testMessage(1024, shared); + testMessage(1025, shared); + for (int size = 8100; size <= maxsize; size++) + testMessage(size, shared); + } + + public void testMessage(int size, boolean shared) throws Exception { + //System.out.println("SIZE: " + size); + /* + * Construct a multipart message with a part of the + * given size. + */ + MimeMessage msg = new MimeMessage(session); + msg.setFrom(new InternetAddress("me@example.com")); + msg.setSubject("test multipart parsing"); + msg.setSentDate(new Date(0)); + MimeBodyPart mbp1 = new MimeBodyPart(); + mbp1.setText("main text\n"); + MimeBodyPart mbp3 = new MimeBodyPart(); + mbp3.setText("end text\n"); + MimeBodyPart mbp2 = new MimeBodyPart(); + byte[] part = new byte[size]; + for (int i = 0; i < size; i++) { + int j = i % 64; + if (j == 62) + part[i] = (byte)'\r'; + else if (j == 63) + part[i] = (byte)'\n'; + else + part[i] = (byte)data.charAt((j + i / 64) % 62); + } + mbp2.setDataHandler(new DataHandler( + new ByteArrayDataSource(part, "text/plain"))); + + MimeMultipart mp = new MimeMultipart(); + mp.addBodyPart(mbp1); + mp.addBodyPart(mbp2); + mp.addBodyPart(mbp3); + msg.setContent(mp); + msg.saveChanges(); + + /* + * Write the message out to a byte array. + */ + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + msg.writeTo(bos); + bos.close(); + byte[] buf = bos.toByteArray(); + + /* + * Construct a new message to parse the bytes. + */ + msg = new MimeMessage(session, shared ? + new SharedByteArrayInputStream(buf) : + new ByteArrayInputStream(buf)); + + // verify that the part content is correct + mp = (MimeMultipart)msg.getContent(); + mbp2 = (MimeBodyPart)mp.getBodyPart(1); + InputStream is = mbp2.getInputStream(); + int k = 0; + int c; + while ((c = is.read()) >= 0) { + int j = k % 64; + byte e; + if (j == 62) + e = (byte)'\r'; + else if (j == 63) + e = (byte)'\n'; + else + e = (byte)data.charAt((j + k / 64) % 62); + Assert.assertEquals("Size " + size + " at byte " + k, e, c); + k++; + } + Assert.assertEquals("Expected size", size, k); + } +}
diff --git a/mail/src/test/java/javax/mail/internet/MimeMultipartPreambleTest.java b/mail/src/test/java/javax/mail/internet/MimeMultipartPreambleTest.java new file mode 100644 index 0000000..7528444 --- /dev/null +++ b/mail/src/test/java/javax/mail/internet/MimeMultipartPreambleTest.java
@@ -0,0 +1,121 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import java.io.ByteArrayInputStream; +import java.util.Properties; + +import javax.mail.BodyPart; +import javax.mail.Session; +import javax.mail.internet.MimeMessage; +import javax.mail.internet.MimeMultipart; + +import org.junit.Test; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +/** + * Test the MIME multipart messages are parsed correctly and the + * correct preamble is returned no matter what line terminators + * are used. This test found some bugs in the way LineInputStream + * handled different line terminators. + * + * @author trejkaz@kenai.com + */ +public class MimeMultipartPreambleTest { + @SuppressWarnings({"SingleCharacterStringConcatenation"}) + private String THREE_PART_MAIL = + "From: user1@example.com\n" + + "To: user2@example.com\n" + + "Subject: Receipts\n" + + "Date: Wed, 14 Jul 2010 19:25:30 +1000\n" + + "MIME-Version: 1.0\n" + + "Content-Type: multipart/mixed;boundary=\"----=_NextPart_000_001C_01CB238A.52E35400\"\n" + + "\n" + + "This is a multi-part message in MIME format.\n" + + "\n" + + "------=_NextPart_000_001C_01CB238A.52E35400\n" + + "Content-Type: text/plain;charset=\"us-ascii\"\n" + + "Content-Transfer-Encoding: 7bit\n" + + "\n" + + "Hi.\n" + + "\n" + + "\n" + + "------=_NextPart_000_001C_01CB238A.52E35400\n" + + "Content-Type: application/pdf;name=\"Receipt 1.pdf\"\n" + + "Content-Transfer-Encoding: base64\n" + + "Content-Disposition: attachment;filename=\"Receipt 1.pdf\"\n" + + "\n" + + "JVBERi0xLjQKJcfsj6IKNSAwIG9iago8PC9MZW5ndGggNiAwIFIvRmlsdGVyIC9GbGF0ZURlY29k\n" + + "\n" + + "------=_NextPart_000_001C_01CB238A.52E35400\n" + + "Content-Type: application/pdf;name=\"Receipt 2.pdf\"\n" + + "Content-Transfer-Encoding: base64\n" + + "Content-Disposition: attachment;filename=\"Receipt 2.pdf\"\n" + + "\n" + + "JVBERi0xLjQKJcfsj6IKNSAwIG9iago8PC9MZW5ndGggNiAwIFIvRmlsdGVyIC9GbGF0ZURlY29k\n" + + "\n" + + "------=_NextPart_000_001C_01CB238A.52E35400--\n" + + "\n" + + "\n"; + + @Test + public void testUnixLines() throws Exception { + doThreePartMailTest(THREE_PART_MAIL); + } + + @Test + public void testWindowsLines() throws Exception { + doThreePartMailTest(THREE_PART_MAIL.replace("\n", "\r\n")); + } + + @Test + public void testMacLines() throws Exception { + doThreePartMailTest(THREE_PART_MAIL.replace('\n', '\r')); + } + + /** + * Performs a check that the multipart of the email was handled correctly. + * + * @param text the email text. + * @throws Exception if an error occurs. + */ + private void doThreePartMailTest(String text) throws Exception { + Session session = Session.getDefaultInstance(new Properties()); + MimeMessage mimeMessage = new MimeMessage(session, + new ByteArrayInputStream(text.getBytes("US-ASCII"))); + + MimeMultipart topMultipart = (MimeMultipart) mimeMessage.getContent(); + assertEquals("Wrong preamble", + "This is a multi-part message in MIME format.", + topMultipart.getPreamble().trim()); + assertEquals("Wrong number of parts", 3, topMultipart.getCount()); + + BodyPart part1 = topMultipart.getBodyPart(0); + assertEquals("Wrong content type for part 1", + "text/plain;charset=\"us-ascii\"", part1.getContentType()); + + BodyPart part2 = topMultipart.getBodyPart(1); + assertEquals("Wrong content type for part 2", + "application/pdf;name=\"Receipt 1.pdf\"", part2.getContentType()); + + BodyPart part3 = topMultipart.getBodyPart(2); + assertEquals("Wrong content type for part 3", + "application/pdf;name=\"Receipt 2.pdf\"", part3.getContentType()); + } +}
diff --git a/mail/src/test/java/javax/mail/internet/MimeMultipartPropertyTest.java b/mail/src/test/java/javax/mail/internet/MimeMultipartPropertyTest.java new file mode 100644 index 0000000..cec3910 --- /dev/null +++ b/mail/src/test/java/javax/mail/internet/MimeMultipartPropertyTest.java
@@ -0,0 +1,193 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import com.sun.mail.test.AsciiStringInputStream; +import com.sun.mail.test.NullOutputStream; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.Properties; + +import javax.mail.Session; +import javax.mail.MessagingException; + +import org.junit.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * Test the properties that control the MimeMultipart class. + * Since the properties are now read in the parse method, all + * these tests can be run in the same JVM. + */ +public class MimeMultipartPropertyTest { + + private static Session s = Session.getInstance(new Properties()); + + /** + * Clear all properties before each test. + */ + @Before + public void beforeTest() { + clearAll(); + } + + @Test + public void testBoundary() throws Exception { + MimeMessage m = createMessage("x", "x", true); + MimeMultipart mp = (MimeMultipart)m.getContent(); + assertEquals(mp.getCount(), 2); + } + + @Test + public void testBoundaryIgnore() throws Exception { + System.setProperty( + "mail.mime.multipart.ignoreexistingboundaryparameter", "true"); + MimeMessage m = createMessage("x", "-", true); + MimeMultipart mp = (MimeMultipart)m.getContent(); + assertEquals(mp.getCount(), 2); + } + + @Test + public void testBoundaryMissing() throws Exception { + MimeMessage m = createMessage(null, "x", true); + MimeMultipart mp = (MimeMultipart)m.getContent(); + assertEquals(mp.getCount(), 2); + } + + @Test(expected=MessagingException.class) + public void testBoundaryMissingEx() throws Exception { + System.setProperty( + "mail.mime.multipart.ignoremissingboundaryparameter", "false"); + MimeMessage m = createMessage(null, "x", true); + MimeMultipart mp = (MimeMultipart)m.getContent(); + mp.getCount(); // throw exception + assertTrue(false); // never get here + } + + @Test + public void testEndBoundaryMissing() throws Exception { + MimeMessage m = createMessage("x", "x", false); + MimeMultipart mp = (MimeMultipart)m.getContent(); + assertEquals(mp.getCount(), 2); + } + + @Test(expected=MessagingException.class) + public void testEndBoundaryMissingEx() throws Exception { + System.setProperty( + "mail.mime.multipart.ignoremissingendboundary", "false"); + MimeMessage m = createMessage("x", "x", false); + MimeMultipart mp = (MimeMultipart)m.getContent(); + mp.getCount(); // throw exception + assertTrue(false); // never get here + } + + @Test + public void testAllowEmpty() throws Exception { + System.setProperty( "mail.mime.multipart.allowempty", "true"); + MimeMessage m = createEmptyMessage(); + MimeMultipart mp = (MimeMultipart)m.getContent(); + assertEquals(mp.getCount(), 0); + } + + @Test(expected=MessagingException.class) + public void testAllowEmptyEx() throws Exception { + MimeMessage m = createEmptyMessage(); + MimeMultipart mp = (MimeMultipart)m.getContent(); + mp.getCount(); // throw exception + assertTrue(false); // never get here + } + + @Test + public void testAllowEmptyOutput() throws Exception { + System.setProperty( "mail.mime.multipart.allowempty", "true"); + MimeMessage m = new MimeMessage(s); + MimeMultipart mp = new MimeMultipart(); + m.setContent(mp); + m.writeTo(new NullOutputStream()); + assertEquals(mp.getCount(), 0); + } + + @Test(expected=IOException.class) + public void testAllowEmptyOutputEx() throws Exception { + MimeMessage m = new MimeMessage(s); + MimeMultipart mp = new MimeMultipart(); + m.setContent(mp); + m.writeTo(new NullOutputStream()); // throw exception + assertTrue(false); // never get here + } + + /** + * Clear all properties after all tests. + */ + @AfterClass + public static void after() { + clearAll(); + } + + private static void clearAll() { + System.clearProperty( + "mail.mime.multipart.ignoreexistingboundaryparameter"); + System.clearProperty( + "mail.mime.multipart.ignoremissingboundaryparameter"); + System.clearProperty( + "mail.mime.multipart.ignoremissingendboundary"); + System.clearProperty( + "mail.mime.multipart.allowempty"); + } + + /** + * Create a test message. + * If param is not null, it specifies the boundary parameter. + * The actual boundary is specified by "actual". + * If "end" is true, include the end boundary. + */ + private static MimeMessage createMessage(String param, String actual, + boolean end) throws MessagingException { + String content = + "Mime-Version: 1.0\n" + + "Subject: Example\n" + + "Content-Type: multipart/mixed; " + + (param != null ? "boundary=\"" + param + "\"" : "") + "\n" + + "\n" + + "preamble\n" + + "--" + actual + "\n" + + "\n" + + "first part\n" + + "\n" + + "--" + actual + "\n" + + "\n" + + "second part\n" + + "\n" + + (end ? "--" + actual + "--\n" : ""); + + return new MimeMessage(s, new AsciiStringInputStream(content)); + } + + /** + * Create a test message with no parts. + */ + private static MimeMessage createEmptyMessage() throws MessagingException { + String content = + "Mime-Version: 1.0\n" + + "Subject: Example\n" + + "Content-Type: multipart/mixed; boundary=\"x\"\n\n"; + + return new MimeMessage(s, new AsciiStringInputStream(content)); + } +}
diff --git a/mail/src/test/java/javax/mail/internet/MimeUtilityTest.java b/mail/src/test/java/javax/mail/internet/MimeUtilityTest.java new file mode 100644 index 0000000..3bf945a --- /dev/null +++ b/mail/src/test/java/javax/mail/internet/MimeUtilityTest.java
@@ -0,0 +1,169 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import java.io.File; +import java.util.Set; +import java.util.HashSet; +import javax.activation.*; +import javax.mail.util.ByteArrayDataSource; + +import org.junit.*; +import static org.junit.Assert.*; + +/** + * Test MimeUtility methods. + * + * XXX - just a beginning... + * + * @author Bill Shannon + */ +public class MimeUtilityTest { + private static final byte[] utf16beBytes = { + (byte)0xfe, (byte)0x5b, (byte)0xdc, (byte)0x5f, + (byte)0x92, (byte)0x30, (byte)0x88, (byte)0x30, + (byte)0x8d, (byte)0x30, (byte)0x57, (byte)0x30, + (byte)0x4f, (byte)0x30, (byte)0x4a, (byte)0x30, + (byte)0x6d, (byte)0x30, (byte)0x4c, (byte)0x30, + (byte)0x44, (byte)0x30, (byte)0x57, (byte)0x30, + (byte)0x7e, (byte)0x30, (byte)0x59, (byte)0x30, + (byte)0x0d, (byte)0x00, (byte)0x0a, (byte)0x00 + }; + + @SuppressWarnings("serial") + private static final Set<String> encodings = new HashSet<String>() {{ + add("7bit"); add("8bit"); add("quoted-printable"); add("base64"); }}; + + /** + * Test that utf-16be data is encoded with base64 and not quoted-printable. + */ + @Test + public void testNonAsciiEncoding() throws Exception { + DataSource ds = new ByteArrayDataSource(utf16beBytes, + "text/plain; charset=utf-16be"); + String en = MimeUtility.getEncoding(ds); + assertEquals("non-ASCII encoding", "base64", en); + } + + /** + * Test that getEncoding returns a valid value even if the file + * doesn't exist. The return value should be a valid + * Content-Transfer-Encoding, but mostly we care that it doesn't + * throw NullPointerException. + */ + @Test + public void getEncodingMissingFile() throws Exception { + File missing = new File(getClass().getName()); + assertFalse(missing.toString(), missing.exists()); + FileDataSource fds = new FileDataSource(missing); + assertEquals(fds.getName(), missing.getName()); + assertTrue("getEncoding(DataSource)", + encodings.contains(MimeUtility.getEncoding(fds))); + assertTrue("getEncoding(DataHandler)", + encodings.contains(MimeUtility.getEncoding(new DataHandler(fds)))); + } + + /** + * Test that getEncoding returns a valid value even if the content + * type is bad. The return value should be a valid + * Content-Transfer-Encoding, but mostly we care that it doesn't + * throw NullPointerException. + */ + @Test + public void getEncodingBadContent() throws Exception { + String content = "bad-content-type"; + ContentType type = null; + try { + type = new ContentType(content); + fail(type.toString()); + } catch (ParseException expect) { + if (type != null) { + throw expect; + } + } + + ByteArrayDataSource bads = new ByteArrayDataSource("", content); + bads.setName(null); + assertTrue(encodings.contains(MimeUtility.getEncoding(bads))); + assertTrue(encodings.contains( + MimeUtility.getEncoding(new DataHandler(bads)))); + + bads.setName(""); + assertTrue(encodings.contains(MimeUtility.getEncoding(bads))); + assertTrue(encodings.contains( + MimeUtility.getEncoding(new DataHandler(bads)))); + + bads.setName(getClass().getName()); + assertTrue(encodings.contains(MimeUtility.getEncoding(bads))); + assertTrue(encodings.contains( + MimeUtility.getEncoding(new DataHandler(bads)))); + } + + /** + * Test that encoding a Unicode string with surrogate pairs + * doesn't split the encoding between the pairs. + */ + @Test + public void testSurrogatePairs() throws Exception { + // test a specific case + String sp = "a" + + "\ud801\udc00\ud801\udc00\ud801\udc00\ud801\udc00" + + "\ud801\udc00\ud801\udc00\ud801\udc00\ud801\udc00" + + "\ud801\udc00\ud801\udc00\ud801\udc00\ud801\udc00" + + "\ud801\udc00\ud801\udc00\ud801\udc00\ud801\udc00"; + String en = MimeUtility.encodeText(sp, "utf-8", "B"); + String dt = MimeUtility.decodeText(en); + // encoding it and decoding it shouldn't change it + assertEquals(dt, sp); + String[] w = en.split(" "); + // the first word should end with the second half of a pair + String dw = MimeUtility.decodeWord(w[0]); + assertTrue(dw.charAt(dw.length()-1) == '\udc00'); + // and the second word should start with the first half of a pair + dw = MimeUtility.decodeWord(w[1]); + assertTrue(dw.charAt(0) == '\ud801'); + + // test various string lengths + int ch = 0xFE000; + String test = ""; + for (int i = 0; i < 50; i++) { + test += new String(Character.toChars(ch)); + String encoded = MimeUtility.encodeText(test, "UTF-8", "B"); + String decoded = MimeUtility.decodeText(encoded); + assertEquals(decoded, test); + } + } + + /** + * Test that encoded words with the wrong Chinese charset still + * decode correctly. + */ + @Test + public void testBadChineseCharsets() throws Exception { + String badgb2312 = "=?gb2312?B?xbfUqqLjIChFVVIpttK7u4EwhDYgKENOWSk=?="; + String badgbk = "=?gbk?B?xbfUqqLjIChFVVIpttK7u4EwhDYgKENOWSk=?="; + String badms936 = "=?ms936?B?xbfUqqLjIChFVVIpttK7u4EwhDYgKENOWSk=?="; + String badcp936 = "=?cp936?B?xbfUqqLjIChFVVIpttK7u4EwhDYgKENOWSk=?="; + String good = "=?gb18030?B?xbfUqqLjIChFVVIpttK7u4EwhDYgKENOWSk=?="; + String goodDecoded = MimeUtility.decodeWord(good); + + assertEquals("gb2312", goodDecoded, MimeUtility.decodeWord(badgb2312)); + assertEquals("gbk", goodDecoded, MimeUtility.decodeWord(badgbk)); + assertEquals("ms936", goodDecoded, MimeUtility.decodeWord(badms936)); + assertEquals("cp936", goodDecoded, MimeUtility.decodeWord(badcp936)); + } +}
diff --git a/mail/src/test/java/javax/mail/internet/ModifyMessageTest.java b/mail/src/test/java/javax/mail/internet/ModifyMessageTest.java new file mode 100644 index 0000000..275b1cf --- /dev/null +++ b/mail/src/test/java/javax/mail/internet/ModifyMessageTest.java
@@ -0,0 +1,167 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import com.sun.mail.test.AsciiStringInputStream; +import java.io.ByteArrayInputStream; +import java.nio.charset.Charset; +import java.util.Properties; + +import javax.mail.Session; +import javax.mail.MessagingException; +import javax.mail.BodyPart; + +import org.junit.*; +import static org.junit.Assert.assertEquals; + +/** + * Test some of the ways you might modify a message that has been + * read from an input stream. + */ +public class ModifyMessageTest { + + private static Session s = Session.getInstance(new Properties()); + + @Test + public void testAddHeader() throws Exception { + MimeMessage m = createMessage(); + MimeMultipart mp = (MimeMultipart)m.getContent(); + m.setHeader("a", "b"); + m.saveChanges(); + + MimeMessage m2 = new MimeMessage(m); + assertEquals("b", m2.getHeader("a", null)); + } + + @Test + public void testChangeHeader() throws Exception { + MimeMessage m = createMessage(); + MimeMultipart mp = (MimeMultipart)m.getContent(); + m.setHeader("Subject", "test"); + m.saveChanges(); + + MimeMessage m2 = new MimeMessage(m); + assertEquals("test", m2.getHeader("Subject", null)); + } + + @Test + public void testAddContent() throws Exception { + MimeMessage m = createMessage(); + MimeMultipart mp = (MimeMultipart)m.getContent(); + MimeBodyPart mbp = new MimeBodyPart(); + mbp.setText("test"); + mp.addBodyPart(mbp); + m.saveChanges(); + + MimeMessage m2 = new MimeMessage(m); + mp = (MimeMultipart)m2.getContent(); + BodyPart bp = mp.getBodyPart(2); + assertEquals("test", bp.getContent()); + // make sure nothing else changed + bp = mp.getBodyPart(0); + assertEquals("first part\n", bp.getContent()); + bp = mp.getBodyPart(1); + assertEquals("second part\n", bp.getContent()); + } + + @Test + public void testChangeContent() throws Exception { + MimeMessage m = createMessage(); + MimeMultipart mp = (MimeMultipart)m.getContent(); + BodyPart bp = mp.getBodyPart(0); + bp.setText("test"); + m.saveChanges(); + + MimeMessage m2 = new MimeMessage(m); + mp = (MimeMultipart)m2.getContent(); + bp = mp.getBodyPart(0); + assertEquals("test", bp.getContent()); + } + + @Test + public void testChangeNestedContent() throws Exception { + MimeMessage m = createNestedMessage(); + MimeMultipart mp = (MimeMultipart)m.getContent(); + mp = (MimeMultipart)mp.getBodyPart(0).getContent(); + BodyPart bp = mp.getBodyPart(0); + bp.setText("test"); + m.saveChanges(); + + MimeMessage m2 = new MimeMessage(m); + mp = (MimeMultipart)m2.getContent(); + mp = (MimeMultipart)mp.getBodyPart(0).getContent(); + bp = mp.getBodyPart(0); + assertEquals("test", bp.getContent()); + // make sure other content is not changed or re-encoded + MimeBodyPart mbp = (MimeBodyPart)mp.getBodyPart(1); + assertEquals("second part\n", mbp.getContent()); + assertEquals("quoted-printable", mbp.getEncoding()); + mbp = (MimeBodyPart)mp.getBodyPart(2); + assertEquals("third part\n", mbp.getContent()); + assertEquals("base64", mbp.getEncoding()); + } + + private static MimeMessage createMessage() throws MessagingException { + String content = + "Mime-Version: 1.0\n" + + "Subject: Example\n" + + "Content-Type: multipart/mixed; boundary=\"-\"\n" + + "\n" + + "preamble\n" + + "---\n" + + "\n" + + "first part\n" + + "\n" + + "---\n" + + "\n" + + "second part\n" + + "\n" + + "-----\n"; + + return new MimeMessage(s, new AsciiStringInputStream(content)); + } + + private static MimeMessage createNestedMessage() throws MessagingException { + String content = + "Mime-Version: 1.0\n" + + "Subject: Example\n" + + "Content-Type: multipart/mixed; boundary=\"-\"\n" + + "\n" + + "preamble\n" + + "---\n" + + "Content-Type: multipart/mixed; boundary=\"x\"\n" + + "\n" + + "--x\n" + + "\n" + + "first part\n" + + "\n" + + "--x\n" + + "Content-Transfer-Encoding: quoted-printable\n" + + "\n" + + "second part\n" + + "\n" + + "--x\n" + + "Content-Transfer-Encoding: base64\n" + + "\n" + + "dGhpcmQgcGFydAo=\n" + // "third part\n", base64 encoded + "\n" + + "--x--\n" + + "-----\n"; + + return new MimeMessage(s, new AsciiStringInputStream(content)); + } +}
diff --git a/mail/src/test/java/javax/mail/internet/NewsAddressTest.java b/mail/src/test/java/javax/mail/internet/NewsAddressTest.java new file mode 100644 index 0000000..93cce3b --- /dev/null +++ b/mail/src/test/java/javax/mail/internet/NewsAddressTest.java
@@ -0,0 +1,36 @@ +/* + * Copyright (c) 2014, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import org.junit.*; +import static org.junit.Assert.assertEquals; + +/** + * Test the NewsAddress class. + * + * XXX - for now, just some simple regression tests for reported bugs. + */ +public class NewsAddressTest { + + @Test + public void testReflexiveEquality() throws Exception { + NewsAddress a = new NewsAddress(); + assertEquals(a, a); // bug 6365 + a = new NewsAddress("net.unix"); + assertEquals(a, a); + } +}
diff --git a/mail/src/test/java/javax/mail/internet/NoEncodeFileName.java b/mail/src/test/java/javax/mail/internet/NoEncodeFileName.java new file mode 100644 index 0000000..5253f4c --- /dev/null +++ b/mail/src/test/java/javax/mail/internet/NoEncodeFileName.java
@@ -0,0 +1,89 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import java.io.UnsupportedEncodingException; + +import javax.mail.MessagingException; +import javax.mail.internet.MimeBodyPart; + +import org.junit.*; +import static org.junit.Assert.assertTrue; + +/** + * Test "mail.mime.encodefilename" System property not set. + */ +public class NoEncodeFileName { + + protected static String fileName; + + // a bunch of non-ASCII characters + private static String encodedFileName = + "=?utf-8?B?w4DDgcOFw4bDgMOBw4XDhsOHw4jDicOKw4vDjMONw47Dj8OQw4DD" + + "gcOFw4bDh8OIw4nDisOLw4zDjcOOw4/DkMORw5LDk8OUw5XDlsOYw5nDmsObw5" + + "zDncOew5/DoMOhw6LDo8Okw6XDpsOnw6jDqcOqw6vDrMOtw67Dr8Oww7HDssOz" + + "w7TDtcO2w7jDucO6w7vDvMO9w77Dv8OAw4HDhcOGw4cuZG9j?="; + + static { + try { + fileName = MimeUtility.decodeText(encodedFileName); + } catch (UnsupportedEncodingException ex) { + // should never happen + } + + } + + // RFC 2231 encoding + private static String expected = + "utf-8''%C3%80%C3%81%C3%85%C3%86%C3%80%C3%81%C3%85%C3%86%C3%87%C3%88" + + "%C3%89%C3%8A%C3%8B%C3%8C%C3%8D%C3%8E%C3%8F%C3%90%C3%80%C3%81%C3%85" + + "%C3%86%C3%87%C3%88%C3%89%C3%8A%C3%8B%C3%8C%C3%8D%C3%8E%C3%8F%C3%90" + + "%C3%91%C3%92%C3%93%C3%94%C3%95%C3%96%C3%98%C3%99%C3%9A%C3%9B%C3%9C" + + "%C3%9D%C3%9E%C3%9F%C3%A0%C3%A1%C3%A2%C3%A3%C3%A4%C3%A5%C3%A6%C3%A7" + + "%C3%A8%C3%A9%C3%AA%C3%AB%C3%AC%C3%AD%C3%AE%C3%AF%C3%B0%C3%B1%C3%B2" + + "%C3%B3%C3%B4%C3%B5%C3%B6%C3%B8%C3%B9%C3%BA%C3%BB%C3%BC%C3%BD%C3%BE" + + "%C3%BF%C3%80%C3%81%C3%85%C3%86%C3%87.doc"; + + @BeforeClass + public static void before() { + System.out.println("NoEncodeFileName"); + System.setProperty("mail.mime.charset", "utf-8"); + System.clearProperty("mail.mime.encodefilename"); + } + + @Test + public void test() throws Exception { + MimeBodyPart mbp = new MimeBodyPart(); + mbp.setText("test"); + mbp.setFileName(fileName); + mbp.updateHeaders(); + String h = mbp.getHeader("Content-Type", ""); + assertTrue(h.contains("name*=")); + assertTrue(h.contains(expected)); + h = mbp.getHeader("Content-Disposition", ""); + assertTrue(h.contains("filename*=")); + assertTrue(h.contains(expected)); + } + + @AfterClass + public static void after() { + // should be unnecessary + System.clearProperty("mail.mime.charset"); + System.clearProperty("mail.mime.encodefilename"); + System.clearProperty("mail.mime.encodeparameters"); + } +}
diff --git a/mail/src/test/java/javax/mail/internet/NonAsciiBoundaryTest.java b/mail/src/test/java/javax/mail/internet/NonAsciiBoundaryTest.java new file mode 100644 index 0000000..d54b9ff --- /dev/null +++ b/mail/src/test/java/javax/mail/internet/NonAsciiBoundaryTest.java
@@ -0,0 +1,68 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import com.sun.mail.test.AsciiStringInputStream; +import java.io.ByteArrayInputStream; +import java.nio.charset.Charset; +import java.util.Properties; + +import javax.mail.Session; +import javax.mail.MessagingException; +import javax.mail.BodyPart; + +import org.junit.*; +import static org.junit.Assert.assertEquals; + +/** + * Test that non-ASCII boundary strings are handled reasonably, + * even though, strictly speaking, the MIME spec doesn't allow them. + */ +public class NonAsciiBoundaryTest { + + private static Session s = Session.getInstance(new Properties()); + + @Test + public void test() throws Exception { + MimeMessage m = createMessage(); + MimeMultipart mp = (MimeMultipart)m.getContent(); + assertEquals(2, mp.getCount()); + BodyPart bp = mp.getBodyPart(0); + assertEquals("first part\n", bp.getContent()); + bp = mp.getBodyPart(1); + assertEquals("second part\n", bp.getContent()); + } + + private static MimeMessage createMessage() throws MessagingException { + String content = + "Mime-Version: 1.0\n" + + "Subject: Example\n" + + "Content-Type: multipart/mixed; boundary=\"\u00A9\"\n" + + "\n" + + "--\u00A9\n" + + "\n" + + "first part\n" + + "\n" + + "--\u00A9\n" + + "\n" + + "second part\n" + + "\n" + + "--\u00A9--\n"; + + return new MimeMessage(s, new AsciiStringInputStream(content, false)); + } +}
diff --git a/mail/src/test/java/javax/mail/internet/NonAsciiFileNames.java b/mail/src/test/java/javax/mail/internet/NonAsciiFileNames.java new file mode 100644 index 0000000..8b79d37 --- /dev/null +++ b/mail/src/test/java/javax/mail/internet/NonAsciiFileNames.java
@@ -0,0 +1,78 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import org.junit.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * Test that non-ASCII file names are encoded by default. + */ +public class NonAsciiFileNames { + + private static String charset; + + @BeforeClass + public static void before() { + System.out.println("NonAsciiFileNames"); + charset = System.getProperty("mail.mime.charset"); + System.setProperty("mail.mime.charset", "utf-8"); + } + + /** + * Test that non-ASCII filenames are encoded by default. + */ + @Test + public void testNonAsciiFileName() throws Exception { + MimeBodyPart mbp = new MimeBodyPart(); + mbp.setText("test\n"); + mbp.setFileName("test\u00a1\u00a2\u00a3"); + MimeBodyPart.updateHeaders(mbp); + + String s = mbp.getHeader("Content-Disposition", null); + assertTrue("Content-Disposition filename", s.indexOf("filename*") >= 0); + s = mbp.getHeader("Content-Type", null); + assertTrue("Content-Type name", s.indexOf("name*") >= 0); + } + + /** + * Test that non-ASCII filenames are encoded by default. + * Make sure an existing Content-Type header is updated. + */ + @Test + public void testNonAsciiFileNameWithContentType() throws Exception { + MimeBodyPart mbp = new MimeBodyPart(); + mbp.setText("test\n"); + mbp.setHeader("Content-Type", "text/x-test"); + mbp.setFileName("test\u00a1\u00a2\u00a3"); + MimeBodyPart.updateHeaders(mbp); + + String s = mbp.getHeader("Content-Disposition", null); + assertTrue("Content-Disposition filename", s.indexOf("filename*") >= 0); + s = mbp.getHeader("Content-Type", null); + assertTrue("Content-Type name", s.indexOf("name*") >= 0); + } + + @AfterClass + public static void after() { + if (charset == null) + System.clearProperty("mail.mime.charset"); + else + System.setProperty("mail.mime.charset", charset); + } +}
diff --git a/mail/src/test/java/javax/mail/internet/ParameterListDecode.java b/mail/src/test/java/javax/mail/internet/ParameterListDecode.java new file mode 100644 index 0000000..0c54ed2 --- /dev/null +++ b/mail/src/test/java/javax/mail/internet/ParameterListDecode.java
@@ -0,0 +1,427 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import java.io.*; +import java.util.*; +import javax.activation.*; +import javax.mail.*; +import javax.mail.internet.*; +import javax.mail.util.*; + +import org.junit.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * Test parameter list parsing. + * + * XXX - this should be a JUnit parameterized test, + * but I can't figure out how to run parameterized + * tests under my ClassLoaderSuite. + * + * @author Bill Shannon + */ + +public class ParameterListDecode { + static boolean gen_test_input = false; // output good for input to -p + static boolean parse_mail = false; // parse input in mail format + static boolean test_mail = false; // test using a mail server + static int errors = 0; // number of errors detected + + static String protocol; + static String host = null; + static String user = null; + static String password = null; + static String mbox = null; + static String url = null; + static int port = -1; + static boolean debug = false; + + static Session session; + static Store store; + static Folder folder; + + static boolean junit; + + protected static void testDecode(String paramData) throws Exception { + junit = true; + parse(new BufferedReader(new InputStreamReader( + ParameterListDecode.class.getResourceAsStream(paramData)))); + } + + public static void main(String argv[]) throws Exception { + System.getProperties().put("mail.mime.decodeparameters", "true"); + int optind; + for (optind = 0; optind < argv.length; optind++) { + if (argv[optind].equals("-")) { + // ignore + } else if (argv[optind].equals("-g")) { + gen_test_input = true; + } else if (argv[optind].equals("-p")) { + parse_mail = true; + } else if (argv[optind].equals("-m")) { + test_mail = true; + } else if (argv[optind].equals("-T")) { + protocol = argv[++optind]; + } else if (argv[optind].equals("-H")) { + host = argv[++optind]; + } else if (argv[optind].equals("-U")) { + user = argv[++optind]; + } else if (argv[optind].equals("-P")) { + password = argv[++optind]; + } else if (argv[optind].equals("-D")) { + debug = true; + } else if (argv[optind].equals("-f")) { + mbox = argv[++optind]; + } else if (argv[optind].equals("-L")) { + url = argv[++optind]; + } else if (argv[optind].equals("-p")) { + port = Integer.parseInt(argv[++optind]); + } else if (argv[optind].equals("-d")) { + debug = true; + } else if (argv[optind].equals("--")) { + optind++; + break; + } else if (argv[optind].startsWith("-")) { + System.out.println( + "Usage: paramtest [-g] [-p] [-] [content-type ...]"); + System.out.println( +"or\tparamtest -m [-g] [-L url] [-T protocol] [-H host] [-p port] [-U user]"); + System.out.println( +"\t[-P password] [-f mailbox] [-d]"); + System.exit(1); + } else { + break; + } + } + + if (test_mail) + initMail(); + + /* + * If there's any args left on the command line, + * concatenate them into a string and test that. + */ + if (optind < argv.length) { + StringBuilder sb = new StringBuilder(); + for (int i = optind; i < argv.length; i++) { + sb.append(argv[i]); + sb.append(" "); + } + test("Content-Type", sb.toString(), null); + } else { + // read from stdin + BufferedReader in = + new BufferedReader(new InputStreamReader(System.in)); + String s; + + if (parse_mail) + parse(in); + else if (test_mail) + testMail(); + else { + while ((s = in.readLine()) != null) + test("Content-Type", s, null); + } + } + + if (test_mail) + doneMail(); + + System.exit(errors); + + } + + /* + * Parse the input in "mail" format, extracting the Content-Type + * headers and testing them. The parse is rather crude, but sufficient + * to test against most existing UNIX mailboxes. + */ + public static void parse(BufferedReader in) throws Exception { + String header = ""; + + for (;;) { + String s = in.readLine(); + if (s != null && s.length() > 0) { + char c = s.charAt(0); + if (c == ' ' || c == '\t') { + // a continuation line, add it to the current header + header += '\n' + s; + continue; + } + } + // "s" is the next header, "header" is the last complete header + if (header.regionMatches(true, 0, "Content-Type: ", 0, 14)) { + int i; + String[] expect = null; + if (s != null && s.startsWith("Expect: ")) { + try { + int nexpect = Integer.parseInt(s.substring(8)); + expect = new String[nexpect]; + for (i = 0; i < nexpect; i++) + expect[i] = decode(trim(in.readLine())); + } catch (NumberFormatException e) { + try { + if (s.substring(8, 17).equals("Exception")) { + expect = new String[1]; + expect[0] = "Exception"; + } + } catch (StringIndexOutOfBoundsException se) { + // ignore it + } + } + } + i = header.indexOf(':'); + try { + test(header.substring(0, i), header.substring(i + 2), + expect); + } catch (StringIndexOutOfBoundsException e) { + // ignore + } + } + if (s == null) + return; // EOF + if (s.length() == 0) { + while ((s = in.readLine()) != null) { + if (s.startsWith("From ")) + break; + } + if (s == null) + return; + } + header = s; + } + } + + /** + * Like String.trim, but only the left side. + */ + public static String trim(String s) { + int i = 0; + while (i < s.length() && s.charAt(i) <= ' ') + i++; + return s.substring(i); + } + + /** + * Decode Unicode escapes. + */ + public static String decode(String s) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c == '\\' && s.charAt(i + 1) == 'u') { + c = (char)Integer.parseInt(s.substring(i + 2, i + 6), 16); + i += 5; + } + sb.append(c); + } + return sb.toString(); + } + + /** + * Test the header's value to see if we can parse it as expected. + */ + public static void test(String header, String value, String expect[]) + throws Exception { + PrintStream out = System.out; + ByteArrayOutputStream bos = null; + if (gen_test_input) { + if (test_mail) { + bos = new ByteArrayOutputStream(); + out = new PrintStream(bos); + } else { + out.println(header + ": " + value); + } + } else if (!junit) + out.println("Test: " + value); + + try { + ContentType ct = new ContentType(value); + ParameterList pl = ct.getParameterList(); + if (gen_test_input) + out.println("Expect: " + pl.size()); + else if (junit) + assertEquals("Number of parameters", + expect.length, pl.size()); + else { + out.println("Got " + pl.size() + " parameters:"); + if (expect != null && pl.size() != expect.length) { + out.println("Expected " + expect.length + " parameters"); + errors++; + } + } + Enumeration<String> e = pl.getNames(); + for (int i = 0; e.hasMoreElements(); i++) { + String name = e.nextElement(); + String pvalue = pl.get(name); + if (gen_test_input) + out.println("\t" + name + "=" + pvalue); // XXX - newline + else if (junit) { + if (i < expect.length) + assertEquals("Parameter value", + expect[i], name + "=" + pvalue); + } else { + out.println("\t[" + (i+1) + "] Name: " + name + + "\t\tValue: " + pvalue); + if (expect != null && i < expect.length && + !expect[i].equals(name + "=" + pvalue)) { + out.println("\tExpected:\t" + expect[i]); + errors++; + } + } + } + } catch (ParseException e) { + if (gen_test_input) + out.println("Expect: Exception " + e); + else if (junit) + assertTrue("Expected exception", + expect.length == 1 && expect[0].equals("Exception")); + else { + out.println("Got Exception: " + e); + if (expect != null && + (expect.length != 1 || !expect[0].equals("Exception"))) { + out.println("Expected " + expect.length + " parameters"); + for (int i = 0; i < expect.length; i++) + out.println("\tExpected:\t" + expect[i]); + errors++; + } + } + } + if (gen_test_input && test_mail) { + MimeMessage msg = new MimeMessage(session); + byte[] buf = bos.toByteArray(); + msg.setDataHandler(new DataHandler( + new ByteArrayDataSource(buf, value))); + msg.saveChanges(); + //msg.writeTo(System.out); + folder.appendMessages(new Message[] { msg }); + } + } + + /** + * Initialize the Session, Store, and Folder. + */ + private static void initMail() { + try { + // Get a Properties object + Properties props = System.getProperties(); + + // Get a Session object + session = Session.getInstance(props, null); + session.setDebug(debug); + + // Get a Store object + if (url != null) { + URLName urln = new URLName(url); + store = session.getStore(urln); + store.connect(); + } else { + if (protocol != null) + store = session.getStore(protocol); + else + store = session.getStore(); + + // Connect + if (host != null || user != null || password != null) + store.connect(host, port, user, password); + else + store.connect(); + } + + // Open the Folder + + folder = store.getDefaultFolder(); + if (folder == null) { + System.out.println("No default folder"); + System.exit(1); + } + + if (mbox == null) + mbox = "parameter-list-test"; + folder = folder.getFolder(mbox); + if (folder == null) { + System.out.println("Invalid folder"); + System.exit(1); + } + + if (gen_test_input) { + folder.delete(false); + folder.create(Folder.HOLDS_MESSAGES); + folder.open(Folder.READ_WRITE); + } else { + folder.open(Folder.READ_ONLY); + } + + } catch (Exception ex) { + System.out.println("Oops, got exception! " + ex.getMessage()); + ex.printStackTrace(); + System.exit(1); + } + } + + /** + * Close the Folder and Store. + */ + private static void doneMail() throws Exception { + folder.close(false); + store.close(); + } + + /** + * Use the messages in the Folder for testing. + */ + private static void testMail() throws Exception { + Message[] msgs = folder.getMessages(); + + for (int i = 0; i < msgs.length; i++) + testMessage(msgs[i]); + } + + /** + * Test an individual message. + */ + private static void testMessage(Message msg) throws Exception { + String[] expect = null; + + BufferedReader in = new BufferedReader( + new InputStreamReader(msg.getInputStream())); + + String s = in.readLine(); + if (s != null && s.startsWith("Expect: ")) { + try { + int nexpect = Integer.parseInt(s.substring(8)); + expect = new String[nexpect]; + for (int i = 0; i < nexpect; i++) + expect[i] = trim(in.readLine()); + } catch (NumberFormatException e) { + try { + if (s.substring(8, 17).equals("Exception")) { + expect = new String[1]; + expect[0] = "Exception"; + } + } catch (StringIndexOutOfBoundsException se) { + // ignore it + } + } + } + + String ct = msg.getContentType(); + test("Content-Type: ", ct, expect); + } +}
diff --git a/mail/src/test/java/javax/mail/internet/ParameterListTestSuite.java b/mail/src/test/java/javax/mail/internet/ParameterListTestSuite.java new file mode 100644 index 0000000..66c32d4 --- /dev/null +++ b/mail/src/test/java/javax/mail/internet/ParameterListTestSuite.java
@@ -0,0 +1,39 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import org.junit.runner.RunWith; +import org.junit.runners.Suite.SuiteClasses; + +import com.sun.mail.test.ClassLoaderSuite; +import com.sun.mail.test.ClassLoaderSuite.TestClass; + +/** + * Suite of ParameterList tests that need to be run in a separate class loader. + */ +@RunWith(ClassLoaderSuite.class) +@TestClass(ParameterList.class) +@SuiteClasses( { + ParameterListTests.class, + WindowsFileNames.class, + AppleFileNames.class, + NonAsciiFileNames.class, + DecodeParameters.class, + ParametersNoStrict.class +}) +public class ParameterListTestSuite { +}
diff --git a/mail/src/test/java/javax/mail/internet/ParameterListTests.java b/mail/src/test/java/javax/mail/internet/ParameterListTests.java new file mode 100644 index 0000000..8df0266 --- /dev/null +++ b/mail/src/test/java/javax/mail/internet/ParameterListTests.java
@@ -0,0 +1,72 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import org.junit.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * XXX - add more tests + */ +public class ParameterListTests { + + @BeforeClass + public static void before() { + System.out.println("ParameterListTests"); + System.clearProperty("mail.mime.windowsfilenames"); + System.clearProperty("mail.mime.applefilenames"); + } + + /** + * Test that backslashes are properly removed. + */ + @Test + public void testBackslash() throws Exception { + System.clearProperty("mail.mime.windowsfilenames"); + ParameterList pl = new ParameterList("; filename=\"\\a\\b\\c.txt\""); + assertEquals(pl.get("filename"), "abc.txt"); + } + + /** + * Test that a long parameter that's been split into segments + * is parsed correctly. + */ + @Test + public void testLongParse() throws Exception { + String p0 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + String p1 = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; + ParameterList pl = new ParameterList("; p*0="+p0+"; p*1="+p1); + assertEquals(p0 + p1, pl.get("p")); + } + + /** + * Test that a long parameter that's set programmatically is split + * into segments. + */ + @Test + public void testLongSet() throws Exception { + String p0 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + String p1 = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; + ParameterList pl = new ParameterList(); + pl.set("p", p0 + p1); + assertEquals(p0 + p1, pl.get("p")); + String pls = pl.toString(); + assertTrue(pls.indexOf("p*0=") >= 0); + assertTrue(pls.indexOf("p*1=") >= 0); + } +}
diff --git a/mail/src/test/java/javax/mail/internet/ParametersNoStrict.java b/mail/src/test/java/javax/mail/internet/ParametersNoStrict.java new file mode 100644 index 0000000..6367797 --- /dev/null +++ b/mail/src/test/java/javax/mail/internet/ParametersNoStrict.java
@@ -0,0 +1,44 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import org.junit.*; +import static org.junit.Assert.assertEquals; + +/** + * Test that the "mail.mime.parameters.strict" System property + * set to false allows bogus parameters to be parsed. + */ +public class ParametersNoStrict extends ParameterListDecode { + + @BeforeClass + public static void before() { + System.out.println("ParametersNoStrict"); + System.setProperty("mail.mime.parameters.strict", "false"); + } + + @Test + public void testDecode() throws Exception { + testDecode("paramdatanostrict"); + } + + @AfterClass + public static void after() { + // should be unnecessary + System.clearProperty("mail.mime.parameters.strict"); + } +}
diff --git a/mail/src/test/java/javax/mail/internet/ReferencesTest.java b/mail/src/test/java/javax/mail/internet/ReferencesTest.java new file mode 100644 index 0000000..62ba5b2 --- /dev/null +++ b/mail/src/test/java/javax/mail/internet/ReferencesTest.java
@@ -0,0 +1,99 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import java.util.Properties; +import javax.mail.*; +import javax.mail.internet.*; + +import org.junit.*; +import static org.junit.Assert.assertEquals; + +/** + * Test setting of the References header. + * + * @author Bill Shannon + */ +public class ReferencesTest { + private static Session session = Session.getInstance(new Properties()); + + /* + * Test cases: + * + * Message-Id References In-Reply-To Expected Result + */ + + @Test + public void test1() throws MessagingException { + test(null, null, null, null); + } + + @Test + public void test2() throws MessagingException { + test(null, null, "<1@a>", "<1@a>"); + } + + @Test + public void test3() throws MessagingException { + test(null, "<2@b>", null, "<2@b>"); + } + + @Test + public void test4() throws MessagingException { + test(null, "<2@b>", "<1@a>", "<2@b>"); + } + + @Test + public void test5() throws MessagingException { + test("<3@c>", null, null, "<3@c>"); + } + + @Test + public void test6() throws MessagingException { + test("<3@c>", null, "<1@a>", "<1@a> <3@c>"); + } + + @Test + public void test7() throws MessagingException { + test("<3@c>", "<2@b>", null, "<2@b> <3@c>"); + } + + @Test + public void test8() throws MessagingException { + test("<3@c>", "<2@b>", "<1@a>", "<2@b> <3@c>"); + } + + private static void test(String msgid, String ref, String irt, String res) + throws MessagingException { + MimeMessage msg = new MimeMessage(session); + msg.setFrom(); + msg.setRecipients(Message.RecipientType.TO, "you@example.com"); + msg.setSubject("test"); + if (msgid != null) + msg.setHeader("Message-Id", msgid); + if (ref != null) + msg.setHeader("References", ref); + if (irt != null) + msg.setHeader("In-Reply-To", irt); + msg.setText("text"); + + MimeMessage reply = (MimeMessage)msg.reply(false); + String rref = reply.getHeader("References", " "); + + assertEquals(res, rref); + } +}
diff --git a/mail/src/test/java/javax/mail/internet/RestrictEncodingTest.java b/mail/src/test/java/javax/mail/internet/RestrictEncodingTest.java new file mode 100644 index 0000000..8d32a0b --- /dev/null +++ b/mail/src/test/java/javax/mail/internet/RestrictEncodingTest.java
@@ -0,0 +1,108 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import com.sun.mail.test.AsciiStringInputStream; +import java.io.ByteArrayInputStream; +import java.nio.charset.Charset; +import java.util.Properties; + +import javax.mail.Session; +import javax.mail.MessagingException; +import javax.mail.BodyPart; + +import org.junit.*; +import static org.junit.Assert.assertEquals; + +/** + * Test that the Content-Transfer-Encoding header is ignored + * for composite parts. + * + * XXX - We don't test any of the properties that control this behavior. + */ +public class RestrictEncodingTest { + + private static Session s = Session.getInstance(new Properties()); + + @Test + public void testMultipart() throws Exception { + MimeMessage m = createMessage(); + MimeMultipart mp = (MimeMultipart)m.getContent(); + assertEquals(2, mp.getCount()); + + BodyPart bp = mp.getBodyPart(0); + assertEquals("first part=\n", bp.getContent()); + } + + @Test + public void testMessage() throws Exception { + MimeMessage m = createMessage(); + MimeMultipart mp = (MimeMultipart)m.getContent(); + + BodyPart bp = mp.getBodyPart(1); + MimeMessage m2 = (MimeMessage)bp.getContent(); + assertEquals("message=\n", m2.getContent()); + } + + @Test + public void testWrite() throws Exception { + MimeMessage m = new MimeMessage(s); + MimeMultipart mp = new MimeMultipart(); + MimeBodyPart mbp = new MimeBodyPart(); + mbp.setText("first part"); + mp.addBodyPart(mbp); + MimeMessage m2 = new MimeMessage(s); + m2.setSubject("example"); + m2.setText("message=\n"); + mbp = new MimeBodyPart(); + mbp.setContent(m2, "message/rfc822"); + mbp.setHeader("Content-Transfer-Encoding", "quoted-printable"); + mp.addBodyPart(mbp); + m.setContent(mp); + m.setHeader("Content-Transfer-Encoding", "quoted-printable"); + + m = new MimeMessage(m); // copy it + mp = (MimeMultipart)m.getContent(); + + BodyPart bp = mp.getBodyPart(1); + m2 = (MimeMessage)bp.getContent(); + assertEquals("message=\n", m2.getContent()); + } + + private static MimeMessage createMessage() throws MessagingException { + String content = + "Mime-Version: 1.0\n" + + "Content-Type: multipart/mixed; boundary=\"=3D\"\n" + + "Content-Transfer-Encoding: quoted-printable\n" + + "\n" + + "--=3D\n" + + "\n" + + "first part=\n" + + "\n" + + "--=3D\n" + + "Content-Type: message/rfc822\n" + + "Content-Transfer-Encoding: quoted-printable\n" + + "\n" + + "Subject: example\n" + + "\n" + + "message=\n" + + "\n" + + "--=3D--\n"; + + return new MimeMessage(s, new AsciiStringInputStream(content)); + } +}
diff --git a/mail/src/test/java/javax/mail/internet/WindowsFileNames.java b/mail/src/test/java/javax/mail/internet/WindowsFileNames.java new file mode 100644 index 0000000..b11c96b --- /dev/null +++ b/mail/src/test/java/javax/mail/internet/WindowsFileNames.java
@@ -0,0 +1,45 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import org.junit.*; +import static org.junit.Assert.assertEquals; + +/** + * Test that the "mail.mime.windowsfilenames" System property + * causes the filename to be returned with backslashes preserved. + */ +public class WindowsFileNames { + + @BeforeClass + public static void before() { + System.out.println("WindowsFileNames"); + System.setProperty("mail.mime.windowsfilenames", "true"); + } + + @Test + public void testProp() throws Exception { + ParameterList pl = new ParameterList("; filename=\"\\a\\b\\c.txt\""); + assertEquals(pl.get("filename"), "\\a\\b\\c.txt"); + } + + @AfterClass + public static void after() { + // should be unnecessary + System.clearProperty("mail.mime.windowsfilenames"); + } +}
diff --git a/mail/src/test/java/javax/mail/search/SearchTermSerializationTest.java b/mail/src/test/java/javax/mail/search/SearchTermSerializationTest.java new file mode 100644 index 0000000..0c03c4b --- /dev/null +++ b/mail/src/test/java/javax/mail/search/SearchTermSerializationTest.java
@@ -0,0 +1,74 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.search; + +import java.io.*; +import java.util.Date; + +import javax.mail.*; +import javax.mail.search.*; +import javax.mail.internet.*; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; + +/** + * SearchTerm serialization test. + * + * @author Bill Shannon + */ + +public class SearchTermSerializationTest { + + @Test + public void testSerialization() throws IOException, ClassNotFoundException { + // construct a SearchTerm using all SearchTerm types + SearchTerm term = new AndTerm(new SearchTerm[] { + new BodyTerm("text"), + new FlagTerm(new Flags(Flags.Flag.RECENT), true), + new FromStringTerm("foo@bar"), + new HeaderTerm("X-Mailer", "dtmail"), + new MessageIDTerm("12345@sun.com"), + new MessageNumberTerm(42), + new NotTerm( + new OrTerm( + new ReceivedDateTerm(ReceivedDateTerm.LT, new Date()), + new RecipientStringTerm(Message.RecipientType.CC, "foo") + ) + ), + new RecipientTerm(MimeMessage.RecipientType.NEWSGROUPS, + new NewsAddress("comp.lang.java", "newshost")), + new SentDateTerm(SentDateTerm.NE, new Date()), + new SizeTerm(SizeTerm.LT, 1000), + new SubjectTerm("test") + }); + + // serialize it to a byte array + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(bos); + oos.writeObject(term); + bos.close(); + + // read it back in + ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); + ObjectInputStream ois = new ObjectInputStream(bis); + SearchTerm term2 = (SearchTerm)ois.readObject(); + + // compare it with the original + assertEquals(term, term2); + } +}
diff --git a/mail/src/test/resources/com/sun/mail/imap/protocol/uiddata b/mail/src/test/resources/com/sun/mail/imap/protocol/uiddata new file mode 100644 index 0000000..006e0ff --- /dev/null +++ b/mail/src/test/resources/com/sun/mail/imap/protocol/uiddata
@@ -0,0 +1,76 @@ +# +# Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. +# +# This program and the accompanying materials are made available under the +# terms of the Eclipse Public License v. 2.0, which is available at +# http://www.eclipse.org/legal/epl-2.0. +# +# This Source Code may also be made available under the following Secondary +# Licenses when the conditions for such availability set forth in the +# Eclipse Public License v. 2.0 are satisfied: GNU General Public License, +# version 2 with the GNU Classpath Exception, which is available at +# https://www.gnu.org/software/classpath/license.html. +# +# SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +# + +# +# Data to test UIDSet. +# + +TEST one UID +DATA 1 +EXPECT 1 + +TEST two UIDs +DATA 1,3 +EXPECT 1 3 + +TEST UID range +DATA 1:2 +EXPECT 1 2 + +TEST bigger UID range +DATA 1:3 +EXPECT 1 2 3 + +TEST two ranges +DATA 1:3,5:7 +EXPECT 1 2 3 5 6 7 + +TEST ranges and singles +DATA 1:3,5,7:9 +EXPECT 1 2 3 5 7 8 9 + +TEST many singles +DATA 1,3,5,7,9 +EXPECT 1 3 5 7 9 + +TEST max +DATA 1 +MAX 1 +EXPECT 1 + +TEST max2 +DATA 2 +MAX 2 +EXPECT 2 + +TEST max3 +DATA 1:2 +MAX 2 +EXPECT 1 2 + +TEST max4 +DATA 1:2 +MAX 1 1 +EXPECT 1 + +TEST max5 +DATA 1:4 +MAX 3 1:3 +EXPECT 1 2 3 + +TEST empty +DATA EMPTY +EXPECT EMPTY
diff --git a/mail/src/test/resources/com/sun/mail/util/uudata b/mail/src/test/resources/com/sun/mail/util/uudata new file mode 100644 index 0000000..56d29de --- /dev/null +++ b/mail/src/test/resources/com/sun/mail/util/uudata
@@ -0,0 +1,197 @@ +# +# Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. +# +# This program and the accompanying materials are made available under the +# terms of the Eclipse Public License v. 2.0, which is available at +# http://www.eclipse.org/legal/epl-2.0. +# +# This Source Code may also be made available under the following Secondary +# Licenses when the conditions for such availability set forth in the +# Eclipse Public License v. 2.0 are satisfied: GNU General Public License, +# version 2 with the GNU Classpath Exception, which is available at +# https://www.gnu.org/software/classpath/license.html. +# +# SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +# + +# +# Data to test uudecoder. +# +# Mostly tests error cases and ability to ignore errors. +# + +TEST a simple decode test +DATA +begin 644 encoder.buf +M=&AI<R!I<R!A('9E<GD@=F5R>2!V97)Y('9E<GD@=F5R>2!L;VYG(&QI;F4@ +4=&\@=&5S="!T:&4@9&5C;V1E<@H! + +end +EXPECT +this is a very very very very very long line to test the decoder +END + +TEST no begin +DATA +M=&AI<R!I<R!A('9E<GD@=F5R>2!V97)Y('9E<GD@=F5R>2!L;VYG(&QI;F4@ +4=&\@=&5S="!T:&4@9&5C;V1E<@H! + +end +EXPECT +EXCEPTION com.sun.mail.util.DecodingException +END + +TEST no end +DATA +begin 644 encoder.buf +M=&AI<R!I<R!A('9E<GD@=F5R>2!V97)Y('9E<GD@=F5R>2!L;VYG(&QI;F4@ +4=&\@=&5S="!T:&4@9&5C;V1E<@H! + +EXPECT +EXCEPTION com.sun.mail.util.DecodingException +END + +TEST no end, no empty line +DATA +begin 644 encoder.buf +M=&AI<R!I<R!A('9E<GD@=F5R>2!V97)Y('9E<GD@=F5R>2!L;VYG(&QI;F4@ +4=&\@=&5S="!T:&4@9&5C;V1E<@H! +EXPECT +EXCEPTION com.sun.mail.util.DecodingException +END + +TEST no begin, ignore errors +DATA ignoreMissingBeginEnd +M=&AI<R!I<R!A('9E<GD@=F5R>2!V97)Y('9E<GD@=F5R>2!L;VYG(&QI;F4@ +4=&\@=&5S="!T:&4@9&5C;V1E<@H! + +end +EXPECT +this is a very very very very very long line to test the decoder +END + +TEST no end, ignore errors +DATA ignoreMissingBeginEnd +begin 644 encoder.buf +M=&AI<R!I<R!A('9E<GD@=F5R>2!V97)Y('9E<GD@=F5R>2!L;VYG(&QI;F4@ +4=&\@=&5S="!T:&4@9&5C;V1E<@H! + +EXPECT +this is a very very very very very long line to test the decoder +END + +TEST no begin, no end, ignore errors +DATA ignoreMissingBeginEnd +M=&AI<R!I<R!A('9E<GD@=F5R>2!V97)Y('9E<GD@=F5R>2!L;VYG(&QI;F4@ +4=&\@=&5S="!T:&4@9&5C;V1E<@H! +EXPECT +this is a very very very very very long line to test the decoder +END + +TEST empty line, ignore errors +DATA ignoreMissingBeginEnd + +begin 644 encoder.buf +M=&AI<R!I<R!A('9E<GD@=F5R>2!V97)Y('9E<GD@=F5R>2!L;VYG(&QI;F4@ +4=&\@=&5S="!T:&4@9&5C;V1E<@H! +EXPECT +this is a very very very very very long line to test the decoder +END + +TEST empty line, no begin, ignore errors +DATA ignoreMissingBeginEnd + +M=&AI<R!I<R!A('9E<GD@=F5R>2!V97)Y('9E<GD@=F5R>2!L;VYG(&QI;F4@ +4=&\@=&5S="!T:&4@9&5C;V1E<@H! +EXPECT +this is a very very very very very long line to test the decoder +END + +TEST bad mode +DATA +begin xxx encoder.buf +M=&AI<R!I<R!A('9E<GD@=F5R>2!V97)Y('9E<GD@=F5R>2!L;VYG(&QI;F4@ +4=&\@=&5S="!T:&4@9&5C;V1E<@H! + +end +EXPECT +EXCEPTION com.sun.mail.util.DecodingException +END + +TEST bad mode, ignore errors +DATA ignoreErrors +begin xxx encoder.buf +M=&AI<R!I<R!A('9E<GD@=F5R>2!V97)Y('9E<GD@=F5R>2!L;VYG(&QI;F4@ +4=&\@=&5S="!T:&4@9&5C;V1E<@H! + +end +EXPECT +this is a very very very very very long line to test the decoder +END + +TEST bad filename +DATA +begin 644 +M=&AI<R!I<R!A('9E<GD@=F5R>2!V97)Y('9E<GD@=F5R>2!L;VYG(&QI;F4@ +4=&\@=&5S="!T:&4@9&5C;V1E<@H! + +end +EXPECT +EXCEPTION com.sun.mail.util.DecodingException +END + +TEST bad filename, ignore errors +DATA ignoreErrors +begin 644 +M=&AI<R!I<R!A('9E<GD@=F5R>2!V97)Y('9E<GD@=F5R>2!L;VYG(&QI;F4@ +4=&\@=&5S="!T:&4@9&5C;V1E<@H! + +end +EXPECT +this is a very very very very very long line to test the decoder +END + +TEST garbage data +DATA +begin 644 encoder.buf +XXX +M=&AI<R!I<R!A('9E<GD@=F5R>2!V97)Y('9E<GD@=F5R>2!L;VYG(&QI;F4@ +4=&\@=&5S="!T:&4@9&5C;V1E<@H! + +end +EXPECT +EXCEPTION com.sun.mail.util.DecodingException +END + +TEST garbage data (tab) +DATA +begin 644 encoder.buf + +M=&AI<R!I<R!A('9E<GD@=F5R>2!V97)Y('9E<GD@=F5R>2!L;VYG(&QI;F4@ +4=&\@=&5S="!T:&4@9&5C;V1E<@H! + +end +EXPECT +EXCEPTION com.sun.mail.util.DecodingException +END + +TEST garbage data, ignore errors +DATA ignoreErrors +begin 644 encoder.buf +XXX +M=&AI<R!I<R!A('9E<GD@=F5R>2!V97)Y('9E<GD@=F5R>2!L;VYG(&QI;F4@ +4=&\@=&5S="!T:&4@9&5C;V1E<@H! + +end +EXPECT +this is a very very very very very long line to test the decoder +END + +TEST ignore both kinds of errors +DATA ignoreErrors ignoreMissingBeginEnd +XXX +M=&AI<R!I<R!A('9E<GD@=F5R>2!V97)Y('9E<GD@=F5R>2!L;VYG(&QI;F4@ +4=&\@=&5S="!T:&4@9&5C;V1E<@H! +EXPECT +this is a very very very very very long line to test the decoder +END
diff --git a/mail/src/test/resources/javax/mail/internet/MailDateFormat_new.ser b/mail/src/test/resources/javax/mail/internet/MailDateFormat_new.ser new file mode 100644 index 0000000..2b31027 --- /dev/null +++ b/mail/src/test/resources/javax/mail/internet/MailDateFormat_new.ser Binary files differ
diff --git a/mail/src/test/resources/javax/mail/internet/MailDateFormat_old.ser b/mail/src/test/resources/javax/mail/internet/MailDateFormat_old.ser new file mode 100644 index 0000000..4c33b9b --- /dev/null +++ b/mail/src/test/resources/javax/mail/internet/MailDateFormat_old.ser Binary files differ
diff --git a/mail/src/test/resources/javax/mail/internet/addrfolddata b/mail/src/test/resources/javax/mail/internet/addrfolddata new file mode 100644 index 0000000..11c0f48 --- /dev/null +++ b/mail/src/test/resources/javax/mail/internet/addrfolddata Binary files differ
diff --git a/mail/src/test/resources/javax/mail/internet/addrlist b/mail/src/test/resources/javax/mail/internet/addrlist new file mode 100644 index 0000000..b1845a4 --- /dev/null +++ b/mail/src/test/resources/javax/mail/internet/addrlist
@@ -0,0 +1,773 @@ +Comment: + + Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +Comment: + + A set of test addresses stolen from the dtmail test suite, + with a few of my own. + Use with "addrtest -p <addrlist". + + grep for "Expected:" in output to find errors + + CAREFUL: a blank line in this file causes everything following it + to be ignored until a line starting with "From " (so that + addrtest -p can be pointed at a real mailbox). + +From start here +Strict: false +Comment: + This first set of addresses are all good. +To: ggere +Expect: 1 + ggere +To: /tmp/mail.out +Expect: 1 + /tmp/mail.out +To: +mailbox +Expect: 1 + +mailbox +To: ~user/mailbox +Expect: 1 + ~user/mailbox +To: ~/mailbox +Expect: 1 + ~/mailbox +To: /PN=x400.address/PRMD=ibmmail/ADMD=ibmx400/C=us/@mhs-mci.ebay +Expect: 1 + /PN=x400.address/PRMD=ibmmail/ADMD=ibmx400/C=us/@mhs-mci.ebay +To: ggere, /tmp/mail.out, +mailbox, ~user/mailbox, ~/mailbox, /PN=x400.address/PRMD=ibmmail/ADMD=ibmx400/C=us/@mhs-mci.ebay +Expect: 6 + ggere + /tmp/mail.out + +mailbox + ~user/mailbox + ~/mailbox + /PN=x400.address/PRMD=ibmmail/ADMD=ibmx400/C=us/@mhs-mci.ebay +To: ggere /tmp/mail.out +mailbox ~user/mailbox ~/mailbox /PN=x400.address/PRMD=ibmmail/ADMD=ibmx400/C=us/@mhs-mci.ebay +Expect: 6 + ggere + /tmp/mail.out + +mailbox + ~user/mailbox + ~/mailbox + /PN=x400.address/PRMD=ibmmail/ADMD=ibmx400/C=us/@mhs-mci.ebay +To: (x)<y@a>(z) +Expect: 1 + y@a +Comment: as above, but strict +Strict: true +To: ggere /tmp/mail.out +mailbox ~user/mailbox ~/mailbox /PN=x400.address/PRMD=ibmmail/ADMD=ibmx400/C=us/@mhs-mci.ebay +Expect: Exception javax.mail.internet.AddressException: Local address contains control or whitespace in string ``ggere /tmp/mail.out +mailbox ~user/mailbox ~/mailbox /PN=x400.address/PRMD=ibmmail/ADMD=ibmx400/C=us/@mhs-mci.ebay'' +Strict: false +To: Mad Genius <george@boole> +Expect: 1 + george@boole +To: "C'est bien moche" <paris@france> +Expect: 1 + paris@france +To: "know, any, famous" <french@physicists> +Expect: 1 + french@physicists +To: laborious (But Bug Free) +Expect: 1 + laborious +To: confused (about, being, french) +Expect: 1 + confused +To: it (takes, no (time, at) all) +Expect: 1 + it +To: two@weeks (It Will Take) +Expect: 1 + two@weeks +To: it@is (brilliant (genius, and) superb) +Expect: 1 + it@is +To: if@you (could, see (almost, as, (badly, you) would) agree) +Expect: 1 + if@you +To: cannot@waste (My, Intellectual, Cycles) +Expect: 1 + cannot@waste +To: users:get,what,they,deserve; +Expect: 1 + users:get,what,they,deserve; +To: "C'est bien moche" <paris@france>, Mad Genius <george@boole>, two@weeks (It Will Take), /tmp/mail.out, laborious (But Bug Free), cannot@waste (My, Intellectual, Cycles), users:get,what,they,deserve;, it (takes, no (time, at) all), if@you (could, see (almost, as, (badly, you) would) agree), "know, any, famous" <French@physicists>, it@is (brilliant (genius, and) superb), confused (about, being, french) +Expect: 12 + paris@france + george@boole + two@weeks + /tmp/mail.out + laborious + cannot@waste + users:get,what,they,deserve; + it + if@you + French@physicists + it@is + confused +To: ggere, /tmp/mail.out, +mailbox, ~user/mailbox, ~/mailbox, /PN=x400.address/PRMD=ibmmail/ADMD=ibmx400/C=us/@mhs-mci.ebay, "C'est bien moche" <paris@france>, Mad Genius <george@boole>, two@weeks (It Will Take), /tmp/mail.out, laborious (But Bug Free), cannot@waste (My, Intellectual, Cycles), users:get,what,they,deserve;, it (takes, no (time, at) all), if@you (could, see (almost, as, (badly, you) would) agree), "know, any, famous" <French@physicists>, it@is (brilliant (genius, and) superb), confused (about, being, french) +Expect: 18 + ggere + /tmp/mail.out + +mailbox + ~user/mailbox + ~/mailbox + /PN=x400.address/PRMD=ibmmail/ADMD=ibmx400/C=us/@mhs-mci.ebay + paris@france + george@boole + two@weeks + /tmp/mail.out + laborious + cannot@waste + users:get,what,they,deserve; + it + if@you + French@physicists + it@is + confused +To: testa testb testc /tmp/mail.out ~ggere/mail.out testd teste +Expect: 7 + testa + testb + testc + /tmp/mail.out + ~ggere/mail.out + testd + teste +Comment: as above, but strict +Strict: true +To: testa testb testc /tmp/mail.out ~ggere/mail.out testd teste +Expect: Exception javax.mail.internet.AddressException: Local address contains control or whitespace in string ``testa testb testc /tmp/mail.out ~ggere/mail.out testd teste'' +Strict: false +To: testa, testb , testc /tmp/mail.out ,~ggere/mail.out, ,testd,teste,,, +Expect: 7 + testa + testb + testc + /tmp/mail.out + ~ggere/mail.out + testd + teste +Comment: as above, but strict +Strict: true +To: testa, testb , testc /tmp/mail.out ,~ggere/mail.out, ,testd,teste,,, +Expect: Exception javax.mail.internet.AddressException: Local address contains control or whitespace in string ``testa testb testc /tmp/mail.out ~ggere/mail.out testd teste'' +Strict: false +To: testa testb <testc@testd> +Expect: 1 + testc@testd +To: Adam S Moskowitz <adamm@onion.inset.com>, + Andrew Gollan <adjg@softway.sw.OZ.AU>, + Bret Anthony Marquis <bam@trout.nosc.MIL>, + Bill Shannon <shannon@Sun.COM>, Bob Gray <rgray@UsWest.COM>, + Brian Ellis <bri@Boulder.Colorado.EDU>, + Don Coleman <coleman@legato.COM>, + Dennis Ritchie <dmr@research.ATT.COM>, Evi Nemeth <evi@Colorado.EDU>, + Lori Grob <grob@chorus.FR>, Paula Hawthorn <hawthorn@HPL.HP.COM>, + Andrew Hume <andrew@research.ATT.COM>, + Jaap Akkerhuis <jaap@research.ATT.COM>, + Jim R Oldroyd <jr@onion.inset.com>, + John Quarterman <jsq@longway.TIC.COM>, + Judy DesHarnais <judy@USENIX.ORG>, Karen Shannon <kas@Eng>, + Ken McDonell <kenj@Pyramid.COM>, + Rob Kolstad <kolstad@rmtc.Central.Sun.COM>, + Paul Kooros <kooros@tigger.cs.Colorado.EDU>, + Robert Elz <kre@Munnari.OZ.AU>, + Miriam Amos Nihart <miriam@decwet.enet.DEC.COM>, + "Mike O'Dell" <mo@gizmo.Bellcore.COM>, + Sharon Murrel <eowyn@research.ATT.COM>, + Peg Schafer <peg@media-lab.media.MIT.EDU>, Peter Salus <peter@SUG.ORG>, + Bill Shannon <shannon@Sun.COM>, Stu Feldman <sif@Bellcore.COM>, + Stephen Williams <stevew@netboss1.trg.saic.com>, + Teus Hagen <teus@oce.NL>, Trent Hein <trent@xor.COM>, + Thomas Alan Wood <twood@capmkt.COM>, + Michael Ubell <"mttam::ubell"@SFBay.ENet.DEC.COM>, + Martha Zimet <zimet@Corp> +Expect: 34 + adamm@onion.inset.com + adjg@softway.sw.OZ.AU + bam@trout.nosc.MIL + shannon@Sun.COM + rgray@UsWest.COM + bri@Boulder.Colorado.EDU + coleman@legato.COM + dmr@research.ATT.COM + evi@Colorado.EDU + grob@chorus.FR + hawthorn@HPL.HP.COM + andrew@research.ATT.COM + jaap@research.ATT.COM + jr@onion.inset.com + jsq@longway.TIC.COM + judy@USENIX.ORG + kas@Eng + kenj@Pyramid.COM + kolstad@rmtc.Central.Sun.COM + kooros@tigger.cs.Colorado.EDU + kre@Munnari.OZ.AU + miriam@decwet.enet.DEC.COM + mo@gizmo.Bellcore.COM + eowyn@research.ATT.COM + peg@media-lab.media.MIT.EDU + peter@SUG.ORG + shannon@Sun.COM + sif@Bellcore.COM + stevew@netboss1.trg.saic.com + teus@oce.NL + trent@xor.COM + twood@capmkt.COM + "mttam::ubell"@SFBay.ENet.DEC.COM + zimet@Corp +To: '"a,b"' +Expect: 1 + '"a,b"' +To: 'a, b' +Expect: 2 + 'a + b' +To: "a,b" +Expect: 1 + "a,b" +To: shakey!"a,b" +Expect: 1 + shakey!"a,b" +To: "foo bar" +Expect: 1 + "foo bar" +To: (Foo Bar) foo@bar +Expect: 1 + foo@bar +To: (Foo Bar) foo@bar, (My Name) my@name +Expect: 2 + foo@bar + my@name +To: Ben Keitch <">"@openworld.co.uk> +Expect: 1 + ">"@openworld.co.uk +To: Ben Keitch <">\""\>@openworld.co.uk> +Expect: 1 + ">\""\>@openworld.co.uk +To: A Group:Chris Jones <c@a.test>,joe@where.test,John <jdoe@one.test>; +Expect: 1 + A Group:Chris Jones <c@a.test>,joe@where.test,John <jdoe@one.test>; +To: Undisclosed-Recipients:; +Expect: 1 + Undisclosed-Recipients:; +To: "Mark Hapner" <mark.hapner@eng.sun.com>, <mark.himelstein@eng.sun.com> +Expect: 2 + mark.hapner@eng.sun.com + mark.himelstein@eng.sun.com +To: <rem@rockit> (Ralph Miller), <shannon@datsun> (Bill Shannon) +Expect: 2 + rem@rockit + shannon@datsun +To: "" <javamail@sun.com> +Expect: 1 + javamail@sun.com +To: sun!joe (joe@sun) +Expect: 1 + sun!joe +To: "john doe" (john.doe@a.com) +Expect: 1 + "john doe" +To: (test domain literal verification) b@[1.2.3.4] +Expect: 1 + b@[1.2.3.4] +Comment: parsed as a header, we pick out the address from the "comment" +Header: true +To: "john doe" (john.doe@a.com) +Expect: 1 + john.doe@a.com +Comment: test that headers are unfolded before parsing +To: "John + Smith" <jsmith@ + crappy-email-archiving-company.com> +Expect: 1 + jsmith@ crappy-email-archiving-company.com +Header: false +To: (Test) <djoe@zimbra.com>,djoe@zimbra.com (Test) +Expect: 2 + djoe@zimbra.com + djoe@zimbra.com +Comment: + All the following addresses are bad. +To: , +Expect: 0 +To: < +Expect: Exception javax.mail.internet.AddressException: Missing '>' in string ``<'' at position 1 +To: > +Expect: Exception javax.mail.internet.AddressException: Missing '<' in string ``>'' at position 0 +To: " +Expect: Exception javax.mail.internet.AddressException: Missing '"' in string ``"'' at position 1 +To: >>> +Expect: Exception javax.mail.internet.AddressException: Missing '<' in string ``>>>'' at position 0 +To: <<< +Expect: Exception javax.mail.internet.AddressException: Missing '>' in string ``<<<'' at position 3 +To: paris@france> +Expect: Exception javax.mail.internet.AddressException: Missing '<' in string ``paris@france>'' at position 12 +To: "C'est bien moche <paris@france> +Expect: Exception javax.mail.internet.AddressException: Missing '"' in string ``"C'est bien moche <paris@france>'' at position 32 +To: "C'est bien moche" <paris@france +Expect: Exception javax.mail.internet.AddressException: Missing '>' in string ``"C'est bien moche" <paris@france'' at position 32 +To: "C'est bien moche" paris@france> +Expect: Exception javax.mail.internet.AddressException: Missing '<' in string ``"C'est bien moche" paris@france>'' at position 31 +To: "C'est bien moche" <paris@> +Expect: Exception javax.mail.internet.AddressException: Missing domain in string ``paris@'' +To: laborious but (Bug Free) +Expect: Exception javax.mail.internet.AddressException: Local address contains control or whitespace in string ``laborious but'' +To: confused (about, being, french +Expect: Exception javax.mail.internet.AddressException: Missing ')' in string ``confused (about, being, french'' at position 30 +To: it@is (brilliant (genius, and superb) +Expect: Exception javax.mail.internet.AddressException: Missing ')' in string ``it@is (brilliant (genius, and superb)'' at position 37 +To: it@is (brilliant (genius), and superb +Expect: Exception javax.mail.internet.AddressException: Missing ')' in string ``it@is (brilliant (genius), and superb'' at position 37 +To: if@you (could, see (almost, as, (badly, you would) agree) +Expect: Exception javax.mail.internet.AddressException: Missing ')' in string ``if@you (could, see (almost, as, (badly, you would) agree)'' at position 57 +To: if@you (could, see (almost, as, (badly, you) would) agree +Expect: Exception javax.mail.internet.AddressException: Missing ')' in string ``if@you (could, see (almost, as, (badly, you) would) agree'' at position 57 +To: two@ (It Will Take) +Expect: Exception javax.mail.internet.AddressException: Missing domain in string ``two@'' +To: two@ +Expect: Exception javax.mail.internet.AddressException: Missing domain in string ``two@'' +To: two@weeks (It Will Take +Expect: Exception javax.mail.internet.AddressException: Missing ')' in string ``two@weeks (It Will Take'' at position 23 +To: <paris@france +Expect: Exception javax.mail.internet.AddressException: Missing '>' in string ``<paris@france'' at position 13 +To: paris@france> +Expect: Exception javax.mail.internet.AddressException: Missing '<' in string ``paris@france>'' at position 12 +To: @france +Expect: Exception javax.mail.internet.AddressException: Missing local name in string ``@france'' +To: @ +Expect: Exception javax.mail.internet.AddressException: Missing local name in string ``@'' +To: < +Expect: Exception javax.mail.internet.AddressException: Missing '>' in string ``<'' at position 1 +To: > +Expect: Exception javax.mail.internet.AddressException: Missing '<' in string ``>'' at position 0 +To: ( +Expect: Exception javax.mail.internet.AddressException: Missing ')' in string ``('' at position 1 +To: ) +Expect: Exception javax.mail.internet.AddressException: Missing '(' in string ``)'' at position 0 +To: " +Expect: Exception javax.mail.internet.AddressException: Missing '"' in string ``"'' at position 1 +To: friends-list:;@mastodon.CS.Berkeley.EDU +Expect: Exception javax.mail.internet.AddressException: Missing local name in string ``@mastodon.CS.Berkeley.EDU'' +To: a@.com +Expect: Exception javax.mail.internet.AddressException: Domain starts with dot in string ``a@.com'' +To: a@com. +Expect: Exception javax.mail.internet.AddressException: Domain ends with dot in string ``a@com.'' +To: a@b..com +Expect: Exception javax.mail.internet.AddressException: Domain contains dot-dot in string ``a@b..com'' +To: .a@b.com +Expect: Exception javax.mail.internet.AddressException: Local address starts with dot in string ``.a@b.com'' +To: a.@b.com +Expect: Exception javax.mail.internet.AddressException: Local address ends with dot in string ``a.@b.com'' +To: a..b@b.com +Expect: Exception javax.mail.internet.AddressException: Local address contains dot-dot in string ``a..b@b.com'' +To: a@com +Expect: Exception javax.mail.internet.AddressException: Domain contains control or whitespace in string ``a@com'' +To: a@b.com +Expect: Exception javax.mail.internet.AddressException: Local address contains control or whitespace in string ``a@b.com'' +To: ab +Expect: Exception javax.mail.internet.AddressException: Local address contains control or whitespace in string ``ab'' +To: @b.com +Expect: Exception javax.mail.internet.AddressException: Missing local name in string ``@b.com'' +To: javamail@Sun.COM; +Expect: Exception javax.mail.internet.AddressException: Illegal semicolon, not in group in string ``javamail@Sun.COM;'' at position 16 +To: Undisclosed-Recipient:;@jupiter.activeisp.com;;;;; +Expect: Exception javax.mail.internet.AddressException: Illegal semicolon, not in group in string ``Undisclosed-Recipient:;@jupiter.activeisp.com;;;;;'' at position 45 +To: Undisclosed-Recipient:;@java.sun.com;;; +Expect: Exception javax.mail.internet.AddressException: Illegal semicolon, not in group in string ``Undisclosed-Recipient:;@java.sun.com;;;'' at position 36 +To: "bejjzcfr@aol.com +Expect: Exception javax.mail.internet.AddressException: Missing '"' in string ``"bejjzcfr@aol.com'' at position 17 +To: "butbp@msn.com +Expect: Exception javax.mail.internet.AddressException: Missing '"' in string ``"butbp@msn.com'' at position 14 +To: "@msn.com +Expect: Exception javax.mail.internet.AddressException: Missing '"' in string ``"@msn.com'' at position 9 +To: "@starband.net +Expect: Exception javax.mail.internet.AddressException: Missing '"' in string ``"@starband.net'' at position 14 +To: jko23l1@klds.java.sun.com;, ad@java.sun.com +Expect: Exception javax.mail.internet.AddressException: Illegal semicolon, not in group in string ``jko23l1@klds.java.sun.com;, ad@java.sun.com'' at position 25 +To: jko23l1@klds;ad;;;; +Expect: Exception javax.mail.internet.AddressException: Illegal semicolon, not in group in string ``jko23l1@klds;ad;;;;'' at position 12 +To: mailto:mailto:snoa101custserv@hotmail.com +Expect: Exception javax.mail.internet.AddressException: Nested group in string ``mailto:mailto:snoa101custserv@hotmail.com'' at position 13 +To: mailto:mailto:snoa101custserv@hotmail.com, foo@bar.com +Expect: Exception javax.mail.internet.AddressException: Nested group in string ``mailto:mailto:snoa101custserv@hotmail.com, foo@bar.com'' at position 13 +To: "@foo.com, a@b.com +Expect: Exception javax.mail.internet.AddressException: Missing '"' in string ``"@foo.com, a@b.com'' at position 18 +To: <@foo.com, a@b.com +Expect: Exception javax.mail.internet.AddressException: Missing '>' in string ``<@foo.com, a@b.com'' at position 18 +To: <a@b.com> <c@d.com> +Expect: Exception javax.mail.internet.AddressException: Extra route-addr in string ``<a@b.com> <c@d.com>'' at position 10 +To: yyiohohjjlj[ojolhji@hotmail.com +Expect: Exception javax.mail.internet.AddressException: Missing ']' in string ``yyiohohjjlj[ojolhji@hotmail.com'' at position 31 +To: www.adslexpress.com.tw--?????q?H?[?o??-0@kathmandu.sun.com +Expect: Exception javax.mail.internet.AddressException: Missing ']' in string ``www.adslexpress.com.tw--?????q?H?[?o??-0@kathmandu.sun.com'' at position 58 +Cc: webmail.hinet.net@nwkea-mail-1.sun.com (¤¤µØºô¶¶l¥ó), + www.adslexpress.com.tw--¤¤µØ¹q«H¥[ªo¯¸-0@nwkea-mail-1.sun.com, + www.cityfamily.com.tw---ºô¸ô¦P¾Ç·|-0@nwkea-mail-1.sun.com +Expect: Exception javax.mail.internet.AddressException: Missing ']' in string ``webmail.hinet.net@nwkea-mail-1.sun.com (?????????l??), + www.adslexpress.com.tw--?????q?H?[?o??-0@nwkea-mail-1.sun.com, + www.cityfamily.com.tw---?????P???|-0@nwkea-mail-1.sun.com'' at position 181 +To: www.adslexpress.com.tw--¤¤µØ¹q«H¥[ªo¯¸-0@patan.sun.com +Expect: Exception javax.mail.internet.AddressException: Missing ']' in string ``www.adslexpress.com.tw--?????q?H?[?o??-0@patan.sun.com'' at position 54 +To: Giant.White.Beans;Great.Northern.Beans;Lima.Beans;Navy.Beans@4email-03;;;;;;;;;;;;;;;;;; +Expect: Exception javax.mail.internet.AddressException: Illegal semicolon, not in group in string ``Giant.White.Beans;Great.Northern.Beans;Lima.Beans;Navy.Beans@4email-03;;;;;;;;;;;;;;;;;;'' at position 17 +To: \(^^/@nwkea-mail-2.sun.com +Expect: Exception javax.mail.internet.AddressException: Missing ')' in string ``\(^^/@nwkea-mail-2.sun.com'' at position 26 +Cc: <tom.ross@sun.com>, Cc: <javamail@sun.com>, Cc: <javahi@sun.com>, + Cc: <javadoc-tool@sun.com>, Cc: <javacard-feedback@sun.com> +Expect: Exception javax.mail.internet.AddressException: Nested group in string ``<tom.ross@sun.com>, Cc: <javamail@sun.com>, Cc: <javahi@sun.com>, + Cc: <javadoc-tool@sun.com>, Cc: <javacard-feedback@sun.com>'' at position 46 +From: <hello@sun.com>, java2d-interest@sun.com, java3d-interest@sun.com, + javadoc-tool@sun.com, javamail@sun.com, javasound-interest@sun.com, + javaspeech-interest@sun.com, jdbc@sun.com, jim.cook@sun.com, + joachim.heck@sun.com, kammy@sun.com, kenneth.cheung@sun.com, + Wed@brmea-mail-3.sun.com, multipart/alternative@brmea-mail-3.sun.com, + text/html@brmea-mail-3.sun.com; +Expect: Exception javax.mail.internet.AddressException: Illegal semicolon, not in group in string ``<hello@sun.com>, java2d-interest@sun.com, java3d-interest@sun.com, + javadoc-tool@sun.com, javamail@sun.com, javasound-interest@sun.com, + javaspeech-interest@sun.com, jdbc@sun.com, jim.cook@sun.com, + joachim.heck@sun.com, kammy@sun.com, kenneth.cheung@sun.com, + Wed@brmea-mail-3.sun.com, multipart/alternative@brmea-mail-3.sun.com, + text/html@brmea-mail-3.sun.com;'' at position 362 +To: "???Y???z???~?j?q?n?? +Expect: Exception javax.mail.internet.AddressException: Missing '"' in string ``"???Y???z???~?j?q?n??'' at position 21 +To: "ZETA CELULARES +Expect: Exception javax.mail.internet.AddressException: Missing '"' in string ``"ZETA CELULARES'' at position 15 +To: "Investigaci?n para el desarrollo de micro-emprendimientos Schreiber TE 4431-7050 +Expect: Exception javax.mail.internet.AddressException: Missing '"' in string ``"Investigaci?n para el desarrollo de micro-emprendimientos Schreiber TE 4431-7050'' at position 81 +To: "Aproveche* +Expect: Exception javax.mail.internet.AddressException: Missing '"' in string ``"Aproveche*'' at position 11 +To: "ULTIMA. OPORTUNIDAD* +Expect: Exception javax.mail.internet.AddressException: Missing '"' in string ``"ULTIMA. OPORTUNIDAD*'' at position 21 +Cc: 55:55@07, 57:18@07, 36:39@10, 36:42@10, streetwalker!1987, streetwalker!5, + streetwalker!87, <8710051455.AA08484@oak.sunecd.com>, <oak!kinnear>, + streetwalker!AA00159;, streetwalker!AA08484;, streetwalker!AA16178;, + streetwalker!Date:, streetwalker!EDT, streetwalker!Ecd.Sun.COM, + streetwalker!From, streetwalker!From:, streetwalker!Message-Id:, + streetwalker!Mon, streetwalker!Oct, streetwalker!PDT, streetwalker!RO, + streetwalker!Received:, streetwalker!Return-Path:, + streetwalker!Status:, streetwalker!To:, streetwalker!by, + streetwalker!from, streetwalker!id, streetwalker!oak.sunecd.com, + streetwalker!root, streetwalker!rr, streetwalker!streetwalker.sun.com +Expect: Exception javax.mail.internet.AddressException: Nested group in string ``55:55@07, 57:18@07, 36:39@10, 36:42@10, streetwalker!1987, streetwalker!5, + streetwalker!87, <8710051455.AA08484@oak.sunecd.com>, <oak!kinnear>, + streetwalker!AA00159;, streetwalker!AA08484;, streetwalker!AA16178;, + streetwalker!Date:, streetwalker!EDT, streetwalker!Ecd.Sun.COM, + streetwalker!From, streetwalker!From:, streetwalker!Message-Id:, + streetwalker!Mon, streetwalker!Oct, streetwalker!PDT, streetwalker!RO, + streetwalker!Received:, streetwalker!Return-Path:, + streetwalker!Status:, streetwalker!To:, streetwalker!by, + streetwalker!from, streetwalker!id, streetwalker!oak.sunecd.com, + streetwalker!root, streetwalker!rr, streetwalker!streetwalker.sun.com'' at position 12 +To: Re:@cachaca, Subject:@cachaca, and@cachaca, getmclist@cachaca, glenn@ivrel, + iconv@cachaca, questions@cachaca +Expect: Exception javax.mail.internet.AddressException: Nested group in string ``Re:@cachaca, Subject:@cachaca, and@cachaca, getmclist@cachaca, glenn@ivrel, + iconv@cachaca, questions@cachaca'' at position 20 +To: " <string@string.ru> +Expect: Exception javax.mail.internet.AddressException: Missing '"' in string ``" <string@string.ru> '' at position 22 +To: [xxx]foo@bar.org +Expect: Exception in thread "main" javax.mail.internet.AddressException: Local address contains illegal character in string ``[xxx]foo@bar.org'' +Comment: + Now parse all the above bad addresses as if they were in a header, + expecting no failures. +Header: true +To: , +Expect: 0 +To: < +Expect: 1 + < +To: > +Expect: 1 + > +To: " +Expect: 1 + " +To: >>> +Expect: 1 + >>> +To: <<< +Expect: 1 + <<< +To: paris@france> +Expect: 1 + paris@france> +To: "C'est bien moche <paris@france> +Expect: 1 + paris@france +To: "C'est bien moche" <paris@france +Expect: 1 + "C'est bien moche" <paris@france +To: "C'est bien moche" paris@france> +Expect: 1 + "C'est bien moche" paris@france> +To: "C'est bien moche" <paris@> +Expect: 1 + paris@ +To: laborious but (Bug Free) +Expect: 1 + laborious but +To: confused (about, being, french +Expect: 3 + confused + being + french +To: it@is (brilliant (genius, and superb) +Expect: 1 + it@is +To: it@is (brilliant (genius), and superb +Expect: 2 + it@is + and superb +To: if@you (could, see (almost, as, (badly, you would) agree) +Expect: 2 + if@you + see +To: if@you (could, see (almost, as, (badly, you) would) agree +Expect: 2 + if@you + see +To: two@ (It Will Take) +Expect: 1 + two@ +To: two@ +Expect: 1 + two@ +To: two@weeks (It Will Take +Expect: 1 + two@weeks +To: <paris@france +Expect: 1 + <paris@france +To: paris@france> +Expect: 1 + paris@france> +To: @france +Expect: 1 + @france +To: @ +Expect: 1 + @ +To: < +Expect: 1 + < +To: > +Expect: 1 + > +To: ( +Expect: 0 +To: ) +Expect: 1 + ) +To: " +Expect: 1 + " +To: friends-list:;@mastodon.CS.Berkeley.EDU +Expect: 1 + friends-list:;@mastodon.CS.Berkeley.EDU +To: a@.com +Expect: 1 + a@.com +To: a@com. +Expect: 1 + a@com. +To: a@b..com +Expect: 1 + a@b..com +To: a@com +Expect: 1 + a@com +To: a@b.com +Expect: 1 + a@b.com +To: ab +Expect: 1 + ab +To: @b.com +Expect: 1 + @b.com +To: javamail@Sun.COM; +Expect: 1 + javamail@Sun.COM +To: Undisclosed-Recipient:;@jupiter.activeisp.com;;;;; +Expect: 1 + Undisclosed-Recipient:;@jupiter.activeisp.com +To: Undisclosed-Recipient:;@java.sun.com;;; +Expect: 1 + Undisclosed-Recipient:;@java.sun.com +To: "bejjzcfr@aol.com +Expect: 1 + "bejjzcfr@aol.com +To: "butbp@msn.com +Expect: 1 + "butbp@msn.com +To: "@msn.com +Expect: 1 + "@msn.com +To: "@starband.net +Expect: 1 + "@starband.net +To: jko23l1@klds.java.sun.com;, ad@java.sun.com +Expect: 2 + jko23l1@klds.java.sun.com + ad@java.sun.com +To: jko23l1@klds;ad;;;; +Expect: 2 + jko23l1@klds + ad +To: mailto:mailto:snoa101custserv@hotmail.com +Expect: 1 + snoa101custserv@hotmail.com +To: mailto:mailto:snoa101custserv@hotmail.com, foo@bar.com +Expect: 2 + snoa101custserv@hotmail.com + foo@bar.com +To: "@foo.com, a@b.com +Expect: 2 + "@foo.com + a@b.com +To: <@foo.com, a@b.com +Expect: 2 + <@foo.com + a@b.com +To: <a@b.com> <c@d.com> +Expect: 2 + a@b.com + c@d.com +To: yyiohohjjlj[ojolhji@hotmail.com +Expect: 1 + yyiohohjjlj[ojolhji@hotmail.com +To: www.adslexpress.com.tw--?????q?H?[?o??-0@kathmandu.sun.com +Expect: 1 + www.adslexpress.com.tw--?????q?H?[?o??-0@kathmandu.sun.com +Cc: webmail.hinet.net@nwkea-mail-1.sun.com (¤¤µØºô¶¶l¥ó), + www.adslexpress.com.tw--¤¤µØ¹q«H¥[ªo¯¸-0@nwkea-mail-1.sun.com, + www.cityfamily.com.tw---ºô¸ô¦P¾Ç·|-0@nwkea-mail-1.sun.com +Expect: 3 + webmail.hinet.net@nwkea-mail-1.sun.com + www.adslexpress.com.tw--¤¤µØ¹q«H¥[ªo¯¸-0@nwkea-mail-1.sun.com + www.cityfamily.com.tw---ºô¸ô¦P¾Ç·|-0@nwkea-mail-1.sun.com +From: ?R?a????@patan.sun.com +Expect: 1 + ?R?a????@patan.sun.com +To: www.adslexpress.com.tw--?????q?H?[?o??-0@patan.sun.com +Expect: 1 + www.adslexpress.com.tw--?????q?H?[?o??-0@patan.sun.com +To: Giant.White.Beans;Great.Northern.Beans;Lima.Beans;Navy.Beans@4email-03;;;;;;;;;;;;;;;;;; +Expect: 4 + Giant.White.Beans + Great.Northern.Beans + Lima.Beans + Navy.Beans@4email-03 +To: \(^^/@nwkea-mail-2.sun.com +Expect: 1 + \\ +Cc: <tom.ross@sun.com>, Cc: <javamail@sun.com>, Cc: <javahi@sun.com>, + Cc: <javadoc-tool@sun.com>, Cc: <javacard-feedback@sun.com> +Expect: 5 + tom.ross@sun.com + javamail@sun.com + javahi@sun.com + javadoc-tool@sun.com + javacard-feedback@sun.com +From: <hello@sun.com>, java2d-interest@sun.com, java3d-interest@sun.com, + javadoc-tool@sun.com, javamail@sun.com, javasound-interest@sun.com, + javaspeech-interest@sun.com, jdbc@sun.com, jim.cook@sun.com, + joachim.heck@sun.com, kammy@sun.com, kenneth.cheung@sun.com, + Wed@brmea-mail-3.sun.com, multipart/alternative@brmea-mail-3.sun.com, + text/html@brmea-mail-3.sun.com; +Expect: 15 + hello@sun.com + java2d-interest@sun.com + java3d-interest@sun.com + javadoc-tool@sun.com + javamail@sun.com + javasound-interest@sun.com + javaspeech-interest@sun.com + jdbc@sun.com + jim.cook@sun.com + joachim.heck@sun.com + kammy@sun.com + kenneth.cheung@sun.com + Wed@brmea-mail-3.sun.com + multipart/alternative@brmea-mail-3.sun.com + text/html@brmea-mail-3.sun.com +To: "???Y???z???~?j?q?n?? +Expect: 1 + "???Y???z???~?j?q?n?? +To: "ZETA CELULARES +Expect: 1 + "ZETA CELULARES +To: "Investigaci?n para el desarrollo de micro-emprendimientos Schreiber TE 4431-7050 +Expect: 1 + "Investigaci?n para el desarrollo de micro-emprendimientos Schreiber TE 4431-7050 +To: "Aproveche* +Expect: 1 + "Aproveche* +To: "ULTIMA. OPORTUNIDAD* +Expect: 1 + "ULTIMA. OPORTUNIDAD* +Cc: 55:55@07, 57:18@07, 36:39@10, 36:42@10, streetwalker!1987, streetwalker!5, + streetwalker!87, <8710051455.AA08484@oak.sunecd.com>, <oak!kinnear>, + streetwalker!AA00159;, streetwalker!AA08484;, streetwalker!AA16178;, + streetwalker!Date:, streetwalker!EDT, streetwalker!Ecd.Sun.COM, + streetwalker!From, streetwalker!From:, streetwalker!Message-Id:, + streetwalker!Mon, streetwalker!Oct, streetwalker!PDT, streetwalker!RO, + streetwalker!Received:, streetwalker!Return-Path:, + streetwalker!Status:, streetwalker!To:, streetwalker!by, + streetwalker!from, streetwalker!id, streetwalker!oak.sunecd.com, + streetwalker!root, streetwalker!rr, streetwalker!streetwalker.sun.com +Expect: 24 + 55:55@07, 57:18@07, 36:39@10, 36:42@10, streetwalker!1987, streetwalker!5, streetwalker!87, <8710051455.AA08484@oak.sunecd.com>, <oak!kinnear>, streetwalker!AA00159; + streetwalker!AA08484 + streetwalker!AA16178 + streetwalker!Date: + streetwalker!EDT + streetwalker!Ecd.Sun.COM + streetwalker!From + streetwalker!From: + streetwalker!Message-Id: + streetwalker!Mon + streetwalker!Oct + streetwalker!PDT + streetwalker!RO + streetwalker!Received: + streetwalker!Return-Path: + streetwalker!Status: + streetwalker!To: + streetwalker!by + streetwalker!from + streetwalker!id + streetwalker!oak.sunecd.com + streetwalker!root + streetwalker!rr + streetwalker!streetwalker.sun.com +To: Re:@cachaca, Subject:@cachaca, and@cachaca, getmclist@cachaca, glenn@ivrel, + iconv@cachaca, questions@cachaca +Expect: 7 + @cachaca + @cachaca + and@cachaca + getmclist@cachaca + glenn@ivrel + iconv@cachaca + questions@cachaca +To: " <string@string.ru> +Expect: 1 + string@string.ru +Header: false
diff --git a/mail/src/test/resources/javax/mail/internet/folddata b/mail/src/test/resources/javax/mail/internet/folddata new file mode 100644 index 0000000..4acc077 --- /dev/null +++ b/mail/src/test/resources/javax/mail/internet/folddata Binary files differ
diff --git a/mail/src/test/resources/javax/mail/internet/paramdata b/mail/src/test/resources/javax/mail/internet/paramdata new file mode 100644 index 0000000..48e673f --- /dev/null +++ b/mail/src/test/resources/javax/mail/internet/paramdata
@@ -0,0 +1,139 @@ +Comment: + + Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +Comment: + + A set of test headers to test parameter list parsing and + especially RFC 2231 decoding. + Use with "paramtest -p <paramdata". + + All tests assume "mail.mime.decodeparameters" is set to "true", + which is done at the start of paramtest. + + CAREFUL: a blank line in this file causes everything following it + to be ignored until a line starting with "From " (so that + paramtest -p can be pointed at a real mailbox). + +From start here +Comment: + This set of headers are all good and test RFC 2231 decoding. +Content-Type: application/x-stuff; + title*0*=us-ascii'en'This%20is%20even%20more%20; + title*1*=%2A%2A%2Afun%2A%2A%2A%20; + title*2="isn't it!" +Expect: 1 + title=This is even more ***fun*** isn't it! +Content-Type: application/x-stuff; + title*0*=''This%20is%20even%20more%20; + title*1*=%2A%2A%2Afun%2A%2A%2A%20; + title*2="isn't it!" +Expect: 1 + title=This is even more ***fun*** isn't it! +Content-Type: application/x-stuff; + title*2="isn't it!"; + title*1*=%2A%2A%2Afun%2A%2A%2A%20; + title*0*=us-ascii'en'This%20is%20even%20more%20 +Expect: 1 + title=This is even more ***fun*** isn't it! +Content-Type: application/x-stuff; + title*2="isn't it!"; + title*1*=%2A%2A%2Afun%2A%2A%2A%20; + title*0*=us-ascii'en'This%20is%20even%20more%20; + name*0="one "; + name*1="two "; + name*2="three"; + p*=us-ascii'en'This%20is%20fun; +Expect: 3 + title=This is even more ***fun*** isn't it! + name=one two three + p=This is fun +Content-Type: application/x-stuff; + title*2="isn't it!"; + title*1*=%2A%2A%2Afun%2A%2A%2A%20; + title*0*=us-ascii'en'This%20is%20even%20more%20; + name*0="one "; + name*1="two "; + name*2="three"; + p*=us-ascii'en'This%20is%20fun +Expect: 3 + title=This is even more ***fun*** isn't it! + name=one two three + p=This is fun +Content-Type: application/x-stuff; + title*2="isn't it!"; + name*1="two "; + title*1*=%2A%2A%2Afun%2A%2A%2A%20; + name*0="one "; + title*0*=us-ascii'en'This%20is%20even%20more%20; + name*2="three"; + p*=us-ascii'en'This%20is%20fun +Expect: 3 + title=This is even more ***fun*** isn't it! + name=one two three + p=This is fun +Comment: + These headers test error cases that don't generate parse exceptions. + . +Content-Type: application/x-stuff; + title*0*=us-ascii'en'This%20is%20even%20more%20; + title*1*=%2A%2A%2Afun%2A%2A%2A; + title*3="isn't it!" +Expect: 2 + title=This is even more ***fun*** + title*3=isn't it! +Content-Type: application/x-stuff; + title*0*=us-ascii'en'This%20is%20even%20more%20; + title*1*=%2A%2A%2Afun%2A%2A%2A; + title*3*="isn't it!" +Expect: 2 + title=This is even more ***fun*** + title*3=isn't it! +Comment: + This is an error case with no right answer. + Should we assume earlier segments are missing and just decode + this segment? Or should we assume that the "*3" is part of + the parameter name and this is a single segment encoded parameter? + Currently we do the former. +Content-Type: application/x-stuff; title*3*=us-ascii'en'isn't%20it! +Expect: 1 + title*3=us-ascii'en'isn't it! +Content-Type: application/x-stuff; title*3*=unknown'en'isn't%20it! +Expect: 1 + title*3=unknown'en'isn't it! +Content-Type: application/x-stuff; + title*0*=us-ascii'en'This%20is%20even%20more%20; + title*1*=%XX%2A%2Afun%2A%2A%2A%20; + title*2="isn't it!" +Expect: 1 + title=This is even more %XX**fun*** isn't it! +Content-Type: application/x-stuff; + title*0*=us-ascii'en'This%20is%20even%20more%20; + title*1="***fun***"; + title*2*=%20isn't%20it! +Expect: 1 + title=This is even more ***fun*** isn't it! +Content-Type: image/png; + name*0*=ISO-2022-JP''%1B%24B%24%22%24%24%24%26%24%28%24*%24%22%24%24%24%26; + name*1*=%24%28%24*%24%22%24%24%24%26%24%28%24*%24%22%24%24%24%26%24%28; + name*2*=%24*%1B%28B.png +Expect: 1 + name=\u3042\u3044\u3046\u3048\u304a\u3042\u3044\u3046\u3048\u304a\u3042\u3044\u3046\u3048\u304a\u3042\u3044\u3046\u3048\u304a.png +Content-Type: image/png; + filename*0*=ISO-2022-JP''%1B%24B%24%22%24%24%24%26%24%28%24*%24%22%24%24; + filename*1*=%24%26%24%28%24*%24%22%24%24%24%26%24%28%24*%24%22%24%24%24; + filename*2*=%26%24%28%24*%1B%28B.png +Expect: 1 + filename=\u3042\u3044\u3046\u3048\u304a\u3042\u3044\u3046\u3048\u304a\u3042\u3044\u3046\u3048\u304a\u3042\u3044\u3046\u3048\u304a.png
diff --git a/mail/src/test/resources/javax/mail/internet/paramdatanostrict b/mail/src/test/resources/javax/mail/internet/paramdatanostrict new file mode 100644 index 0000000..a155889 --- /dev/null +++ b/mail/src/test/resources/javax/mail/internet/paramdatanostrict
@@ -0,0 +1,70 @@ +Comment: + + Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +Comment: + + A set of test headers to test parameter list parsing + with mail.mime.parameters.strict=false. + Use with: + java -Dmail.mime.parameters.strict=false paramtest -p <paramdatanostrict + + CAREFUL: a blank line in this file causes everything following it + to be ignored until a line starting with "From " (so that + paramtest -p can be pointed at a real mailbox). + +From start here +Content-Type: text/plain; + creation-date=Tue, 22 Jul 2008 10:03:09 GMT; + filename="test1kb.file"; + modification-date=Tue, 22 Jul 2008 10:03:24 GMT +Expect: 3 + creation-date=Tue, 22 Jul 2008 10:03:09 GMT + filename=test1kb.file + modification-date=Tue, 22 Jul 2008 10:03:24 GMT +Comment: embedded whitespace +Content-Type: text/plain; name=file name.txt +Expect: 1 + name=file name.txt +Comment: trailing whitespace +Content-Type: text/plain; name=file name.txt +Expect: 1 + name=file name.txt +Comment: leading and trailing whitespace +Content-Type: text/plain; name= file name.txt +Expect: 1 + name=file name.txt +Comment: trailing newline +Content-Type: text/plain; name=file name.txt + ; +Expect: 1 + name=file name.txt +Content-Type: text/plain; name=file name.txt ; time= now +Expect: 2 + name=file name.txt + time=now +Content-Type: text/plain; name=file name.txt ; + time= now +Expect: 2 + name=file name.txt + time=now +Content-Type: text/plain; name=file name.txt + ; time = now +Expect: 2 + name=file name.txt + time=now +Content-Type: text/plain; filename==?Windows-1251?B?8OXq4ujn6PL7IMjPLmRvYw?= +Expect: 1 + filename==?Windows-1251?B?8OXq4ujn6PL7IMjPLmRvYw?=
diff --git a/mail/src/test/resources/javax/mail/internet/tokenlist b/mail/src/test/resources/javax/mail/internet/tokenlist new file mode 100644 index 0000000..d014ee0 --- /dev/null +++ b/mail/src/test/resources/javax/mail/internet/tokenlist
@@ -0,0 +1,736 @@ +Comment: + + Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +Comment: + + A set of test addresses stolen from the dtmail test suite, + with a few of my own. Used here to test the header tokenizer. + Use with "tokenizertest -p <tokenlist". + + CAREFUL: a blank line in this file causes everything following it + to be ignored until a line starting with "From " (so that + tokenizertest -p can be pointed at a real mailbox). + +From start here +Comment: + The following should all be tokenized successfully. +To: ggere +Expect: 1 + ATOM ggere +To: /tmp/mail-out +Expect: 1 + ATOM /tmp/mail-out +To: +mailbox +Expect: 1 + ATOM +mailbox +To: ~user/mailbox +Expect: 1 + ATOM ~user/mailbox +To: ~/mailbox +Expect: 1 + ATOM ~/mailbox +To: /PN=x400-address/PRMD=ibmmail/ADMD=ibmx400/C=us/@mhs-mci-ebay +Expect: 3 + ATOM /PN=x400-address/PRMD=ibmmail/ADMD=ibmx400/C=us/ + SPECIAL @ + ATOM mhs-mci-ebay +To: ggere, /tmp/mail-out, +mailbox, ~user/mailbox, ~/mailbox, /PN=x400-address/PRMD=ibmmail/ADMD=ibmx400/C=us/@mhs-mci-ebay +Expect: 13 + ATOM ggere + SPECIAL , + ATOM /tmp/mail-out + SPECIAL , + ATOM +mailbox + SPECIAL , + ATOM ~user/mailbox + SPECIAL , + ATOM ~/mailbox + SPECIAL , + ATOM /PN=x400-address/PRMD=ibmmail/ADMD=ibmx400/C=us/ + SPECIAL @ + ATOM mhs-mci-ebay +To: ggere /tmp/mail-out +mailbox ~user/mailbox ~/mailbox /PN=x400-address/PRMD=ibmmail/ADMD=ibmx400/C=us/@mhs-mci-ebay +Expect: 8 + ATOM ggere + ATOM /tmp/mail-out + ATOM +mailbox + ATOM ~user/mailbox + ATOM ~/mailbox + ATOM /PN=x400-address/PRMD=ibmmail/ADMD=ibmx400/C=us/ + SPECIAL @ + ATOM mhs-mci-ebay +To: Mad Genius <george@boole> +Expect: 7 + ATOM Mad + ATOM Genius + SPECIAL < + ATOM george + SPECIAL @ + ATOM boole + SPECIAL > +To: "C'est bien moche" <paris@france> +Expect: 6 + QUOTEDSTRING C'est bien moche + SPECIAL < + ATOM paris + SPECIAL @ + ATOM france + SPECIAL > +To: "know, any, famous" <french@physicists> +Expect: 6 + QUOTEDSTRING know, any, famous + SPECIAL < + ATOM french + SPECIAL @ + ATOM physicists + SPECIAL > +To: laborious (But Bug Free) +Expect: 1 + ATOM laborious +To: confused (about, being, french) +Expect: 1 + ATOM confused +To: it (takes, no (time, at) all) +Expect: 1 + ATOM it +To: two@weeks (It Will Take) +Expect: 3 + ATOM two + SPECIAL @ + ATOM weeks +To: it@is (brilliant (genius, and) superb) +Expect: 3 + ATOM it + SPECIAL @ + ATOM is +To: if@you (could, see (almost, as, (badly, you) would) agree) +Expect: 3 + ATOM if + SPECIAL @ + ATOM you +To: cannot@waste (My, Intellectual, Cycles) +Expect: 3 + ATOM cannot + SPECIAL @ + ATOM waste +To: users:get,what,they,deserve; +Expect: 10 + ATOM users + SPECIAL : + ATOM get + SPECIAL , + ATOM what + SPECIAL , + ATOM they + SPECIAL , + ATOM deserve + SPECIAL ; +To: "C'est bien moche" <paris@france>, Mad Genius <george@boole>, two@weeks (It Will Take), /tmp/mail-out, laborious (But Bug Free), cannot@waste (My, Intellectual, Cycles), users:get,what,they,deserve;, it (takes, no (time, at) all), if@you (could, see (almost, as, (badly, you) would) agree), "know, any, famous" <French@physicists>, it@is (brilliant (genius, and) superb), confused (about, being, french) +Expect: 56 + QUOTEDSTRING C'est bien moche + SPECIAL < + ATOM paris + SPECIAL @ + ATOM france + SPECIAL > + SPECIAL , + ATOM Mad + ATOM Genius + SPECIAL < + ATOM george + SPECIAL @ + ATOM boole + SPECIAL > + SPECIAL , + ATOM two + SPECIAL @ + ATOM weeks + SPECIAL , + ATOM /tmp/mail-out + SPECIAL , + ATOM laborious + SPECIAL , + ATOM cannot + SPECIAL @ + ATOM waste + SPECIAL , + ATOM users + SPECIAL : + ATOM get + SPECIAL , + ATOM what + SPECIAL , + ATOM they + SPECIAL , + ATOM deserve + SPECIAL ; + SPECIAL , + ATOM it + SPECIAL , + ATOM if + SPECIAL @ + ATOM you + SPECIAL , + QUOTEDSTRING know, any, famous + SPECIAL < + ATOM French + SPECIAL @ + ATOM physicists + SPECIAL > + SPECIAL , + ATOM it + SPECIAL @ + ATOM is + SPECIAL , + ATOM confused +To: ggere, /tmp/mail-out, +mailbox, ~user/mailbox, ~/mailbox, /PN=x400-address/PRMD=ibmmail/ADMD=ibmx400/C=us/@mhs-mci-ebay, "C'est bien moche" <paris@france>, Mad Genius <george@boole>, two@weeks (It Will Take), /tmp/mail-out, laborious (But Bug Free), cannot@waste (My, Intellectual, Cycles), users:get,what,they,deserve;, it (takes, no (time, at) all), if@you (could, see (almost, as, (badly, you) would) agree), "know, any, famous" <French@physicists>, it@is (brilliant (genius, and) superb), confused (about, being, french) +Expect: 70 + ATOM ggere + SPECIAL , + ATOM /tmp/mail-out + SPECIAL , + ATOM +mailbox + SPECIAL , + ATOM ~user/mailbox + SPECIAL , + ATOM ~/mailbox + SPECIAL , + ATOM /PN=x400-address/PRMD=ibmmail/ADMD=ibmx400/C=us/ + SPECIAL @ + ATOM mhs-mci-ebay + SPECIAL , + QUOTEDSTRING C'est bien moche + SPECIAL < + ATOM paris + SPECIAL @ + ATOM france + SPECIAL > + SPECIAL , + ATOM Mad + ATOM Genius + SPECIAL < + ATOM george + SPECIAL @ + ATOM boole + SPECIAL > + SPECIAL , + ATOM two + SPECIAL @ + ATOM weeks + SPECIAL , + ATOM /tmp/mail-out + SPECIAL , + ATOM laborious + SPECIAL , + ATOM cannot + SPECIAL @ + ATOM waste + SPECIAL , + ATOM users + SPECIAL : + ATOM get + SPECIAL , + ATOM what + SPECIAL , + ATOM they + SPECIAL , + ATOM deserve + SPECIAL ; + SPECIAL , + ATOM it + SPECIAL , + ATOM if + SPECIAL @ + ATOM you + SPECIAL , + QUOTEDSTRING know, any, famous + SPECIAL < + ATOM French + SPECIAL @ + ATOM physicists + SPECIAL > + SPECIAL , + ATOM it + SPECIAL @ + ATOM is + SPECIAL , + ATOM confused +To: testa testb testc /tmp/mail-out ~ggere/mail-out testd teste +Expect: 7 + ATOM testa + ATOM testb + ATOM testc + ATOM /tmp/mail-out + ATOM ~ggere/mail-out + ATOM testd + ATOM teste +To: testa, testb , testc /tmp/mail-out ,~ggere/mail-out, ,testd,teste,,, +Expect: 16 + ATOM testa + SPECIAL , + ATOM testb + SPECIAL , + ATOM testc + ATOM /tmp/mail-out + SPECIAL , + ATOM ~ggere/mail-out + SPECIAL , + SPECIAL , + ATOM testd + SPECIAL , + ATOM teste + SPECIAL , + SPECIAL , + SPECIAL , +To: testa testb <testc@testd> +Expect: 7 + ATOM testa + ATOM testb + SPECIAL < + ATOM testc + SPECIAL @ + ATOM testd + SPECIAL > +To: Adam S Moskowitz <adamm@onion-inset-com>, + Andrew Gollan <adjg@softway-sw-OZ-AU>, + Bret Anthony Marquis <bam@trout-nosc-MIL>, + Bill Shannon <shannon@Sun-COM>, Bob Gray <rgray@UsWest-COM>, + Brian Ellis <bri@Boulder-Colorado-EDU>, + Don Coleman <coleman@legato-COM>, + Dennis Ritchie <dmr@research-ATT-COM>, Evi Nemeth <evi@Colorado-EDU>, + Lori Grob <grob@chorus-FR>, Paula Hawthorn <hawthorn@HPL-HP-COM>, + Andrew Hume <andrew@research-ATT-COM>, + Jaap Akkerhuis <jaap@research-ATT-COM>, + Jim R Oldroyd <jr@onion-inset-com>, + John Quarterman <jsq@longway-TIC-COM>, + Judy DesHarnais <judy@USENIX-ORG>, Karen Shannon <kas@Eng>, + Ken McDonell <kenj@Pyramid-COM>, + Rob Kolstad <kolstad@rmtc-Central-Sun-COM>, + Paul Kooros <kooros@tigger-cs-Colorado-EDU>, + Robert Elz <kre@Munnari-OZ-AU>, + Miriam Amos Nihart <miriam@decwet-enet-DEC-COM>, + "Mike O'Dell" <mo@gizmo-Bellcore-COM>, + Sharon Murrel <eowyn@research-ATT-COM>, + Peg Schafer <peg@media-lab-media-MIT-EDU>, Peter Salus <peter@SUG-ORG>, + Bill Shannon <shannon@Sun-COM>, Stu Feldman <sif@Bellcore-COM>, + Stephen Williams <stevew@netboss1-trg-saic-com>, + Teus Hagen <teus@oce-NL>, Trent Hein <trent@xor-COM>, + Thomas Alan Wood <twood@capmkt-COM>, + Michael Ubell <"mttam::ubell"@SFBay-ENet-DEC-COM>, + Martha Zimet <zimet@Corp> +Expect: 275 + ATOM Adam + ATOM S + ATOM Moskowitz + SPECIAL < + ATOM adamm + SPECIAL @ + ATOM onion-inset-com + SPECIAL > + SPECIAL , + ATOM Andrew + ATOM Gollan + SPECIAL < + ATOM adjg + SPECIAL @ + ATOM softway-sw-OZ-AU + SPECIAL > + SPECIAL , + ATOM Bret + ATOM Anthony + ATOM Marquis + SPECIAL < + ATOM bam + SPECIAL @ + ATOM trout-nosc-MIL + SPECIAL > + SPECIAL , + ATOM Bill + ATOM Shannon + SPECIAL < + ATOM shannon + SPECIAL @ + ATOM Sun-COM + SPECIAL > + SPECIAL , + ATOM Bob + ATOM Gray + SPECIAL < + ATOM rgray + SPECIAL @ + ATOM UsWest-COM + SPECIAL > + SPECIAL , + ATOM Brian + ATOM Ellis + SPECIAL < + ATOM bri + SPECIAL @ + ATOM Boulder-Colorado-EDU + SPECIAL > + SPECIAL , + ATOM Don + ATOM Coleman + SPECIAL < + ATOM coleman + SPECIAL @ + ATOM legato-COM + SPECIAL > + SPECIAL , + ATOM Dennis + ATOM Ritchie + SPECIAL < + ATOM dmr + SPECIAL @ + ATOM research-ATT-COM + SPECIAL > + SPECIAL , + ATOM Evi + ATOM Nemeth + SPECIAL < + ATOM evi + SPECIAL @ + ATOM Colorado-EDU + SPECIAL > + SPECIAL , + ATOM Lori + ATOM Grob + SPECIAL < + ATOM grob + SPECIAL @ + ATOM chorus-FR + SPECIAL > + SPECIAL , + ATOM Paula + ATOM Hawthorn + SPECIAL < + ATOM hawthorn + SPECIAL @ + ATOM HPL-HP-COM + SPECIAL > + SPECIAL , + ATOM Andrew + ATOM Hume + SPECIAL < + ATOM andrew + SPECIAL @ + ATOM research-ATT-COM + SPECIAL > + SPECIAL , + ATOM Jaap + ATOM Akkerhuis + SPECIAL < + ATOM jaap + SPECIAL @ + ATOM research-ATT-COM + SPECIAL > + SPECIAL , + ATOM Jim + ATOM R + ATOM Oldroyd + SPECIAL < + ATOM jr + SPECIAL @ + ATOM onion-inset-com + SPECIAL > + SPECIAL , + ATOM John + ATOM Quarterman + SPECIAL < + ATOM jsq + SPECIAL @ + ATOM longway-TIC-COM + SPECIAL > + SPECIAL , + ATOM Judy + ATOM DesHarnais + SPECIAL < + ATOM judy + SPECIAL @ + ATOM USENIX-ORG + SPECIAL > + SPECIAL , + ATOM Karen + ATOM Shannon + SPECIAL < + ATOM kas + SPECIAL @ + ATOM Eng + SPECIAL > + SPECIAL , + ATOM Ken + ATOM McDonell + SPECIAL < + ATOM kenj + SPECIAL @ + ATOM Pyramid-COM + SPECIAL > + SPECIAL , + ATOM Rob + ATOM Kolstad + SPECIAL < + ATOM kolstad + SPECIAL @ + ATOM rmtc-Central-Sun-COM + SPECIAL > + SPECIAL , + ATOM Paul + ATOM Kooros + SPECIAL < + ATOM kooros + SPECIAL @ + ATOM tigger-cs-Colorado-EDU + SPECIAL > + SPECIAL , + ATOM Robert + ATOM Elz + SPECIAL < + ATOM kre + SPECIAL @ + ATOM Munnari-OZ-AU + SPECIAL > + SPECIAL , + ATOM Miriam + ATOM Amos + ATOM Nihart + SPECIAL < + ATOM miriam + SPECIAL @ + ATOM decwet-enet-DEC-COM + SPECIAL > + SPECIAL , + QUOTEDSTRING Mike O'Dell + SPECIAL < + ATOM mo + SPECIAL @ + ATOM gizmo-Bellcore-COM + SPECIAL > + SPECIAL , + ATOM Sharon + ATOM Murrel + SPECIAL < + ATOM eowyn + SPECIAL @ + ATOM research-ATT-COM + SPECIAL > + SPECIAL , + ATOM Peg + ATOM Schafer + SPECIAL < + ATOM peg + SPECIAL @ + ATOM media-lab-media-MIT-EDU + SPECIAL > + SPECIAL , + ATOM Peter + ATOM Salus + SPECIAL < + ATOM peter + SPECIAL @ + ATOM SUG-ORG + SPECIAL > + SPECIAL , + ATOM Bill + ATOM Shannon + SPECIAL < + ATOM shannon + SPECIAL @ + ATOM Sun-COM + SPECIAL > + SPECIAL , + ATOM Stu + ATOM Feldman + SPECIAL < + ATOM sif + SPECIAL @ + ATOM Bellcore-COM + SPECIAL > + SPECIAL , + ATOM Stephen + ATOM Williams + SPECIAL < + ATOM stevew + SPECIAL @ + ATOM netboss1-trg-saic-com + SPECIAL > + SPECIAL , + ATOM Teus + ATOM Hagen + SPECIAL < + ATOM teus + SPECIAL @ + ATOM oce-NL + SPECIAL > + SPECIAL , + ATOM Trent + ATOM Hein + SPECIAL < + ATOM trent + SPECIAL @ + ATOM xor-COM + SPECIAL > + SPECIAL , + ATOM Thomas + ATOM Alan + ATOM Wood + SPECIAL < + ATOM twood + SPECIAL @ + ATOM capmkt-COM + SPECIAL > + SPECIAL , + ATOM Michael + ATOM Ubell + SPECIAL < + QUOTEDSTRING mttam::ubell + SPECIAL @ + ATOM SFBay-ENet-DEC-COM + SPECIAL > + SPECIAL , + ATOM Martha + ATOM Zimet + SPECIAL < + ATOM zimet + SPECIAL @ + ATOM Corp + SPECIAL > +To: '"a,b"' +Expect: 3 + ATOM ' + QUOTEDSTRING a,b + ATOM ' +To: 'a, b' +Expect: 3 + ATOM 'a + SPECIAL , + ATOM b' +To: "a,b" +Expect: 1 + QUOTEDSTRING a,b +To: shakey!"a,b" +Expect: 2 + ATOM shakey! + QUOTEDSTRING a,b +To: "foo bar" +Expect: 1 + QUOTEDSTRING foo bar +To: (Foo Bar) foo@bar +Expect: 3 + ATOM foo + SPECIAL @ + ATOM bar +To: (Foo Bar) foo@bar, (My Name) my@name +Expect: 7 + ATOM foo + SPECIAL @ + ATOM bar + SPECIAL , + ATOM my + SPECIAL @ + ATOM name +To: , +Expect: 1 + SPECIAL , +To: "C'est bien moche" <paris@france +Expect: 5 + QUOTEDSTRING C'est bien moche + SPECIAL < + ATOM paris + SPECIAL @ + ATOM france +To: "C'est bien moche" paris@france> +Expect: 5 + QUOTEDSTRING C'est bien moche + ATOM paris + SPECIAL @ + ATOM france + SPECIAL > +To: "C'est bien moche" <paris@> +Expect: 5 + QUOTEDSTRING C'est bien moche + SPECIAL < + ATOM paris + SPECIAL @ + SPECIAL > +To: laborious but (Bug Free) +Expect: 2 + ATOM laborious + ATOM but +To: two@ (It Will Take) +Expect: 2 + ATOM two + SPECIAL @ +To: two@ +Expect: 2 + ATOM two + SPECIAL @ +To: <paris@france +Expect: 4 + SPECIAL < + ATOM paris + SPECIAL @ + ATOM france +To: paris@france> +Expect: 4 + ATOM paris + SPECIAL @ + ATOM france + SPECIAL > +To: @france +Expect: 2 + SPECIAL @ + ATOM france +To: @ +Expect: 1 + SPECIAL @ +To: < +Expect: 1 + SPECIAL < +To: > +Expect: 1 + SPECIAL > +Comment: + I'm not sure the following should be valid. +To: ) +Expect: 1 + SPECIAL ) +To: friends-list:;@mastodon-CS-Berkeley-EDU +Expect: 5 + ATOM friends-list + SPECIAL : + SPECIAL ; + SPECIAL @ + ATOM mastodon-CS-Berkeley-EDU +Comment: + The following should all generate exceptions. +To: "C'est bien moche <paris@france> +Expect: Exception javax.mail.internet.ParseException: Unbalanced quoted string +To: confused (about, being, french +Expect: Exception javax.mail.internet.ParseException: Unbalanced comments +To: it@is (brilliant (genius, and superb) +Expect: Exception javax.mail.internet.ParseException: Unbalanced comments +To: it@is (brilliant (genius), and superb +Expect: Exception javax.mail.internet.ParseException: Unbalanced comments +To: if@you (could, see (almost, as, (badly, you would) agree) +Expect: Exception javax.mail.internet.ParseException: Unbalanced comments +To: if@you (could, see (almost, as, (badly, you) would) agree +Expect: Exception javax.mail.internet.ParseException: Unbalanced comments +To: " +Expect: Exception javax.mail.internet.ParseException: Unbalanced quoted string +To: two@weeks (It Will Take +Expect: Exception javax.mail.internet.ParseException: Unbalanced comments +To: ( +Expect: Exception javax.mail.internet.ParseException: Unbalanced comments
diff --git a/mailapi/pom.xml b/mailapi/pom.xml new file mode 100644 index 0000000..0fa1e9c --- /dev/null +++ b/mailapi/pom.xml
@@ -0,0 +1,233 @@ +<?xml version="1.0" encoding="iso-8859-1"?> +<!-- + + Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<!-- + This project builds the JavaMail "mailapi.jar" file. This jar file + contains a fully operational JavaMail implementation but without any + protocol providers. It can be used with the separate protocol provider + jar files (e.g., smtp.jar, imap.jar) to provide a full JavaMail runtime + implementation. +--> + +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 + http://maven.apache.org/maven-v4_0_0.xsd"> + <parent> + <groupId>com.sun.mail</groupId> + <artifactId>all</artifactId> + <version>1.6.3</version> + </parent> + <modelVersion>4.0.0</modelVersion> + <groupId>com.sun.mail</groupId> + <artifactId>mailapi</artifactId> + <packaging>jar</packaging> + <name>JavaMail API (no providers)</name> + + <properties> + <mail.packages.export> + javax.mail.*; version=${mail.spec.version}, + com.sun.mail.util; version=${mail.osgiversion}, + com.sun.mail.auth; version=${mail.osgiversion}, + com.sun.mail.handlers; version=${mail.osgiversion} + </mail.packages.export> + <mail.bundle.symbolicName> + jakarta.mail.api + </mail.bundle.symbolicName> + </properties> + + <profiles> + <!-- + A special profile for compiling with the real JDK 1.7 compiler. + Exclude MailSessionDefinition and MailSessionDefinitions since + the former requires JDK 1.8 and the latter depends on the former. + --> + <profile> + <id>1.7</id> + <build> + <plugins> + <plugin> + <artifactId>maven-compiler-plugin</artifactId> + <executions> + <execution> + <id>default-compile</id> + <configuration> + <excludes> + <exclude> + javax/mail/MailSessionDefinition.java + </exclude> + <exclude> + javax/mail/MailSessionDefinitions.java + </exclude> + <exclude> + module-info.java + </exclude> + </excludes> + </configuration> + </execution> + </executions> + </plugin> + </plugins> + </build> + </profile> + + <!-- + A special profile for compiling with JDK 9. + --> + <profile> + <id>9</id> + <activation> + <jdk>9</jdk> + </activation> + <build> + <plugins> + <plugin> + <artifactId>maven-compiler-plugin</artifactId> + <executions> + <execution> + <id>default-compile</id> + <configuration> + <release>9</release> + <source>9</source> + <target>9</target> + <compilerArgs> + <arg>-Xlint</arg> + <arg>-Xlint:-options</arg> + <arg>-Xlint:-path</arg> + <!-- + Too many finalize warnings. + <arg>-Werror</arg> + --> + </compilerArgs> + <!-- un-exclude module-info.java --> + <excludes combine.self="override"> + </excludes> + </configuration> + </execution> + </executions> + </plugin> + <!-- + Require JDK 9. + --> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-enforcer-plugin</artifactId> + <executions> + <execution> + <id>enforce-version</id> + <goals> + <goal>enforce</goal> + </goals> + <configuration> + <rules> + <requireMavenVersion> + <version>[3.0.3,)</version> + </requireMavenVersion> + <requireJavaVersion> + <version>9</version> + </requireJavaVersion> + </rules> + </configuration> + </execution> + </executions> + </plugin> + </plugins> + </build> + </profile> + </profiles> + + <build> + <plugins> + <plugin> + <artifactId>maven-dependency-plugin</artifactId> + <executions> + <execution> + <!-- download the binaries --> + <id>get-binaries</id> + <phase>generate-sources</phase> + <goals> + <goal>unpack</goal> + </goals> + </execution> + <execution> + <!-- download the sources --> + <id>get-sources</id> + <phase>generate-sources</phase> + <goals> + <goal>unpack</goal> + </goals> + <configuration> + <artifactItems> + <artifactItem> + <groupId>com.sun.mail</groupId> + <artifactId>jakarta.mail</artifactId> + <version>${mail.version}</version> + <classifier>sources</classifier> + <outputDirectory> + ${project.build.directory}/sources + </outputDirectory> + </artifactItem> + </artifactItems> + </configuration> + </execution> + </executions> + <configuration> + <artifactItems> + <artifactItem> + <groupId>com.sun.mail</groupId> + <artifactId>jakarta.mail</artifactId> + <version>${mail.version}</version> + </artifactItem> + </artifactItems> + <outputDirectory> + ${project.build.outputDirectory} + </outputDirectory> + <includes> + javax/**, + com/sun/mail/util/**, + com/sun/mail/auth/**, + com/sun/mail/handlers/**, + META-INF/* + </includes> + <!-- + Have to exclude package.html because it links to + com.sun.mail.{imap,pop3,smtp} packages and jdk 9 + javadoc fails that during deploy. + --> + <excludes> + javax/mail/package.html, + com/sun/mail/util/logging/**, + META-INF/javamail.default.* + </excludes> + </configuration> + </plugin> + <plugin> + <artifactId>maven-jar-plugin</artifactId> + <configuration> + <finalName>${project.artifactId}</finalName> + <archive> + <manifestFile> + ${project.build.outputDirectory}/META-INF/MANIFEST.MF + </manifestFile> + </archive> + </configuration> + </plugin> + </plugins> + </build> +</project>
diff --git a/mailapi/src/main/java/module-info.java b/mailapi/src/main/java/module-info.java new file mode 100644 index 0000000..76f9f3e --- /dev/null +++ b/mailapi/src/main/java/module-info.java
@@ -0,0 +1,33 @@ +/* + * Copyright (c) 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +module jakarta.mail { + exports javax.mail; + exports javax.mail.event; + exports javax.mail.internet; + exports javax.mail.search; + exports javax.mail.util; + exports com.sun.mail.util; + exports com.sun.mail.auth; + exports com.sun.mail.handlers; + + requires transitive jakarta.activation; + requires java.logging; + requires java.xml; // for text/xml handler + requires java.desktop; // for image/jpeg handler + requires java.security.sasl; // for OAuth2 support + uses javax.mail.Provider; +}
diff --git a/mailapijar/pom.xml b/mailapijar/pom.xml new file mode 100644 index 0000000..c5affd1 --- /dev/null +++ b/mailapijar/pom.xml
@@ -0,0 +1,119 @@ +<?xml version="1.0" encoding="iso-8859-1"?> +<!-- + + Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<!-- + This project builds the JavaMail API jar file, which contains only + the javax.mail.* API definitions and is *only* intended to be used + for programs to compile against. Note that it includes none of the + implementation-specific classes that the javax.mail.* classes rely on. +--> + +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 + http://maven.apache.org/maven-v4_0_0.xsd"> + <parent> + <groupId>com.sun.mail</groupId> + <artifactId>all</artifactId> + <version>1.6.3</version> + </parent> + <modelVersion>4.0.0</modelVersion> + <groupId>jakarta.mail</groupId> + <artifactId>jakarta.mail-api</artifactId> + <packaging>jar</packaging> + <name>JavaMail API jar</name> + + <properties> + <mail.extensionName> + jakarta.mail + </mail.extensionName> + <mail.packages.export> + javax.mail.*; version=${mail.spec.version} + </mail.packages.export> + <mail.bundle.symbolicName> + jakarta.mail-api + </mail.bundle.symbolicName> + </properties> + + <build> + <plugins> + <plugin> + <artifactId>maven-dependency-plugin</artifactId> + <executions> + <execution> + <!-- download the binaries --> + <id>get-binaries</id> + <phase>compile</phase> + <goals> + <goal>unpack</goal> + </goals> + </execution> + <execution> + <!-- download the sources --> + <id>get-sources</id> + <phase>compile</phase> + <goals> + <goal>unpack</goal> + </goals> + <configuration> + <artifactItems> + <artifactItem> + <groupId>com.sun.mail</groupId> + <artifactId>jakarta.mail</artifactId> + <version>${mail.version}</version> + <classifier>sources</classifier> + <outputDirectory> + ${project.build.directory}/sources + </outputDirectory> + </artifactItem> + </artifactItems> + </configuration> + </execution> + </executions> + <configuration> + <artifactItems> + <artifactItem> + <groupId>com.sun.mail</groupId> + <artifactId>jakarta.mail</artifactId> + <version>${mail.version}</version> + </artifactItem> + </artifactItems> + <outputDirectory> + ${project.build.outputDirectory} + </outputDirectory> + <includes> + javax/**, + META-INF/LICENSE.txt + </includes> + </configuration> + </plugin> + <plugin> + <artifactId>maven-jar-plugin</artifactId> + <configuration> + <finalName>${project.artifactId}</finalName> + <archive> + <manifestFile> + ${project.build.outputDirectory}/META-INF/MANIFEST.MF + </manifestFile> + </archive> + </configuration> + </plugin> + </plugins> + </build> +</project>
diff --git a/mailhandler/pom.xml b/mailhandler/pom.xml new file mode 100644 index 0000000..dc8aaaa --- /dev/null +++ b/mailhandler/pom.xml
@@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="iso-8859-1"?> +<!-- + + Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 + http://maven.apache.org/maven-v4_0_0.xsd"> + <parent> + <groupId>com.sun.mail</groupId> + <artifactId>parent-distrib</artifactId> + <version>1.6.3</version> + <relativePath>../parent-distrib/pom.xml</relativePath> + </parent> + <modelVersion>4.0.0</modelVersion> + <groupId>com.sun.mail</groupId> + <artifactId>logging-mailhandler</artifactId> + <packaging>jar</packaging> + <name>JavaMail API logging handler</name> + + <properties> + <mail.packages.export> + com.sun.mail.util.logging; version=${mail.osgiversion} + </mail.packages.export> + <mail.classes>com/sun/mail/util/logging/**</mail.classes> + </properties> + +</project>
diff --git a/mailhandler/src/main/java/module-info.java b/mailhandler/src/main/java/module-info.java new file mode 100644 index 0000000..268c8e0 --- /dev/null +++ b/mailhandler/src/main/java/module-info.java
@@ -0,0 +1,22 @@ +/* + * Copyright (c) 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +module com.sun.mail.util.logging { + exports com.sun.mail.util.logging; + + requires jakarta.mail; + requires java.logging; +}
diff --git a/mbox.xml b/mbox.xml new file mode 100644 index 0000000..1ee9685 --- /dev/null +++ b/mbox.xml
@@ -0,0 +1,206 @@ +<?xml version="1.0" encoding="iso-8859-1"?> +<!-- + + Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<!DOCTYPE project [ + <!ENTITY commonBuild SYSTEM "ant-common.xml"> +]> +<project name="JavaMail" default="all" basedir="." + xmlns:cpptasks="antlib:org.sf.net.antcontrib.cpptasks"> + + <taskdef resource="cpptasks.tasks"/> + <typedef resource="cpptasks.types"/> + +<!-- ========== Initialize Properties =================================== --> + + <!-- + component.name: required property. the value should be the + name of the component directory + --> + <property name="component.name" value="mbox"/> + + <property file="./build.properties"/> + + <property name="cc.debug" value="false"/> + <property name="generated.dir" value="mbox/target/generated"/> + <property name="obj.dir" value="mbox/target/native/${os.name}-${os.arch}"/> + <property name="lib.dir" value="mbox/target/lib/${os.name}-${os.arch}"/> + <condition property="is-solaris"> + <os name="SunOS"/> + </condition> + + &commonBuild; + + <!-- all --> + <target name="all" depends="mboxcompile, mboxcc, mboxjar" + description="Build entire mbox component"> + </target> + + <!-- build --> + <target name="build" depends="mboxcompile, mboxcc, mboxjar" + description="Build entire mbox component"> + </target> + + <!-- init. Initialization involves creating publishing directories and + OS specific targets. --> + <target name="init" description="${component.name} initialization"> + <tstamp> + <format property="start.time" pattern="MM/dd/yyyy hh:mm aa"/> + </tstamp> + <echo message="Building component ${component.name}"/> + <mkdir dir="${component.classes.mbox.dir}"/> + <mkdir dir="${generated.dir}"/> + <mkdir dir="${obj.dir}"/> + <mkdir dir="${lib.dir}"/> + </target> + + <!-- prepare manifest files for jars --> + <target name="cook-manifest" depends="init" + description="Generate MANIFEST.MF files"> + <mkdir dir="${component.classes.mbox.dir}/manifest"/> + <copy file="${resources.mbox.dir}/META-INF/MANIFEST.MF" + tofile="${component.classes.mbox.dir}/manifest/MANIFEST.MF"> + <filterset begintoken="{" endtoken="}"> + <filter token="mail.spec.version" + value="${release.specversion}"/> + <filter token="mail.version" value="${release.version}"/> + </filterset> + </copy> + </target> + + <!-- mbox compile --> + <target name="mboxcompile" depends="init" + description="Compile com/sun/mail/mbox sources"> + + <javac srcdir="${src.mbox.dir}" + destdir="${component.classes.mbox.dir}" + debug="${javac.debug}" + optimize="${javac.optimize}" + source="${javac.source}" + deprecation="${javac.deprecation}" + failonerror="true" + target="${javac.target}"> + <classpath> + <pathelement location="${component.classes.dir}"/> + <pathelement location="${activation.jar}"/> + </classpath> + <!-- + <include name="com/sun/mail/mbox/**"/> + --> + </javac> + </target> + + <target name="mboxheaders" depends="mboxcompile"> + <javah destdir="${generated.dir}" + force="yes" + verbose="yes" + classpath="${component.classes.mbox.dir}:${component.classes.dir}"> + <class name="com.sun.mail.mbox.UNIXFile"/> + <class name="com.sun.mail.mbox.UNIXInbox"/> + </javah> + </target> + + <target name="mboxcc" depends="mboxheaders, mboxcc-solaris"/> + + <!-- + make-based build uses these compiler and linker options: + + cc -Xa -xO2 -v -D_REENTRANT -Dsparc -DSOLARIS2 + + cc -G -o ../../../../../../build/solaris/lib/sparc/libmbox.so + obj/sparc/./UNIXFile.o obj/sparc/./UNIXInbox.o + -L/java/re/jdk/1.4/archive/fcs/binaries/solsparc/jre/lib/sparc + -L../../../../../../build/solaris/lib/sparc + -lmail + -L../../../../../../build/solaris/lib/sparc + -ljava + -lc + --> + + <target name="mboxcc-solaris" depends="mboxheaders" if="is-solaris"> + <cc debug="${cc.debug}" + link="shared" + outfile="${lib.dir}/mbox" + objdir="${obj.dir}"> + <compiler name="sunc89"/> + <fileset dir="mbox/src/main/cpp" includes="**/*.c"/> + <includepath location="${generated.dir}"/> + <sysincludepath location="${java.home}/../include"/> + <sysincludepath location="${java.home}/../include/solaris"/> + <defineset> + <define name="_REENTRANT"/> + </defineset> + <compilerarg value="-Xa"/> + <compilerarg value="-xO2"/> + <linker name="sunc89"> + <syslibset libs="mail,java,c"/> + </linker> + </cc> + </target> + + <target name="clean" description="Clean the build"> + <delete includeEmptyDirs="true" failonerror="false"> + <fileset dir="${component.classes.mbox.dir}"/> + </delete> + </target> + + <!-- JavaMail bundle build targets --> + + <target name="mboxrelease" depends="init, mboxjar, mboxcc"> + <property name="rel" value="mbox-${release.version}"/> + <property name="zipname" value="${rel}.zip"/> + <delete file="${basedir}/mbox/target/${zipname}"/> + <echo message="Creating mbox bundle ${basedir}/mbox/target/${zipname}"/> + <zip destfile="${basedir}/mbox/target/${zipname}"> + <zipfileset dir="${resources.legal.dir}/META-INF" prefix="${rel}" + includes="LICENSE.txt"/> + <zipfileset dir="${release.dir}/lib" prefix="${rel}" + includes="mbox.jar"/> + <zipfileset dir="${lib.dir}/.." prefix="${rel}/lib" + includes="**/*.so"/> + </zip> + </target> + + <target name="mboxjar" depends="init, mboxcompile, cook-manifest"> + <mkdir dir="${release.dir}/lib"/> + <jar jarfile="${release.mbox.jar}" + manifest="${component.classes.mbox.dir}/manifest/MANIFEST.MF"> + <metainf dir="${resources.legal.dir}/META-INF" + includes="LICENSE.txt"/> + <fileset dir="${component.classes.mbox.dir}"> + <include name="com/sun/mail/mbox/*.class"/> + <include name="com/sun/mail/mbox/mailcap"/> + </fileset> + </jar> + </target> + + <target name="push-to-maven-prepare" depends="-push-to-maven-init, mboxjar" + description="creates an image for the 'push-to-maven' goal"> + <delete dir="target/maven-repo" /><!-- clean it --> + <maven-repository-importer destdir="target/maven-repo" + version="${release.version}"> + <artifact jar="target/release/mbox.jar" pom="mbox.pom" + srczip="target/mbox.src.zip" /> + </maven-repository-importer> + </target> + + <target name="push-to-maven" depends="push-to-maven-prepare" + description="pushes jars to the java.net maven repository"> + <cvs-import src="target/maven-repo" dest="glassfish/repo" /> + </target> +</project>
diff --git a/mbox/exclude.xml b/mbox/exclude.xml new file mode 100644 index 0000000..659e6e7 --- /dev/null +++ b/mbox/exclude.xml
@@ -0,0 +1,86 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<!-- FindBugs exclude list for JavaMail mbox provider --> + +<FindBugsFilter> + <!-- + Without multi-catch we just catch all possible exceptions. + --> + <Match> + <Class name="com.sun.mail.mbox.MboxStore"/> + <Method name="<init>"/> <!-- match constructor --> + <Bug pattern="REC_CATCH_EXCEPTION"/> + </Match> + + <!-- + We purposely return null instead of an empty list. + --> + <Match> + <Class name="com.sun.mail.mbox.MboxFolder"/> + <Method name="load"/> + <Bug pattern="PZLA_PREFER_ZERO_LENGTH_ARRAYS"/> + </Match> + + <!-- + We purposely don't close this stream, which is just a wrapper + around the original stream that needs to remain open. + --> + <Match> + <Class name="com.sun.mail.mbox.SunV3Multipart"/> + <Method name="parse"/> + <Bug pattern="OS_OPEN_STREAM"/> + </Match> + + <!-- + When deleting the temp file fails, there's really nothing to be done. + --> + <Match> + <Class name="com.sun.mail.mbox.TempFile"/> + <Method name="close"/> + <Bug pattern="RV_RETURN_VALUE_IGNORED_BAD_PRACTICE"/> + </Match> + + <!-- + We never serialize this class. + --> + <Match> + <Class name="com.sun.mail.mbox.UNIXInbox"/> + <Field name="lockfileName"/> + <Bug pattern="SE_TRANSIENT_FIELD_NOT_RESTORED"/> + </Match> + + <!-- + If close fails, what are we supposed to do? + --> + <Match> + <Class name="com.sun.mail.mbox.MboxFolder"/> + <Method name="appendMessages"/> + <Bug pattern="DE_MIGHT_IGNORE"/> + </Match> + + <!-- + If getSentDate fails, treat it the same as returning null. + --> + <Match> + <Class name="com.sun.mail.mbox.MboxMessage"/> + <Method name="getUnixFromLine"/> + <Bug pattern="DE_MIGHT_IGNORE"/> + </Match> +</FindBugsFilter>
diff --git a/mbox/native/pom.xml b/mbox/native/pom.xml new file mode 100644 index 0000000..b9220c9 --- /dev/null +++ b/mbox/native/pom.xml
@@ -0,0 +1,153 @@ +<?xml version="1.0" encoding="iso-8859-1"?> +<!-- + + Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 + http://maven.apache.org/maven-v4_0_0.xsd"> + <parent> + <groupId>com.sun.mail</groupId> + <artifactId>all</artifactId> + <version>1.6.3</version> + <relativePath>../../pom.xml</relativePath> + </parent> + <modelVersion>4.0.0</modelVersion> + <groupId>com.sun.mail</groupId> + <artifactId>libmbox</artifactId> + <packaging>so</packaging> + <name>JavaMail API mbox native library</name> + <description>JavaMail API mbox native library</description> + <url>http://java.sun.com/projects/javamail</url> + + <properties> + <m64> + </m64> + <compiler.name> + c89 + </compiler.name> + <compiler.start.options> + ${m64} -Xa -xO2 -v -D_REENTRANT -KPIC + -I${env.JAVA_HOME}/include -I${env.JAVA_HOME}/include/solaris + </compiler.start.options> + <linker.name> + c89 + </linker.name> + <linker.start.options> + -G ${m64} -KPIC -z text + </linker.start.options> + <linker.arch> + ${env.MACH} + </linker.arch> + <linker.end.options> + -L${env.JAVA_HOME}/jre/lib/${linker.arch} -lmail -ljava -lc + </linker.end.options> + </properties> + + <profiles> + <profile> + <!-- + Override the settings necessary to build with JDK 1.8. + --> + <id>1.8</id> + <properties> + <m64> + -m64 + </m64> + <linker.arch> + amd64 + </linker.arch> + </properties> + </profile> + </profiles> + + <build> + <plugins> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>native-maven-plugin</artifactId> + <version>1.0-alpha-7</version> + <extensions>true</extensions> + <configuration> + <compilerProvider>generic</compilerProvider> + <compilerExecutable>${compiler.name}</compilerExecutable> + <linkerExecutable>${linker.name}</linkerExecutable> + <compilerStartOptions> + <compilerStartOption> + ${compiler.start.options} + </compilerStartOption> + </compilerStartOptions> + <linkerStartOptions> + <linkerStartOption> + ${linker.start.options} + </linkerStartOption> + </linkerStartOptions> + <linkerEndOptions> + <linkerEndOption> + ${linker.end.options} + </linkerEndOption> + </linkerEndOptions> + <sources> + <source> + <directory>../src/main/cpp</directory> + <includes> + <include>**/*.c</include> + </includes> + </source> + <source> + <directory>target/native/javah</directory> + <dependencyAnalysisParticipation> + false + </dependencyAnalysisParticipation> + </source> + </sources> + </configuration> + <executions> + <execution> + <id>javah</id> + <phase>generate-sources</phase> + <goals> + <goal>javah</goal> + </goals> + <configuration> + <classNames> + <className> + com.sun.mail.mbox.UNIXFile + </className> + <className> + com.sun.mail.mbox.UNIXInbox + </className> + </classNames> + </configuration> + </execution> + </executions> + </plugin> + </plugins> + </build> + + <dependencies> + <dependency> + <groupId>com.sun.mail</groupId> + <artifactId>jakarta.mail</artifactId> + </dependency> + <dependency> + <groupId>com.sun.mail</groupId> + <artifactId>mbox</artifactId> + </dependency> + </dependencies> +</project>
diff --git a/mbox/pom.xml b/mbox/pom.xml new file mode 100644 index 0000000..7ac3d54 --- /dev/null +++ b/mbox/pom.xml
@@ -0,0 +1,78 @@ +<?xml version="1.0" encoding="iso-8859-1"?> +<!-- + + Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 + http://maven.apache.org/maven-v4_0_0.xsd"> + <parent> + <groupId>com.sun.mail</groupId> + <artifactId>all</artifactId> + <version>1.6.3</version> + </parent> + <modelVersion>4.0.0</modelVersion> + <groupId>com.sun.mail</groupId> + <artifactId>mbox</artifactId> + <packaging>jar</packaging> + <name>JavaMail API mbox provider</name> + + <properties> + <mail.packages.export> + com.sun.mail.mbox; version=${mail.osgiversion} + </mail.packages.export> + <findbugs.skip> + false + </findbugs.skip> + <findbugs.exclude> + ${project.basedir}/exclude.xml + </findbugs.exclude> + </properties> + + <build> + <plugins> + <!-- + Configure test plugin to find *TestSuite classes. + --> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-surefire-plugin</artifactId> + <configuration> + <includes> + <include>**/*Test.java</include> + <include>**/*TestSuite.java</include> + </includes> + </configuration> + </plugin> + </plugins> + </build> + + <dependencies> + <dependency> + <groupId>com.sun.mail</groupId> + <artifactId>jakarta.mail</artifactId> + </dependency> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <version>4.12</version> + <scope>test</scope> + <optional>true</optional> + </dependency> + </dependencies> +</project>
diff --git a/mbox/src/main/cpp/com/sun/mail/mbox/UNIXFile.c b/mbox/src/main/cpp/com/sun/mail/mbox/UNIXFile.c new file mode 100644 index 0000000..e66a772 --- /dev/null +++ b/mbox/src/main/cpp/com/sun/mail/mbox/UNIXFile.c
@@ -0,0 +1,100 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +#include <jni.h> +#include <fcntl.h> +#include <sys/types.h> +#include <sys/stat.h> + +extern int _fcntl(); + +#include "com_sun_mail_mbox_UNIXFile.h" + +static jfieldID IO_fd_fdID; +static int fd_offset; + +/* + * Class: com_sun_mail_mbox_UNIXFile + * Method: initIDs + * Signature: (Ljava/lang/Class;Ljava/io/FileDescriptor;)V + */ +JNIEXPORT void JNICALL +Java_com_sun_mail_mbox_UNIXFile_initIDs(JNIEnv *env, jclass ufClass, + jclass fdClass, jobject stdin_obj) +{ + IO_fd_fdID = (*env)->GetFieldID(env, fdClass, "fd", "I"); + /* + * Because pre-JDK 1.2 stored the "fd" as one more than + * its actual value, we remember the value it stored for + * stdin, which should be zero, and use it as the offset + * for other fd's we extract. + */ + fd_offset = (*env)->GetIntField(env, stdin_obj, IO_fd_fdID); +} + +/* + * Class: com_sun_mail_mbox_UNIXFile + * Method: lock0 + * Signature: (Ljava/io/FileDescriptor;Ljava/lang/String;Z)Z + */ +JNIEXPORT jboolean JNICALL +Java_com_sun_mail_mbox_UNIXFile_lock0(JNIEnv *env, jclass clazz, + jobject fdobj, jstring umode, jboolean block) +{ + int fd; + const char *mode; + static struct flock flock0; + struct flock flock = flock0; + + fd = (*env)->GetIntField(env, fdobj, IO_fd_fdID); + fd -= fd_offset; + /* XXX - a lot of work to examine one character in a string */ + mode = (*env)->GetStringUTFChars(env, umode, 0); + flock.l_type = mode[1] == 'w' ? F_WRLCK : F_RDLCK; + (*env)->ReleaseStringUTFChars(env, umode, mode); + flock.l_whence = SEEK_SET; + flock.l_start = 0; + flock.l_len = 0; + return (_fcntl(fd, block ? F_SETLKW : F_SETLK, &flock) == 0 ? + JNI_TRUE : JNI_FALSE); +} + +/* + * Class: com_sun_mail_mbox_UNIXFile + * Method: lastAccessed0 + * Signature: (Ljava/lang/String;)J + */ +JNIEXPORT jlong JNICALL +Java_com_sun_mail_mbox_UNIXFile_lastAccessed0(JNIEnv *env, jclass clazz, + jstring uname) +{ + const char *name; + jlong ret = -1; + struct stat st; + + name = (*env)->GetStringUTFChars(env, uname, 0); + if (stat(name, &st) == 0) { + /* + * Should be... + ret = (jlong)st.st_atim.tv_sec * 1000 + + st.st_atim.tv_nsec / 1000000; + * but for compatibility with lastModified we use... + */ + ret = (jlong)st.st_atime * 1000; + } + (*env)->ReleaseStringUTFChars(env, uname, name); + return ret; +}
diff --git a/mbox/src/main/cpp/com/sun/mail/mbox/UNIXInbox.c b/mbox/src/main/cpp/com/sun/mail/mbox/UNIXInbox.c new file mode 100644 index 0000000..274a44e --- /dev/null +++ b/mbox/src/main/cpp/com/sun/mail/mbox/UNIXInbox.c
@@ -0,0 +1,60 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +#include <jni.h> +#include <maillock.h> +extern void touchlock(); /* XXX - should be in maillock.h */ + +#include "com_sun_mail_mbox_UNIXInbox.h" + +/* + * Class: com_sun_mail_mbox_UNIXInbox + * Method: maillock + * Signature: (Ljava/lang/String;I)Z + */ +JNIEXPORT jboolean JNICALL +Java_com_sun_mail_mbox_UNIXInbox_maillock(JNIEnv *env, jobject obj, + jstring user, jint retry_count) +{ + jboolean ret; + const char *name = (*env)->GetStringUTFChars(env, user, 0); + ret = maillock((char *)name, retry_count) == L_SUCCESS ? + JNI_TRUE : JNI_FALSE; + (*env)->ReleaseStringUTFChars(env, user, name); + return (ret); +} + +/* + * Class: com_sun_mail_mbox_UNIXInbox + * Method: mailunlock + * Signature: ()V + */ +JNIEXPORT void JNICALL +Java_com_sun_mail_mbox_UNIXInbox_mailunlock(JNIEnv *env, jobject obj) +{ + (void) mailunlock(); +} + +/* + * Class: com_sun_mail_mbox_UNIXInbox + * Method: touchlock0 + * Signature: ()V + */ +JNIEXPORT void JNICALL +Java_com_sun_mail_mbox_UNIXInbox_touchlock0(JNIEnv *env, jobject obj) +{ + (void) touchlock(); +}
diff --git a/mbox/src/main/java/com/sun/mail/mbox/ContentLengthCounter.java b/mbox/src/main/java/com/sun/mail/mbox/ContentLengthCounter.java new file mode 100644 index 0000000..f01aa59 --- /dev/null +++ b/mbox/src/main/java/com/sun/mail/mbox/ContentLengthCounter.java
@@ -0,0 +1,75 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.mbox; + +import java.io.*; + +/** + * Count the number of bytes in the body of the message written to the stream. + */ +class ContentLengthCounter extends OutputStream { + private long size = 0; + private boolean inHeader = true; + private int lastb1 = -1, lastb2 = -1; + + public void write(int b) throws IOException { + if (inHeader) { + // if line terminator is CR + if (b == '\r' && lastb1 == '\r') + inHeader = false; + else if (b == '\n') { + // if line terminator is \n + if (lastb1 == '\n') + inHeader = false; + // if line terminator is CRLF + else if (lastb1 == '\r' && lastb2 == '\n') + inHeader = false; + } + lastb2 = lastb1; + lastb1 = b; + } else + size++; + } + + public void write(byte[] b) throws IOException { + if (inHeader) + super.write(b); + else + size += b.length; + } + + public void write(byte[] b, int off, int len) throws IOException { + if (inHeader) + super.write(b, off, len); + else + size += len; + } + + public long getSize() { + return size; + } + + /* + public static void main(String argv[]) throws Exception { + int b; + ContentLengthCounter os = new ContentLengthCounter(); + while ((b = System.in.read()) >= 0) + os.write(b); + System.out.println("size " + os.getSize()); + } + */ +}
diff --git a/mbox/src/main/java/com/sun/mail/mbox/ContentLengthUpdater.java b/mbox/src/main/java/com/sun/mail/mbox/ContentLengthUpdater.java new file mode 100644 index 0000000..b6d4408 --- /dev/null +++ b/mbox/src/main/java/com/sun/mail/mbox/ContentLengthUpdater.java
@@ -0,0 +1,116 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.mbox; + +import java.io.*; + +/** + * Update the Content-Length header in the message written to the stream. + */ +class ContentLengthUpdater extends FilterOutputStream { + private String contentLength; + private boolean inHeader = true; + private boolean sawContentLength = false; + private int lastb1 = -1, lastb2 = -1; + private StringBuilder line = new StringBuilder(); + + public ContentLengthUpdater(OutputStream os, long contentLength) { + super(os); + this.contentLength = "Content-Length: " + contentLength; + } + + public void write(int b) throws IOException { + if (inHeader) { + String eol = "\n"; + // First, determine if we're still in the header. + if (b == '\r') { + // if line terminator is CR + if (lastb1 == '\r') { + inHeader = false; + eol = "\r"; + // else, if line terminator is CRLF + } else if (lastb1 == '\n' && lastb2 == '\r') { + inHeader = false; + eol = "\r\n"; + } + // else, if line terminator is \n + } else if (b == '\n') { + if (lastb1 == '\n') { + inHeader = false; + eol = "\n"; + } + } + + // If we're no longer in the header, and we haven't seen + // a Content-Length header yet, it's time to put one out. + if (!inHeader && !sawContentLength) { + out.write(contentLength.getBytes("iso-8859-1")); + out.write(eol.getBytes("iso-8859-1")); + } + + // If we have a full line, see if it's a Content-Length header. + if (b == '\r' || (b == '\n' && lastb1 != '\r')) { + if (line.toString().regionMatches(true, 0, + "content-length:", 0, 15)) { + // yup, got it + sawContentLength = true; + // put out the new version + out.write(contentLength.getBytes("iso-8859-1")); + } else { + // not a Content-Length header, just write it out + out.write(line.toString().getBytes("iso-8859-1")); + } + line.setLength(0); // clear buffer for next line + } + if (b == '\r' || b == '\n') + out.write(b); // write out line terminator immediately + else + line.append((char)b); // accumulate characters of the line + + // rotate saved characters for next time through loop + lastb2 = lastb1; + lastb1 = b; + } else + out.write(b); // not in the header, just write it out + } + + public void write(byte[] b) throws IOException { + if (inHeader) + write(b, 0, b.length); + else + out.write(b); + } + + public void write(byte[] b, int off, int len) throws IOException { + if (inHeader) { + for (int i = 0 ; i < len ; i++) { + write(b[off + i]); + } + } else + out.write(b, off, len); + } + + // for testing + public static void main(String argv[]) throws Exception { + int b; + ContentLengthUpdater os = + new ContentLengthUpdater(System.out, Long.parseLong(argv[0])); + while ((b = System.in.read()) >= 0) + os.write(b); + os.flush(); + } +}
diff --git a/mbox/src/main/java/com/sun/mail/mbox/DefaultMailbox.java b/mbox/src/main/java/com/sun/mail/mbox/DefaultMailbox.java new file mode 100644 index 0000000..bac0b05 --- /dev/null +++ b/mbox/src/main/java/com/sun/mail/mbox/DefaultMailbox.java
@@ -0,0 +1,106 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.mbox; + +import java.io.*; + +public class DefaultMailbox extends Mailbox { + private final String home; + + private static final boolean homeRelative = + Boolean.getBoolean("mail.mbox.homerelative"); + + public DefaultMailbox() { + home = System.getProperty("user.home"); + } + + public MailFile getMailFile(String user, String folder) { + return new DefaultMailFile(filename(user, folder)); + } + + public String filename(String user, String folder) { + try { + char c = folder.charAt(0); + if (c == File.separatorChar) { + return folder; + } else if (c == '~') { + int i = folder.indexOf(File.separatorChar); + String tail = ""; + if (i > 0) { + tail = folder.substring(i); + folder = folder.substring(0, i); + } + return home + tail; + } else { + if (folder.equalsIgnoreCase("INBOX")) + folder = "INBOX"; + if (homeRelative) + return home + File.separator + folder; + else + return folder; + } + } catch (StringIndexOutOfBoundsException e) { + return folder; + } + } +} + +class DefaultMailFile extends File implements MailFile { + protected transient RandomAccessFile file; + + private static final long serialVersionUID = 3713116697523761684L; + + DefaultMailFile(String name) { + super(name); + } + + public boolean lock(String mode) { + try { + file = new RandomAccessFile(this, mode); + return true; + } catch (FileNotFoundException fe) { + return false; + } catch (IOException ie) { + file = null; + return false; + } + } + + public void unlock() { + if (file != null) { + try { + file.close(); + } catch (IOException e) { + // ignore it + } + file = null; + } + } + + public void touchlock() { + } + + public FileDescriptor getFD() { + if (file == null) + return null; + try { + return file.getFD(); + } catch (IOException e) { + return null; + } + } +}
diff --git a/mbox/src/main/java/com/sun/mail/mbox/FileInterface.java b/mbox/src/main/java/com/sun/mail/mbox/FileInterface.java new file mode 100644 index 0000000..891f0d5 --- /dev/null +++ b/mbox/src/main/java/com/sun/mail/mbox/FileInterface.java
@@ -0,0 +1,143 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.mbox; + +import java.io.File; +import java.io.FilenameFilter; + +public interface FileInterface { + /** + * Gets the name of the file. This method does not include the + * directory. + * @return the file name. + */ + public String getName(); + + /** + * Gets the path of the file. + * @return the file path. + */ + public String getPath(); + + /** + * Gets the absolute path of the file. + * @return the absolute file path. + */ + public String getAbsolutePath(); + + /** + * Gets the official, canonical path of the File. + * @return canonical path + */ + // XXX - JDK1.1 + // public String getCanonicalPath(); + + /** + * Gets the name of the parent directory. + * @return the parent directory, or null if one is not found. + */ + public String getParent(); + + /** + * Returns a boolean indicating whether or not a file exists. + */ + public boolean exists(); + + /** + * Returns a boolean indicating whether or not a writable file + * exists. + */ + public boolean canWrite(); + + /** + * Returns a boolean indicating whether or not a readable file + * exists. + */ + public boolean canRead(); + + /** + * Returns a boolean indicating whether or not a normal file + * exists. + */ + public boolean isFile(); + + /** + * Returns a boolean indicating whether or not a directory file + * exists. + */ + public boolean isDirectory(); + + /** + * Returns a boolean indicating whether the file name is absolute. + */ + public boolean isAbsolute(); + + /** + * Returns the last modification time. The return value should + * only be used to compare modification dates. It is meaningless + * as an absolute time. + */ + public long lastModified(); + + /** + * Returns the length of the file. + */ + public long length(); + + /** + * Creates a directory and returns a boolean indicating the + * success of the creation. Will return false if the directory already + * exists. + */ + public boolean mkdir(); + + /** + * Renames a file and returns a boolean indicating whether + * or not this method was successful. + * @param dest the new file name + */ + public boolean renameTo(File dest); + + /** + * Creates all directories in this path. This method + * returns true if the target (deepest) directory was created, + * false if the target directory was not created (e.g., if it + * existed previously). + */ + public boolean mkdirs(); + + /** + * Lists the files in a directory. Works only on directories. + * @return an array of file names. This list will include all + * files in the directory except the equivalent of "." and ".." . + */ + public String[] list(); + + /** + * Uses the specified filter to list files in a directory. + * @param filter the filter used to select file names + * @return the filter selected files in this directory. + * @see FilenameFilter + */ + public String[] list(FilenameFilter filter); + + /** + * Deletes the specified file. Returns true + * if the file could be deleted. + */ + public boolean delete(); +}
diff --git a/mbox/src/main/java/com/sun/mail/mbox/InboxFile.java b/mbox/src/main/java/com/sun/mail/mbox/InboxFile.java new file mode 100644 index 0000000..e4fffd5 --- /dev/null +++ b/mbox/src/main/java/com/sun/mail/mbox/InboxFile.java
@@ -0,0 +1,22 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.mbox; + +public interface InboxFile extends MailFile { + public boolean openLock(String mode); + public void closeLock(); +}
diff --git a/mbox/src/main/java/com/sun/mail/mbox/LineCounter.java b/mbox/src/main/java/com/sun/mail/mbox/LineCounter.java new file mode 100644 index 0000000..1ae4084 --- /dev/null +++ b/mbox/src/main/java/com/sun/mail/mbox/LineCounter.java
@@ -0,0 +1,64 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.mbox; + +import java.io.*; + +/** + * Count number of lines output. + */ +class LineCounter extends FilterOutputStream { + private int lastb = -1; + protected int lineCount; + + public LineCounter(OutputStream os) { + super(os); + } + + public void write(int b) throws IOException { + // If we have a full line, count it. + if (b == '\r' || (b == '\n' && lastb != '\r')) + lineCount++; + out.write(b); + lastb = b; + } + + public void write(byte[] b) throws IOException { + write(b, 0, b.length); + } + + public void write(byte[] b, int off, int len) throws IOException { + for (int i = 0 ; i < len ; i++) { + write(b[off + i]); + } + } + + public int getLineCount() { + return lineCount; + } + + // for testing + public static void main(String argv[]) throws Exception { + int b; + LineCounter os = + new LineCounter(System.out); + while ((b = System.in.read()) >= 0) + os.write(b); + os.flush(); + System.out.println(os.getLineCount()); + } +}
diff --git a/mbox/src/main/java/com/sun/mail/mbox/MailFile.java b/mbox/src/main/java/com/sun/mail/mbox/MailFile.java new file mode 100644 index 0000000..f790fce --- /dev/null +++ b/mbox/src/main/java/com/sun/mail/mbox/MailFile.java
@@ -0,0 +1,26 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.mbox; + +import java.io.FileDescriptor; + +public interface MailFile extends FileInterface { + public boolean lock(String mode); + public void unlock(); + public void touchlock(); + public FileDescriptor getFD(); +}
diff --git a/mbox/src/main/java/com/sun/mail/mbox/Mailbox.java b/mbox/src/main/java/com/sun/mail/mbox/Mailbox.java new file mode 100644 index 0000000..b902121 --- /dev/null +++ b/mbox/src/main/java/com/sun/mail/mbox/Mailbox.java
@@ -0,0 +1,29 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.mbox; + +public abstract class Mailbox { + /** + * Return a MailFile object for the specified user's folder. + */ + public abstract MailFile getMailFile(String user, String folder); + + /** + * Return the file name corresponding to a folder with the given name. + */ + public abstract String filename(String user, String folder); +}
diff --git a/mbox/src/main/java/com/sun/mail/mbox/MboxFolder.java b/mbox/src/main/java/com/sun/mail/mbox/MboxFolder.java new file mode 100644 index 0000000..340b84f --- /dev/null +++ b/mbox/src/main/java/com/sun/mail/mbox/MboxFolder.java
@@ -0,0 +1,1164 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.mbox; + +import javax.mail.*; +import javax.mail.event.*; +import javax.mail.internet.*; +import javax.mail.util.*; +import java.io.*; +import java.util.*; +import com.sun.mail.util.LineInputStream; + +/** + * This class represents a mailbox file containing RFC822 style email messages. + * + * @author John Mani + * @author Bill Shannon + */ + +public class MboxFolder extends Folder { + + private String name; // null => the default folder + private boolean is_inbox = false; + private int total; // total number of messages in mailbox + private volatile boolean opened = false; + private List<MessageMetadata> messages; + private TempFile temp; + private MboxStore mstore; + private MailFile folder; + private long file_size; // the size the last time we read or wrote it + private long saved_file_size; // size at the last open, close, or expunge + private boolean special_imap_message; + + private static final boolean homeRelative = + Boolean.getBoolean("mail.mbox.homerelative"); + + /** + * Metadata for each message, to avoid instantiating MboxMessage + * objects for messages we're not going to look at. <p> + * + * MboxFolder keeps an array of these objects corresponding to + * each message in the folder. Access to the array elements is + * synchronized, but access to contents of this object is not. + * The metadata stored here is only accessed if the message field + * is null; otherwise the MboxMessage object contains the metadata. + */ + static final class MessageMetadata { + public long start; // offset in temp file of start of this message + // public long end; // offset in temp file of end of this message + public long dataend; // offset of end of message data, <= "end" + public MboxMessage message; // the message itself + public boolean recent; // message is recent? + public boolean deleted; // message is marked deleted? + public boolean imap; // special imap message? + } + + public MboxFolder(MboxStore store, String name) { + super(store); + this.mstore = store; + this.name = name; + + if (name != null && name.equalsIgnoreCase("INBOX")) + is_inbox = true; + + folder = mstore.getMailFile(name == null ? "~" : name); + if (folder.exists()) + saved_file_size = folder.length(); + else + saved_file_size = -1; + } + + public char getSeparator() { + return File.separatorChar; + } + + public Folder[] list(String pattern) throws MessagingException { + if (!folder.isDirectory()) + throw new MessagingException("not a directory"); + + if (name == null) + return list(null, pattern, true); + else + return list(name + File.separator, pattern, false); + } + + /* + * Version of list shared by MboxStore and MboxFolder. + */ + protected Folder[] list(String ref, String pattern, boolean fromStore) + throws MessagingException { + if (ref != null && ref.length() == 0) + ref = null; + int i; + String refdir = null; + String realdir = null; + + pattern = canonicalize(ref, pattern); + if ((i = indexOfAny(pattern, "%*")) >= 0) { + refdir = pattern.substring(0, i); + } else { + refdir = pattern; + } + if ((i = refdir.lastIndexOf(File.separatorChar)) >= 0) { + // get rid of anything after directory name + refdir = refdir.substring(0, i + 1); + realdir = mstore.mb.filename(mstore.user, refdir); + } else if (refdir.length() == 0 || refdir.charAt(0) != '~') { + // no separator and doesn't start with "~" => home or cwd + refdir = null; + if (homeRelative) + realdir = mstore.home; + else + realdir = "."; + } else { + realdir = mstore.mb.filename(mstore.user, refdir); + } + List<String> flist = new ArrayList<String>(); + listWork(realdir, refdir, pattern, fromStore ? 0 : 1, flist); + if (Match.path("INBOX", pattern, '\0')) + flist.add("INBOX"); + + Folder fl[] = new Folder[flist.size()]; + for (i = 0; i < fl.length; i++) { + fl[i] = createFolder(mstore, flist.get(i)); + } + return fl; + } + + public String getName() { + if (name == null) + return ""; + else if (is_inbox) + return "INBOX"; + else + return folder.getName(); + } + + public String getFullName() { + if (name == null) + return ""; + else + return name; + } + + public Folder getParent() { + if (name == null) + return null; + else if (is_inbox) + return createFolder(mstore, null); + else + // XXX - have to recognize other folders under default folder + return createFolder(mstore, folder.getParent()); + } + + public boolean exists() { + return folder.exists(); + } + + public int getType() { + if (folder.isDirectory()) + return HOLDS_FOLDERS; + else + return HOLDS_MESSAGES; + } + + public Flags getPermanentFlags() { + return MboxStore.permFlags; + } + + public synchronized boolean hasNewMessages() { + if (folder instanceof UNIXFile) { + UNIXFile f = (UNIXFile)folder; + if (f.length() > 0) { + long atime = f.lastAccessed(); + long mtime = f.lastModified(); +//System.out.println(name + " atime " + atime + " mtime " + mtime); + return atime < mtime; + } + return false; + } + long current_size; + if (folder.exists()) + current_size = folder.length(); + else + current_size = -1; + // if we've never opened the folder, remember the size now + // (will cause us to return false the first time) + if (saved_file_size < 0) + saved_file_size = current_size; + return current_size > saved_file_size; + } + + public synchronized Folder getFolder(String name) + throws MessagingException { + if (folder.exists() && !folder.isDirectory()) + throw new MessagingException("not a directory"); + return createFolder(mstore, + (this.name == null ? "~" : this.name) + File.separator + name); + } + + public synchronized boolean create(int type) throws MessagingException { + switch (type) { + case HOLDS_FOLDERS: + if (!folder.mkdirs()) { + return false; + } + break; + + case HOLDS_MESSAGES: + if (folder.exists()) { + return false; + } + try { + (new FileOutputStream((File)folder)).close(); + } catch (FileNotFoundException fe) { + File parent = new File(folder.getParent()); + if (!parent.mkdirs()) + throw new + MessagingException("can't create folder: " + name); + try { + (new FileOutputStream((File)folder)).close(); + } catch (IOException ex3) { + throw new + MessagingException("can't create folder: " + name, ex3); + } + } catch (IOException e) { + throw new + MessagingException("can't create folder: " + name, e); + } + break; + + default: + throw new MessagingException("type not supported"); + } + notifyFolderListeners(FolderEvent.CREATED); + return true; + } + + public synchronized boolean delete(boolean recurse) + throws MessagingException { + checkClosed(); + if (name == null) + throw new MessagingException("can't delete default folder"); + boolean ret = true; + if (recurse && folder.isDirectory()) + ret = delete(new File(folder.getPath())); + if (ret && folder.delete()) { + notifyFolderListeners(FolderEvent.DELETED); + return true; + } + return false; + } + + /** + * Recursively delete the specified file/directory. + */ + private boolean delete(File f) { + File[] files = f.listFiles(); + boolean ret = true; + for (int i = 0; ret && i < files.length; i++) { + if (files[i].isDirectory()) + ret = delete(files[i]); + else + ret = files[i].delete(); + } + return ret; + } + + public synchronized boolean renameTo(Folder f) + throws MessagingException { + checkClosed(); + if (name == null) + throw new MessagingException("can't rename default folder"); + if (!(f instanceof MboxFolder)) + throw new MessagingException("can't rename to: " + f.getName()); + String newname = ((MboxFolder)f).folder.getPath(); + if (folder.renameTo(new File(newname))) { + notifyFolderRenamedListeners(f); + return true; + } + return false; + } + + /* Ensure the folder is open */ + void checkOpen() throws IllegalStateException { + if (!opened) + throw new IllegalStateException("Folder is not Open"); + } + + /* Ensure the folder is not open */ + private void checkClosed() throws IllegalStateException { + if (opened) + throw new IllegalStateException("Folder is Open"); + } + + /* + * Check that the given message number is within the range + * of messages present in this folder. If the message + * number is out of range, we check to see if new messages + * have arrived. + */ + private void checkRange(int msgno) throws MessagingException { + if (msgno < 1) // message-numbers start at 1 + throw new IndexOutOfBoundsException("message number < 1"); + + if (msgno <= total) + return; + + // Out of range, let's check if there are any new messages. + getMessageCount(); + + if (msgno > total) // Still out of range ? Throw up ... + throw new IndexOutOfBoundsException(msgno + " > " + total); + } + + /* Ensure the folder is open & readable */ + private void checkReadable() throws IllegalStateException { + if (!opened || (mode != READ_ONLY && mode != READ_WRITE)) + throw new IllegalStateException("Folder is not Readable"); + } + + /* Ensure the folder is open & writable */ + private void checkWritable() throws IllegalStateException { + if (!opened || mode != READ_WRITE) + throw new IllegalStateException("Folder is not Writable"); + } + + public boolean isOpen() { + return opened; + } + + /* + * Open the folder in the specified mode. + */ + public synchronized void open(int mode) throws MessagingException { + if (opened) + throw new IllegalStateException("Folder is already Open"); + + if (!folder.exists()) + throw new FolderNotFoundException(this, "Folder doesn't exist: " + + folder.getPath()); + this.mode = mode; + switch (mode) { + case READ_WRITE: + default: + if (!folder.canWrite()) + throw new MessagingException("Open Failure, can't write: " + + folder.getPath()); + // fall through... + + case READ_ONLY: + if (!folder.canRead()) + throw new MessagingException("Open Failure, can't read: " + + folder.getPath()); + break; + } + + if (is_inbox && folder instanceof InboxFile) { + InboxFile inf = (InboxFile)folder; + if (!inf.openLock(mode == READ_WRITE ? "rw" : "r")) + throw new MessagingException("Failed to lock INBOX"); + } + if (!folder.lock("r")) + throw new MessagingException("Failed to lock folder: " + name); + messages = new ArrayList<MessageMetadata>(); + total = 0; + Message[] msglist = null; + try { + temp = new TempFile(null); + saved_file_size = folder.length(); + msglist = load(0L, false); + } catch (IOException e) { + throw new MessagingException("IOException", e); + } finally { + folder.unlock(); + } + notifyConnectionListeners(ConnectionEvent.OPENED); + if (msglist != null) + notifyMessageAddedListeners(msglist); + opened = true; // now really opened + } + + public synchronized void close(boolean expunge) throws MessagingException { + checkOpen(); + + try { + if (mode == READ_WRITE) { + try { + writeFolder(true, expunge); + } catch (IOException e) { + throw new MessagingException("I/O Exception", e); + } + } + messages = null; + } finally { + opened = false; + if (is_inbox && folder instanceof InboxFile) { + InboxFile inf = (InboxFile)folder; + inf.closeLock(); + } + temp.close(); + temp = null; + notifyConnectionListeners(ConnectionEvent.CLOSED); + } + } + + /** + * Re-write the folder with the current contents of the messages. + * If closing is true, turn off the RECENT flag. If expunge is + * true, don't write out deleted messages (only used from close() + * when the message cache won't be accessed again). + * + * Return the number of messages written. + */ + protected int writeFolder(boolean closing, boolean expunge) + throws IOException, MessagingException { + + /* + * First, see if there have been any changes. + */ + int modified = 0, deleted = 0, recent = 0; + for (int msgno = 1; msgno <= total; msgno++) { + MessageMetadata md = messages.get(messageIndexOf(msgno)); + MboxMessage msg = md.message; + if (msg != null) { + Flags flags = msg.getFlags(); + if (msg.isModified() || !msg.origFlags.equals(flags)) + modified++; + if (flags.contains(Flags.Flag.DELETED)) + deleted++; + if (flags.contains(Flags.Flag.RECENT)) + recent++; + } else { + if (md.deleted) + deleted++; + if (md.recent) + recent++; + } + } + if ((!closing || recent == 0) && (!expunge || deleted == 0) && + modified == 0) + return 0; + + /* + * Have to save any new mail that's been appended to the + * folder since we last loaded it. + */ + if (!folder.lock("rw")) + throw new MessagingException("Failed to lock folder: " + name); + int oldtotal = total; // XXX + Message[] msglist = null; + if (folder.length() != file_size) + msglist = load(file_size, !closing); + // don't use the folder's FD, need to re-open in order to trunc the file + OutputStream os = + new BufferedOutputStream(new FileOutputStream((File)folder)); + int wr = 0; + boolean keep = true; + try { + if (special_imap_message) + appendStream(getMessageStream(0), os); + for (int msgno = 1; msgno <= total; msgno++) { + MessageMetadata md = messages.get(messageIndexOf(msgno)); + MboxMessage msg = md.message; + if (msg != null) { + if (expunge && msg.isSet(Flags.Flag.DELETED)) + continue; // skip it; + if (closing && msgno <= oldtotal && + msg.isSet(Flags.Flag.RECENT)) + msg.setFlag(Flags.Flag.RECENT, false); + writeMboxMessage(msg, os); + } else { + if (expunge && md.deleted) + continue; // skip it; + if (closing && msgno <= oldtotal && md.recent) { + // have to instantiate message so that we can + // clear the recent flag + msg = (MboxMessage)getMessage(msgno); + msg.setFlag(Flags.Flag.RECENT, false); + writeMboxMessage(msg, os); + } else { + appendStream(getMessageStream(msgno), os); + } + } + folder.touchlock(); + wr++; + } + // If no messages in the mailbox, and we're closing, + // maybe we should remove the mailbox. + if (wr == 0 && closing) { + String skeep = ((MboxStore)store).getSession(). + getProperty("mail.mbox.deleteEmpty"); + if (skeep != null && skeep.equalsIgnoreCase("true")) + keep = false; + } + } catch (IOException e) { + throw e; + } catch (MessagingException e) { + throw e; + } catch (Exception e) { +e.printStackTrace(); + throw new MessagingException("unexpected exception " + e); + } finally { + // close the folder, flushing out the data + try { + os.close(); + file_size = saved_file_size = folder.length(); + if (!keep) { + folder.delete(); + file_size = 0; + } + } catch (IOException ex) {} + + if (keep) { + // make sure the access time is greater than the mod time + // XXX - would be nice to have utime() + try { + Thread.sleep(1000); // sleep for a second + } catch (InterruptedException ex) {} + InputStream is = null; + try { + is = new FileInputStream((File)folder); + is.read(); // read a byte + } catch (IOException ex) {} // ignore errors + try { + if (is != null) + is.close(); + is = null; + } catch (IOException ex) {} // ignore errors + } + + folder.unlock(); + if (msglist != null) + notifyMessageAddedListeners(msglist); + } + return wr; + } + + /** + * Append the input stream to the output stream, closing the + * input stream when done. + */ + private static final void appendStream(InputStream is, OutputStream os) + throws IOException { + try { + byte[] buf = new byte[64 * 1024]; + int len; + while ((len = is.read(buf)) > 0) + os.write(buf, 0, len); + } finally { + is.close(); + } + } + + /** + * Write a MimeMessage to the specified OutputStream in a + * format suitable for a UNIX mailbox, i.e., including a correct + * Content-Length header and with the local platform's line + * terminating convention. <p> + * + * If the message is really a MboxMessage, use its writeToFile + * method, which has access to the UNIX From line. Otherwise, do + * all the work here, creating an appropriate UNIX From line. + */ + public static void writeMboxMessage(MimeMessage msg, OutputStream os) + throws IOException, MessagingException { + try { + if (msg instanceof MboxMessage) { + ((MboxMessage)msg).writeToFile(os); + } else { + // XXX - modify the message to preserve the flags in headers + MboxMessage.setHeadersFromFlags(msg); + ContentLengthCounter cos = new ContentLengthCounter(); + NewlineOutputStream nos = new NewlineOutputStream(cos); + msg.writeTo(nos); + nos.flush(); + os = new NewlineOutputStream(os, true); + os = new ContentLengthUpdater(os, cos.getSize()); + PrintStream pos = new PrintStream(os, false, "iso-8859-1"); + pos.println(getUnixFrom(msg)); + msg.writeTo(pos); + pos.flush(); + } + } catch (MessagingException me) { + throw me; + } catch (IOException ioe) { + throw ioe; + } + } + + /** + * Construct an appropriately formatted UNIX From line using + * the sender address and the date in the message. + */ + protected static String getUnixFrom(MimeMessage msg) { + Address[] afrom; + String from; + Date ddate; + String date; + try { + if ((afrom = msg.getFrom()) == null || + !(afrom[0] instanceof InternetAddress) || + (from = ((InternetAddress)afrom[0]).getAddress()) == null) + from = "UNKNOWN"; + if ((ddate = msg.getReceivedDate()) == null || + (ddate = msg.getSentDate()) == null) + ddate = new Date(); + } catch (MessagingException e) { + from = "UNKNOWN"; + ddate = new Date(); + } + date = ddate.toString(); + // date is of the form "Sat Aug 12 02:30:00 PDT 1995" + // need to strip out the timezone + return "From " + from + " " + + date.substring(0, 20) + date.substring(24); + } + + public synchronized int getMessageCount() throws MessagingException { + if (!opened) + return -1; + + boolean locked = false; + Message[] msglist = null; + try { + if (folder.length() != file_size) { + if (!folder.lock("r")) + throw new MessagingException("Failed to lock folder: " + + name); + locked = true; + msglist = load(file_size, true); + } + } catch (IOException e) { + throw new MessagingException("I/O Exception", e); + } finally { + if (locked) { + folder.unlock(); + if (msglist != null) + notifyMessageAddedListeners(msglist); + } + } + return total; + } + + /** + * Get the specified message. Note that messages are numbered + * from 1. + */ + public synchronized Message getMessage(int msgno) + throws MessagingException { + checkReadable(); + checkRange(msgno); + + MessageMetadata md = messages.get(messageIndexOf(msgno)); + MboxMessage m = md.message; + if (m == null) { + InputStream is = getMessageStream(msgno); + try { + m = loadMessage(is, msgno, mode == READ_WRITE); + } catch (IOException ex) { + MessagingException mex = + new MessageRemovedException("mbox message gone", ex); + throw mex; + } + md.message = m; + } + return m; + } + + private final int messageIndexOf(int msgno) { + return special_imap_message ? msgno : msgno - 1; + } + + private InputStream getMessageStream(int msgno) { + int index = messageIndexOf(msgno); + MessageMetadata md = messages.get(index); + return temp.newStream(md.start, md.dataend); + } + + public synchronized void appendMessages(Message[] msgs) + throws MessagingException { + if (!folder.lock("rw")) + throw new MessagingException("Failed to lock folder: " + name); + + OutputStream os = null; + boolean err = false; + try { + os = new BufferedOutputStream( + new FileOutputStream(((File)folder).getPath(), true)); + // XXX - should use getAbsolutePath()? + for (int i = 0; i < msgs.length; i++) { + if (msgs[i] instanceof MimeMessage) { + writeMboxMessage((MimeMessage)msgs[i], os); + } else { + err = true; + continue; + } + folder.touchlock(); + } + } catch (IOException e) { + throw new MessagingException("I/O Exception", e); + } catch (MessagingException e) { + throw e; + } catch (Exception e) { +e.printStackTrace(); + throw new MessagingException("unexpected exception " + e); + } finally { + if (os != null) + try { + os.close(); + } catch (IOException e) { + // ignored + } + folder.unlock(); + } + if (opened) + getMessageCount(); // loads new messages as a side effect + if (err) + throw new MessagingException("Can't append non-Mime message"); + } + + public synchronized Message[] expunge() throws MessagingException { + checkWritable(); + + /* + * First, write out the folder to make sure we have permission, + * disk space, etc. + */ + int wr = total; // number of messages written out + try { + wr = writeFolder(false, true); + } catch (IOException e) { + throw new MessagingException("expunge failed", e); + } + if (wr == total) // wrote them all => nothing expunged + return new Message[0]; + + /* + * Now, actually get rid of the expunged messages. + */ + int del = 0; + Message[] msglist = new Message[total - wr]; + int msgno = 1; + while (msgno <= total) { + MessageMetadata md = messages.get(messageIndexOf(msgno)); + MboxMessage msg = md.message; + if (msg != null) { + if (msg.isSet(Flags.Flag.DELETED)) { + msg.setExpunged(true); + msglist[del] = msg; + del++; + messages.remove(messageIndexOf(msgno)); + total--; + } else { + msg.setMessageNumber(msgno); // update message number + msgno++; + } + } else { + if (md.deleted) { + // have to instantiate it for the notification + msg = (MboxMessage)getMessage(msgno); + msg.setExpunged(true); + msglist[del] = msg; + del++; + messages.remove(messageIndexOf(msgno)); + total--; + } else { + msgno++; + } + } + } + if (del != msglist.length) // this is really an assert + throw new MessagingException("expunge delete count wrong"); + notifyMessageRemovedListeners(true, msglist); + return msglist; + } + + /* + * Load more messages from the folder starting at the specified offset. + */ + private Message[] load(long offset, boolean notify) + throws MessagingException, IOException { + int oldtotal = total; + MessageLoader loader = new MessageLoader(temp); + int loaded = loader.load(folder.getFD(), offset, messages); + total += loaded; + file_size = folder.length(); + + if (offset == 0 && loaded > 0) { + /* + * If the first message is the special message that the + * IMAP server adds to the mailbox, remember that we've + * seen it so it won't be shown to the user. + */ + MessageMetadata md = messages.get(0); + if (md.imap) { + special_imap_message = true; + total--; + } + } + if (notify) { + Message[] msglist = new Message[total - oldtotal]; + for (int i = oldtotal, j = 0; i < total; i++, j++) + msglist[j] = getMessage(i + 1); + return msglist; + } else + return null; + } + + /** + * Parse the input stream and return an appropriate message object. + * The InputStream must be a SharedInputStream. + */ + private MboxMessage loadMessage(InputStream is, int msgno, + boolean writable) throws MessagingException, IOException { + LineInputStream in = new LineInputStream(is); + + /* + * Read lines until a UNIX From line, + * skipping blank lines. + */ + String line; + String unix_from = null; + while ((line = in.readLine()) != null) { + if (line.trim().length() == 0) + continue; + if (line.startsWith("From ")) { + /* + * A UNIX From line looks like: + * From address Day Mon DD HH:MM:SS YYYY + */ + unix_from = line; + int i; + // find the space after the address, before the date + i = unix_from.indexOf(' ', 5); + if (i < 0) + continue; // not a valid UNIX From line + break; + } + throw new MessagingException("Garbage in mailbox: " + line); + } + + if (unix_from == null) + throw new EOFException("end of mailbox"); + + /* + * Now load the RFC822 headers into an InternetHeaders object. + */ + InternetHeaders hdrs = new InternetHeaders(is); + + // the rest is the message content + SharedInputStream sis = (SharedInputStream)is; + InputStream stream = sis.newStream(sis.getPosition(), -1); + return new MboxMessage(this, hdrs, stream, msgno, unix_from, writable); + } + + /* + * Only here to make accessible to MboxMessage. + */ + protected void notifyMessageChangedListeners(int type, Message m) { + super.notifyMessageChangedListeners(type, m); + } + + + /** + * this is an exact duplicate of the Folder.getURL except it doesn't + * add a beginning '/' to the URLName. + */ + public URLName getURLName() { + // XXX - note: this should not be done this way with the + // new javax.mail apis. + + URLName storeURL = getStore().getURLName(); + if (name == null) + return storeURL; + + char separator = getSeparator(); + String fullname = getFullName(); + StringBuilder encodedName = new StringBuilder(); + + // We need to encode each of the folder's names, and replace + // the store's separator char with the URL char '/'. + StringTokenizer tok = new StringTokenizer( + fullname, Character.toString(separator), true); + + while (tok.hasMoreTokens()) { + String s = tok.nextToken(); + if (s.charAt(0) == separator) + encodedName.append("/"); + else + // XXX - should encode, but since there's no decoder... + //encodedName.append(java.net.URLEncoder.encode(s)); + encodedName.append(s); + } + + return new URLName(storeURL.getProtocol(), storeURL.getHost(), + storeURL.getPort(), encodedName.toString(), + storeURL.getUsername(), + null /* no password */); + } + + /** + * Create an MboxFolder object, or a subclass thereof. + * Can be overridden by subclasses of MboxFolder so that + * the appropriate subclass is created by the list method. + */ + protected Folder createFolder(MboxStore store, String name) { + return new MboxFolder(store, name); + } + + /* + * Support routines for list(). + */ + + /** + * Return a canonicalized pattern given a reference name and a pattern. + */ + private static String canonicalize(String ref, String pat) { + if (ref == null) + return pat; + try { + if (pat.length() == 0) { + return ref; + } else if (pat.charAt(0) == File.separatorChar) { + return ref.substring(0, ref.indexOf(File.separatorChar)) + pat; + } else { + return ref + pat; + } + } catch (StringIndexOutOfBoundsException e) { + return pat; + } + } + + /** + * Return the first index of any of the characters in "any" in "s", + * or -1 if none are found. + * + * This should be a method on String. + */ + private static int indexOfAny(String s, String any) { + try { + int len = s.length(); + for (int i = 0; i < len; i++) { + if (any.indexOf(s.charAt(i)) >= 0) + return i; + } + return -1; + } catch (StringIndexOutOfBoundsException e) { + return -1; + } + } + + /** + * The recursive part of generating the list of mailboxes. + * realdir is the full pathname to the directory to search. + * dir is the name the user uses, often a relative name that's + * relative to the user's home directory. dir (if not null) always + * has a trailing file separator character. + * + * @param realdir real pathname of directory to start looking in + * @param dir user's name for realdir + * @param pat pattern to match against + * @param level level of the directory hierarchy we're in + * @param flist list to which to add folder names that match + */ + // Derived from the c-client listWork() function. + private void listWork(String realdir, String dir, String pat, + int level, List<String> flist) { + String sl[]; + File fdir = new File(realdir); + try { + sl = fdir.list(); + } catch (SecurityException e) { + return; // can't read it, ignore it + } + + if (level == 0 && dir != null && + Match.path(dir, pat, File.separatorChar)) + flist.add(dir); + + if (sl == null) + return; // nothing return, we're done + + if (realdir.charAt(realdir.length() - 1) != File.separatorChar) + realdir += File.separator; + + for (int i = 0; i < sl.length; i++) { + if (sl[i].charAt(0) == '.') + continue; // ignore all "dot" files for now + String md = realdir + sl[i]; + File mf = new File(md); + if (!mf.exists()) + continue; + String name; + if (dir != null) + name = dir + sl[i]; + else + name = sl[i]; + if (mf.isDirectory()) { + if (Match.path(name, pat, File.separatorChar)) { + flist.add(name); + name += File.separator; + } else { + name += File.separator; + if (Match.path(name, pat, File.separatorChar)) + flist.add(name); + } + if (Match.dir(name, pat, File.separatorChar)) + listWork(md, name, pat, level + 1, flist); + } else { + if (Match.path(name, pat, File.separatorChar)) + flist.add(name); + } + } + } +} + +/** + * Pattern matching support class for list(). + * Should probably be more public. + */ +// Translated from the c-client functions pmatch_full() and dmatch(). +class Match { + /** + * Pathname pattern match + * + * @param s base string + * @param pat pattern string + * @param delim delimiter character + * @return true if base matches pattern + */ + static public boolean path(String s, String pat, char delim) { + try { + return path(s, 0, s.length(), pat, 0, pat.length(), delim); + } catch (StringIndexOutOfBoundsException e) { + return false; + } + } + + static private boolean path(String s, int s_index, int s_len, + String pat, int p_index, int p_len, char delim) + throws StringIndexOutOfBoundsException { + + while (p_index < p_len) { + char c = pat.charAt(p_index); + switch (c) { + case '%': + if (++p_index >= p_len) // % at end of pattern + // ok if no delimiters + return delim == 0 || s.indexOf(delim, s_index) < 0; + // scan remainder until delimiter + do { + if (path(s, s_index, s_len, pat, p_index, p_len, delim)) + return true; + } while (s.charAt(s_index) != delim && ++s_index < s_len); + // ran into a delimiter or ran out of string without a match + return false; + + case '*': + if (++p_index >= p_len) // end of pattern? + return true; // unconditional match + do { + if (path(s, s_index, s_len, pat, p_index, p_len, delim)) + return true; + } while (++s_index < s_len); + // ran out of string without a match + return false; + + default: + // if ran out of string or no match, fail + if (s_index >= s_len || c != s.charAt(s_index)) + return false; + + // try the next string and pattern characters + s_index++; + p_index++; + } + } + return s_index >= s_len; + } + + /** + * Directory pattern match + * + * @param s base string + * @param pat pattern string + * @return true if base is a matching directory of pattern + */ + static public boolean dir(String s, String pat, char delim) { + try { + return dir(s, 0, s.length(), pat, 0, pat.length(), delim); + } catch (StringIndexOutOfBoundsException e) { + return false; + } + } + + static private boolean dir(String s, int s_index, int s_len, + String pat, int p_index, int p_len, char delim) + throws StringIndexOutOfBoundsException { + + while (p_index < p_len) { + char c = pat.charAt(p_index); + switch (c) { + case '%': + if (s_index >= s_len) // end of base? + return true; // subset match + if (++p_index >= p_len) // % at end of pattern? + return false; // no inferiors permitted + do { + if (dir(s, s_index, s_len, pat, p_index, p_len, delim)) + return true; + } while (s.charAt(s_index) != delim && ++s_index < s_len); + + if (s_index + 1 == s_len) // s ends with a delimiter + return true; // must be a subset of pattern + return dir(s, s_index, s_len, pat, p_index, p_len, delim); + + case '*': + return true; // unconditional match + + default: + if (s_index >= s_len) // end of base? + return c == delim; // matched if at delimiter + + if (c != s.charAt(s_index)) + return false; + + // try the next string and pattern characters + s_index++; + p_index++; + } + } + return s_index >= s_len; + } +} + +/** + * A ByteArrayOutputStream that allows us to share the byte array + * rather than copy it. Eventually could replace this with something + * that doesn't require a single contiguous byte array. + */ +class SharedByteArrayOutputStream extends ByteArrayOutputStream { + public SharedByteArrayOutputStream(int size) { + super(size); + } + + public InputStream toStream() { + return new SharedByteArrayInputStream(buf, 0, count); + } +}
diff --git a/mbox/src/main/java/com/sun/mail/mbox/MboxMessage.java b/mbox/src/main/java/com/sun/mail/mbox/MboxMessage.java new file mode 100644 index 0000000..db57a26 --- /dev/null +++ b/mbox/src/main/java/com/sun/mail/mbox/MboxMessage.java
@@ -0,0 +1,532 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.mbox; + +import java.io.*; +import java.util.StringTokenizer; +import java.util.Date; +import java.text.SimpleDateFormat; +import javax.activation.*; +import javax.mail.*; +import javax.mail.internet.*; +import javax.mail.event.MessageChangedEvent; +import com.sun.mail.util.LineInputStream; + +/** + * This class represents an RFC822 style email message that resides in a file. + * + * @author Bill Shannon + */ + +public class MboxMessage extends MimeMessage { + + boolean writable = false; + // original msg flags, used by MboxFolder to detect modification + Flags origFlags; + /* + * A UNIX From line looks like: + * From address Day Mon DD HH:MM:SS YYYY + */ + String unix_from; + InternetAddress unix_from_user; + Date rcvDate; + int lineCount = -1; + private static OutputStream nullOutputStream = new OutputStream() { + public void write(int b) { } + public void write(byte[] b, int off, int len) { } + }; + + /** + * Construct an MboxMessage from the InputStream. + */ + public MboxMessage(Session session, InputStream is) + throws MessagingException, IOException { + super(session); + BufferedInputStream bis; + if (is instanceof BufferedInputStream) + bis = (BufferedInputStream)is; + else + bis = new BufferedInputStream(is); + LineInputStream dis = new LineInputStream(bis); + bis.mark(1024); + String line = dis.readLine(); + if (line != null && line.startsWith("From ")) + this.unix_from = line; + else + bis.reset(); + parse(bis); + saved = true; + } + + /** + * Construct an MboxMessage using the given InternetHeaders object + * and content from an InputStream. + */ + public MboxMessage(MboxFolder folder, InternetHeaders hdrs, InputStream is, + int msgno, String unix_from, boolean writable) + throws MessagingException { + super(folder, hdrs, null, msgno); + setFlagsFromHeaders(); + origFlags = getFlags(); + this.unix_from = unix_from; + this.writable = writable; + this.contentStream = is; + } + + /** + * Returns the "From" attribute. The "From" attribute contains + * the identity of the person(s) who wished this message to + * be sent. <p> + * + * If our superclass doesn't have a value, we return the address + * from the UNIX From line. + * + * @return array of Address objects + * @exception MessagingException + */ + public Address[] getFrom() throws MessagingException { + Address[] ret = super.getFrom(); + if (ret == null) { + InternetAddress ia = getUnixFrom(); + if (ia != null) + ret = new InternetAddress[] { ia }; + } + return ret; + } + + /** + * Returns the address from the UNIX "From" line. + * + * @return UNIX From address + * @exception MessagingException + */ + public synchronized InternetAddress getUnixFrom() + throws MessagingException { + if (unix_from_user == null && unix_from != null) { + int i; + // find the space after the address, before the date + i = unix_from.indexOf(' ', 5); + if (i > 5) { + try { + unix_from_user = + new InternetAddress(unix_from.substring(5, i)); + } catch (AddressException e) { + // ignore it + } + } + } + return unix_from_user != null ? + (InternetAddress)unix_from_user.clone() : null; + } + + private String getUnixFromLine() { + if (unix_from != null) + return unix_from; + String from = "unknown"; + try { + Address[] froma = getFrom(); + if (froma != null && froma.length > 0 && + froma[0] instanceof InternetAddress) + from = ((InternetAddress)froma[0]).getAddress(); + } catch (MessagingException ex) { } + Date d = null; + try { + d = getSentDate(); + } catch (MessagingException ex) { + // ignore + } + if (d == null) + d = new Date(); + // From shannon Mon Jun 10 12:06:52 2002 + SimpleDateFormat fmt = new SimpleDateFormat("EEE LLL dd HH:mm:ss yyyy"); + return "From " + from + " " + fmt.format(d); + } + + /** + * Get the date this message was received, from the UNIX From line. + * + * @return the date this message was received + * @exception MessagingException + */ + @SuppressWarnings("deprecation") // for Date constructor + public Date getReceivedDate() throws MessagingException { + if (rcvDate == null && unix_from != null) { + int i; + // find the space after the address, before the date + i = unix_from.indexOf(' ', 5); + if (i > 5) { + try { + rcvDate = new Date(unix_from.substring(i)); + } catch (IllegalArgumentException iae) { + // ignore it + } + } + } + return rcvDate == null ? null : new Date(rcvDate.getTime()); + } + + /** + * Return the number of lines for the content of this message. + * Return -1 if this number cannot be determined. <p> + * + * Note that this number may not be an exact measure of the + * content length and may or may not account for any transfer + * encoding of the content. <p> + * + * This implementation returns -1. + * + * @return number of lines in the content. + * @exception MessagingException + */ + public int getLineCount() throws MessagingException { + if (lineCount < 0 && isMimeType("text/plain")) { + LineCounter lc = null; + // writeTo will set the SEEN flag, remember the original state + boolean seen = isSet(Flags.Flag.SEEN); + try { + lc = new LineCounter(nullOutputStream); + getDataHandler().writeTo(lc); + lineCount = lc.getLineCount(); + } catch (IOException ex) { + // ignore it, can't happen + } finally { + try { + if (lc != null) + lc.close(); + } catch (IOException ex) { + // can't happen + } + } + if (!seen) + setFlag(Flags.Flag.SEEN, false); + } + return lineCount; + } + + /** + * Set the specified flags on this message to the specified value. + * + * @param flags the flags to be set + * @param set the value to be set + */ + public void setFlags(Flags newFlags, boolean set) + throws MessagingException { + Flags oldFlags = (Flags)flags.clone(); + super.setFlags(newFlags, set); + if (!flags.equals(oldFlags)) { + setHeadersFromFlags(this); + if (folder != null) + ((MboxFolder)folder).notifyMessageChangedListeners( + MessageChangedEvent.FLAGS_CHANGED, this); + } + } + + /** + * Return the content type, mapping from SunV3 types to MIME types + * as necessary. + */ + public String getContentType() throws MessagingException { + String ct = super.getContentType(); + if (ct.indexOf('/') < 0) + ct = SunV3BodyPart.MimeV3Map.toMime(ct); + return ct; + } + + /** + * Produce the raw bytes of the content. This method is used during + * parsing, to create a DataHandler object for the content. Subclasses + * that can provide a separate input stream for just the message + * content might want to override this method. <p> + * + * This implementation just returns a ByteArrayInputStream constructed + * out of the <code>content</code> byte array. + * + * @see #content + */ + protected InputStream getContentStream() throws MessagingException { + if (folder != null) + ((MboxFolder)folder).checkOpen(); + if (isExpunged()) + throw new MessageRemovedException("mbox message expunged"); + if (!isSet(Flags.Flag.SEEN)) + setFlag(Flags.Flag.SEEN, true); + return super.getContentStream(); + } + + /** + * Return a DataHandler for this Message's content. + * If this is a SunV3 multipart message, handle it specially. + * + * @exception MessagingException + */ + public synchronized DataHandler getDataHandler() + throws MessagingException { + if (dh == null) { + // XXX - Following is a kludge to avoid having to register + // the "multipart/x-sun-attachment" data type with the JAF. + String ct = getContentType(); + if (ct.equalsIgnoreCase("multipart/x-sun-attachment")) + dh = new DataHandler( + new SunV3Multipart(new MimePartDataSource(this)), ct); + else + return super.getDataHandler(); // will set "dh" + } + return dh; + } + + // here only to allow package private access from MboxFolder + protected void setMessageNumber(int msgno) { + super.setMessageNumber(msgno); + } + + // here to synchronize access to expunged field + public synchronized boolean isExpunged() { + return super.isExpunged(); + } + + // here to synchronize and to allow access from MboxFolder + protected synchronized void setExpunged(boolean expunged) { + super.setExpunged(expunged); + } + + // XXX - We assume that only body parts that are part of a SunV3 + // multipart will use the SunV3 headers (X-Sun-Content-Length, + // X-Sun-Content-Lines, X-Sun-Data-Type, X-Sun-Encoding-Info, + // X-Sun-Data-Description, X-Sun-Data-Name) so we don't handle + // them here. + + /** + * Set the flags for this message based on the Status, + * X-Status, and X-Dt-Delete-Time headers. + * + * SIMS 2.0: + * "X-Status: DFAT", deleted, flagged, answered, draft. + * Unset flags represented as "$". + * User flags not supported. + * + * University of Washington IMAP server: + * "X-Status: DFAT", deleted, flagged, answered, draft. + * Unset flags not present. + * "X-Keywords: userflag1 userflag2" + */ + private synchronized void setFlagsFromHeaders() { + flags = new Flags(Flags.Flag.RECENT); + try { + String s = getHeader("Status", null); + if (s != null) { + if (s.indexOf('R') >= 0) + flags.add(Flags.Flag.SEEN); + if (s.indexOf('O') >= 0) + flags.remove(Flags.Flag.RECENT); + } + s = getHeader("X-Dt-Delete-Time", null); // set by dtmail + if (s != null) + flags.add(Flags.Flag.DELETED); + s = getHeader("X-Status", null); // set by IMAP server + if (s != null) { + if (s.indexOf('D') >= 0) + flags.add(Flags.Flag.DELETED); + if (s.indexOf('F') >= 0) + flags.add(Flags.Flag.FLAGGED); + if (s.indexOf('A') >= 0) + flags.add(Flags.Flag.ANSWERED); + if (s.indexOf('T') >= 0) + flags.add(Flags.Flag.DRAFT); + } + s = getHeader("X-Keywords", null); // set by IMAP server + if (s != null) { + StringTokenizer st = new StringTokenizer(s); + while (st.hasMoreTokens()) + flags.add(st.nextToken()); + } + } catch (MessagingException e) { + // ignore it + } + } + + /** + * Set the various header fields that represent the message flags. + */ + static void setHeadersFromFlags(MimeMessage msg) { + try { + Flags flags = msg.getFlags(); + StringBuilder status = new StringBuilder(); + if (flags.contains(Flags.Flag.SEEN)) + status.append('R'); + if (!flags.contains(Flags.Flag.RECENT)) + status.append('O'); + if (status.length() > 0) + msg.setHeader("Status", status.toString()); + else + msg.removeHeader("Status"); + + boolean sims = false; + String s = msg.getHeader("X-Status", null); + // is it a SIMS 2.0 format X-Status header? + sims = s != null && s.length() == 4 && s.indexOf('$') >= 0; + status.setLength(0); + if (flags.contains(Flags.Flag.DELETED)) + status.append('D'); + else if (sims) + status.append('$'); + if (flags.contains(Flags.Flag.FLAGGED)) + status.append('F'); + else if (sims) + status.append('$'); + if (flags.contains(Flags.Flag.ANSWERED)) + status.append('A'); + else if (sims) + status.append('$'); + if (flags.contains(Flags.Flag.DRAFT)) + status.append('T'); + else if (sims) + status.append('$'); + if (status.length() > 0) + msg.setHeader("X-Status", status.toString()); + else + msg.removeHeader("X-Status"); + + String[] userFlags = flags.getUserFlags(); + if (userFlags.length > 0) { + status.setLength(0); + for (int i = 0; i < userFlags.length; i++) + status.append(userFlags[i]).append(' '); + status.setLength(status.length() - 1); // smash trailing space + msg.setHeader("X-Keywords", status.toString()); + } + if (flags.contains(Flags.Flag.DELETED)) { + s = msg.getHeader("X-Dt-Delete-Time", null); + if (s == null) + // XXX - should be time + msg.setHeader("X-Dt-Delete-Time", "1"); + } + } catch (MessagingException e) { + // ignore it + } + } + + protected void updateHeaders() throws MessagingException { + super.updateHeaders(); + setHeadersFromFlags(this); + } + + /** + * Save any changes made to this message. + */ + public void saveChanges() throws MessagingException { + if (folder != null) + ((MboxFolder)folder).checkOpen(); + if (isExpunged()) + throw new MessageRemovedException("mbox message expunged"); + if (!writable) + throw new MessagingException("Message is read-only"); + + super.saveChanges(); + + try { + /* + * Count the size of the body, in order to set the Content-Length + * header. (Should we only do this to update an existing + * Content-Length header?) + * XXX - We could cache the content bytes here, for use later + * in writeTo. + */ + ContentLengthCounter cos = new ContentLengthCounter(); + OutputStream os = new NewlineOutputStream(cos); + super.writeTo(os); + os.flush(); + setHeader("Content-Length", String.valueOf(cos.getSize())); + // setContentSize((int)cos.getSize()); + } catch (MessagingException e) { + throw e; + } catch (Exception e) { + throw new MessagingException("unexpected exception " + e); + } + } + + /** + * Expose modified flag to MboxFolder. + */ + boolean isModified() { + return modified; + } + + /** + * Put out a byte stream suitable for saving to a file. + * XXX - ultimately implement "ignore headers" here? + */ + public void writeToFile(OutputStream os) throws IOException { + try { + if (getHeader("Content-Length") == null) { + /* + * Count the size of the body, in order to set the + * Content-Length header. + */ + ContentLengthCounter cos = new ContentLengthCounter(); + OutputStream oos = new NewlineOutputStream(cos); + super.writeTo(oos, null); + oos.flush(); + setHeader("Content-Length", String.valueOf(cos.getSize())); + // setContentSize((int)cos.getSize()); + } + + os = new NewlineOutputStream(os, true); + PrintStream pos = new PrintStream(os, false, "iso-8859-1"); + + pos.println(getUnixFromLine()); + super.writeTo(pos, null); + pos.flush(); + } catch (MessagingException e) { + throw new IOException("unexpected exception " + e); + } + } + + public void writeTo(OutputStream os, String[] ignoreList) + throws IOException, MessagingException { + // set the SEEN flag now, which will normally be set by + // getContentStream, so it will show up in our headers + if (!isSet(Flags.Flag.SEEN)) + setFlag(Flags.Flag.SEEN, true); + super.writeTo(os, ignoreList); + } + + /** + * Interpose on superclass method to make sure folder is still open + * and message hasn't been expunged. + */ + public String[] getHeader(String name) + throws MessagingException { + if (folder != null) + ((MboxFolder)folder).checkOpen(); + if (isExpunged()) + throw new MessageRemovedException("mbox message expunged"); + return super.getHeader(name); + } + + /** + * Interpose on superclass method to make sure folder is still open + * and message hasn't been expunged. + */ + public String getHeader(String name, String delimiter) + throws MessagingException { + if (folder != null) + ((MboxFolder)folder).checkOpen(); + if (isExpunged()) + throw new MessageRemovedException("mbox message expunged"); + return super.getHeader(name, delimiter); + } +}
diff --git a/mbox/src/main/java/com/sun/mail/mbox/MboxProvider.java b/mbox/src/main/java/com/sun/mail/mbox/MboxProvider.java new file mode 100644 index 0000000..d0b056d --- /dev/null +++ b/mbox/src/main/java/com/sun/mail/mbox/MboxProvider.java
@@ -0,0 +1,29 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.mbox; + +import javax.mail.Provider; + +/** + * The Mbox protocol provider. + */ +public class MboxProvider extends Provider { + public MboxProvider() { + super(Provider.Type.STORE, "mbox", MboxStore.class.getName(), + "Oracle", null); + } +}
diff --git a/mbox/src/main/java/com/sun/mail/mbox/MboxStore.java b/mbox/src/main/java/com/sun/mail/mbox/MboxStore.java new file mode 100644 index 0000000..1ce39bc --- /dev/null +++ b/mbox/src/main/java/com/sun/mail/mbox/MboxStore.java
@@ -0,0 +1,113 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.mbox; + +import java.io.*; +import javax.mail.*; + +public class MboxStore extends Store { + + String user; + String home; + Mailbox mb; + static Flags permFlags; + + static { + // we support all flags + permFlags = new Flags(); + permFlags.add(Flags.Flag.SEEN); + permFlags.add(Flags.Flag.RECENT); + permFlags.add(Flags.Flag.DELETED); + permFlags.add(Flags.Flag.FLAGGED); + permFlags.add(Flags.Flag.ANSWERED); + permFlags.add(Flags.Flag.DRAFT); + permFlags.add(Flags.Flag.USER); + } + + public MboxStore(Session session, URLName url) { + super(session, url); + + // XXX - handle security exception + user = System.getProperty("user.name"); + home = System.getProperty("user.home"); + String os = System.getProperty("os.name"); + try { + String cl = "com.sun.mail.mbox." + os + "Mailbox"; + mb = (Mailbox)Class.forName(cl). + getDeclaredConstructor().newInstance(); + } catch (Exception e) { + mb = new DefaultMailbox(); + } + } + + /** + * Since we do not have any authentication + * to do and we do not want a dialog put up asking the user for a + * password we always succeed in connecting. + * But if we're given a password, that means the user is + * doing something wrong so fail the request. + */ + protected boolean protocolConnect(String host, int port, String user, + String passwd) throws MessagingException { + + if (passwd != null) + throw new AuthenticationFailedException( + "mbox does not allow passwords"); + // XXX - should we use the user? + return true; + } + + protected void setURLName(URLName url) { + // host, user, password, and file don't matter so we strip them out + if (url != null && (url.getUsername() != null || + url.getHost() != null || + url.getFile() != null)) + url = new URLName(url.getProtocol(), null, -1, null, null, null); + super.setURLName(url); + } + + + public Folder getDefaultFolder() throws MessagingException { + checkConnected(); + + return new MboxFolder(this, null); + } + + public Folder getFolder(String name) throws MessagingException { + checkConnected(); + + return new MboxFolder(this, name); + } + + public Folder getFolder(URLName url) throws MessagingException { + checkConnected(); + return getFolder(url.getFile()); + } + + private void checkConnected() throws MessagingException { + if (!isConnected()) + throw new MessagingException("Not connected"); + } + + MailFile getMailFile(String folder) { + return mb.getMailFile(user, folder); + } + + Session getSession() { + return session; + } +}
diff --git a/mbox/src/main/java/com/sun/mail/mbox/MessageLoader.java b/mbox/src/main/java/com/sun/mail/mbox/MessageLoader.java new file mode 100644 index 0000000..b5ecaaa --- /dev/null +++ b/mbox/src/main/java/com/sun/mail/mbox/MessageLoader.java
@@ -0,0 +1,285 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.mbox; + +import java.io.*; +import java.util.*; + +/** + * A support class that contains the state and logic needed when + * loading messages from a folder. + */ +final class MessageLoader { + private final TempFile temp; + private FileInputStream fis = null; + private OutputStream fos = null; + private int pos, len; // position in and length of buffer + private long off; // current offset in temp file + private long prevend; // the end of the previous message in temp file + private MboxFolder.MessageMetadata md; + private byte[] buf = null; + // the length of the longest header we'll need to look at + private static final int LINELEN = "Content-Length: XXXXXXXXXX".length(); + private char[] line; + + public MessageLoader(TempFile temp) { + this.temp = temp; + } + + /** + * Load messages from the given file descriptor, starting at the + * specified offset, adding the MessageMetadata to the list. <p> + * + * The data is assumed to be in UNIX mbox format, with newlines + * only as the line terminators. + */ + public int load(FileDescriptor fd, long offset, + List<MboxFolder.MessageMetadata> msgs) + throws IOException { + // XXX - could allocate and deallocate buffers here + int loaded = 0; + try { + fis = new FileInputStream(fd); + if (fis.skip(offset) != offset) + throw new EOFException("Failed to skip to offset " + offset); + this.off = prevend = temp.length(); + pos = len = 0; + line = new char[LINELEN]; + buf = new byte[64 * 1024]; + fos = temp.getAppendStream(); + int n; + // keep loading messages as long as we have headers + while ((n = skipHeader(loaded == 0)) >= 0) { + long start; + if (n == 0) { + // didn't find a Content-Length, skip the body + start = skipBody(); + if (start < 0) { + // md.end = -1; + md.dataend = -1; + msgs.add(md); + loaded++; + break; + } + md.dataend = start; + } else { + // skip over the body + skip(n); + md.dataend = off; + int b; + // skip any blank lines after the body + while ((b = get()) >= 0) { + if (b != '\n') + break; + } + start = off; + if (b >= 0) + start--; // back up one byte if not at EOF + } + // md.end = start; + prevend = start; + msgs.add(md); + loaded++; + } + } finally { + try { + fis.close(); + } catch (IOException ex) { + // ignore + } + try { + fos.close(); + } catch (IOException ex) { + // ignore + } + line = null; + buf = null; + } + return loaded; + } + + /** + * Skip over the message header, returning the content length + * of the body, or 0 if no Content-Length header was seen. + * Update the MessageMetadata based on the headers seen. + * return -1 on EOF. + */ + private int skipHeader(boolean first) throws IOException { + int clen = 0; + boolean bol = true; + int lpos = -1; + int b; + boolean saw_unix_from = false; + int lineno = 0; + md = new MboxFolder.MessageMetadata(); + md.start = prevend; + md.recent = true; + while ((b = get()) >= 0) { + if (bol) { + if (b == '\n') + break; + lpos = 0; + } + if (b == '\n') { + bol = true; + lineno++; + // newline at end of line, was the line one of the headers + // we're looking for? + if (lpos > 7) { + // XXX - make this more efficient? + String s = new String(line, 0, lpos); + // fast check for Content-Length header + if (lineno == 1 && line[0] == 'F' && isPrefix(s, "From ")) { + saw_unix_from = true; + } else if (line[7] == '-' && + isPrefix(s, "Content-Length:")) { + s = s.substring(15).trim(); + try { + clen = Integer.parseInt(s); + } catch (NumberFormatException ex) { + // ignore it + } + // fast check for Status header + } else if ((line[1] == 't' || line[1] == 'T') && + isPrefix(s, "Status:")) { + if (s.indexOf('O') >= 0) + md.recent = false; + // fast check for X-Status header + } else if ((line[3] == 't' || line[3] == 'T') && + isPrefix(s, "X-Status:")) { + if (s.indexOf('D') >= 0) + md.deleted = true; + // fast check for X-Dt-Delete-Time header + } else if (line[4] == '-' && + isPrefix(s, "X-Dt-Delete-Time:")) { + md.deleted = true; + // fast check for X-IMAP header + } else if (line[5] == 'P' && s.startsWith("X-IMAP:")) { + md.imap = true; + } + } + } else { + // accumlate data in line buffer + bol = false; + if (lpos < 0) // ignoring this line + continue; + if (lpos == 0 && (b == ' ' || b == '\t')) + lpos = -1; // ignore continuation lines + else if (lpos < line.length) + line[lpos++] = (char)b; + } + } + + // if we hit EOF, or this is the first message we're loading and + // it doesn't have a UNIX From line, return EOF. + // (After the first message, UNIX From lines are seen by skipBody + // to terminate the message.) + if (b < 0 || (first && !saw_unix_from)) + return -1; + else + return clen; + } + + /** + * Does "s" start with "pre", ignoring case? + */ + private static final boolean isPrefix(String s, String pre) { + return s.regionMatches(true, 0, pre, 0, pre.length()); + } + + /** + * Skip over the body of the message looking for a line that starts + * with "From ". If found, return the offset of the beginning of + * that line. Return -1 on EOF. + */ + private long skipBody() throws IOException { + boolean bol = true; + int lpos = -1; + long loff = off; + int b; + while ((b = get()) >= 0) { + if (bol) { + lpos = 0; + loff = off - 1; + } + if (b == '\n') { + bol = true; + if (lpos >= 5) { // have enough data to test? + if (line[0] == 'F' && line[1] == 'r' && line[2] == 'o' && + line[3] == 'm' && line[4] == ' ') + return loff; + } + } else { + bol = false; + if (lpos < 0) + continue; + if (lpos == 0 && b != 'F') + lpos = -1; // ignore lines that don't start with F + else if (lpos < 5) // only need first 5 chars to test + line[lpos++] = (char)b; + } + } + return -1; + } + + /** + * Skip "n" bytes, returning how much we were able to skip. + */ + private final int skip(int n) throws IOException { + int n0 = n; + if (pos + n < len) { + pos += n; // can do it all within this buffer + off += n; + } else { + do { + n -= (len - pos); // skip rest of this buffer + off += (len - pos); + fill(); + if (len <= 0) // ran out of data + return n0 - n; + } while (n > len); + pos += n; + off += n; + } + return n0; + } + + /** + * Return the next byte. + */ + private final int get() throws IOException { + if (pos >= len) + fill(); + if (pos >= len) + return -1; + else { + off++; + return buf[pos++] & 0xff; + } + } + + /** + * Fill our buffer with more data. + * Every buffer we read is also written to the temp file. + */ + private final void fill() throws IOException { + len = fis.read(buf); + pos = 0; + if (len > 0) + fos.write(buf, 0, len); + } +}
diff --git a/mbox/src/main/java/com/sun/mail/mbox/NewlineOutputStream.java b/mbox/src/main/java/com/sun/mail/mbox/NewlineOutputStream.java new file mode 100644 index 0000000..f4a31cb --- /dev/null +++ b/mbox/src/main/java/com/sun/mail/mbox/NewlineOutputStream.java
@@ -0,0 +1,94 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.mbox; + +import java.io.*; +import java.nio.charset.StandardCharsets; + +/** + * Convert the various newline conventions to the local platform's + * newline convention. Optionally, make sure the output ends with + * a blank line. + */ +public class NewlineOutputStream extends FilterOutputStream { + private int lastb = -1; + private int bol = 1; // number of times in a row we're at beginning of line + private final boolean endWithBlankLine; + private static final byte[] newline; + + static { + String s = null; + try { + s = System.lineSeparator(); + } catch (SecurityException sex) { + // ignore, should never happen + } + if (s == null || s.length() <= 0) + s = "\n"; + newline = s.getBytes(StandardCharsets.ISO_8859_1); + } + + public NewlineOutputStream(OutputStream os) { + this(os, false); + } + + public NewlineOutputStream(OutputStream os, boolean endWithBlankLine) { + super(os); + this.endWithBlankLine = endWithBlankLine; + } + + public void write(int b) throws IOException { + if (b == '\r') { + out.write(newline); + bol++; + } else if (b == '\n') { + if (lastb != '\r') { + out.write(newline); + bol++; + } + } else { + out.write(b); + bol = 0; // no longer at beginning of line + } + lastb = b; + } + + public void write(byte b[]) throws IOException { + write(b, 0, b.length); + } + + public void write(byte b[], int off, int len) throws IOException { + for (int i = 0 ; i < len ; i++) { + write(b[off + i]); + } + } + + public void flush() throws IOException { + if (endWithBlankLine) { + if (bol == 0) { + // not at bol, return to bol and add a blank line + out.write(newline); + out.write(newline); + } else if (bol == 1) { + // at bol, add a blank line + out.write(newline); + } + } + bol = 2; + out.flush(); + } +}
diff --git a/mbox/src/main/java/com/sun/mail/mbox/SolarisMailbox.java b/mbox/src/main/java/com/sun/mail/mbox/SolarisMailbox.java new file mode 100644 index 0000000..84f3abc --- /dev/null +++ b/mbox/src/main/java/com/sun/mail/mbox/SolarisMailbox.java
@@ -0,0 +1,81 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.mbox; + +import java.io.File; + +public class SolarisMailbox extends Mailbox { + private final String home; + private final String user; + + private static final boolean homeRelative = + Boolean.getBoolean("mail.mbox.homerelative"); + + public SolarisMailbox() { + String h = System.getenv("HOME"); + if (h == null) + h = System.getProperty("user.home"); + home = h; + user = System.getProperty("user.name"); + } + + public MailFile getMailFile(String user, String folder) { + if (folder.equalsIgnoreCase("INBOX")) + return new UNIXInbox(user, filename(user, folder)); + else + return new UNIXFolder(filename(user, folder)); + } + + /** + * Given a name of a mailbox folder, expand it to a full path name. + */ + public String filename(String user, String folder) { + try { + switch (folder.charAt(0)) { + case '/': + return folder; + case '~': + int i = folder.indexOf(File.separatorChar); + String tail = ""; + if (i > 0) { + tail = folder.substring(i); + folder = folder.substring(0, i); + } + if (folder.length() == 1) + return home + tail; + else + return "/home/" + folder.substring(1) + tail; // XXX + default: + if (folder.equalsIgnoreCase("INBOX")) { + if (user == null) // XXX - should never happen + user = this.user; + String inbox = System.getenv("MAIL"); + if (inbox == null) + inbox = "/var/mail/" + user; + return inbox; + } else { + if (homeRelative) + return home + File.separator + folder; + else + return folder; + } + } + } catch (StringIndexOutOfBoundsException e) { + return folder; + } + } +}
diff --git a/mbox/src/main/java/com/sun/mail/mbox/SunOSMailbox.java b/mbox/src/main/java/com/sun/mail/mbox/SunOSMailbox.java new file mode 100644 index 0000000..55230d1 --- /dev/null +++ b/mbox/src/main/java/com/sun/mail/mbox/SunOSMailbox.java
@@ -0,0 +1,20 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.mbox; + +public class SunOSMailbox extends SolarisMailbox { +}
diff --git a/mbox/src/main/java/com/sun/mail/mbox/SunV3BodyPart.java b/mbox/src/main/java/com/sun/mail/mbox/SunV3BodyPart.java new file mode 100644 index 0000000..189ff85 --- /dev/null +++ b/mbox/src/main/java/com/sun/mail/mbox/SunV3BodyPart.java
@@ -0,0 +1,314 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.mbox; + +import javax.mail.*; +import javax.mail.internet.*; +import javax.activation.*; +import java.io.*; + +/** + * This class represents a SunV3 BodyPart. + * + * @author Bill Shannon + * @see javax.mail.Part + * @see javax.mail.internet.MimePart + * @see javax.mail.internet.MimeBodyPart + */ + +public class SunV3BodyPart extends MimeBodyPart { + /** + * Constructs a SunV3BodyPart using the given header and + * content bytes. <p> + * + * Used by providers. + * + * @param headers The header of this part + * @param content bytes representing the body of this part. + */ + public SunV3BodyPart(InternetHeaders headers, byte[] content) + throws MessagingException { + super(headers, content); + } + + /** + * Return the size of the content of this BodyPart in bytes. + * Return -1 if the size cannot be determined. <p> + * + * Note that this number may not be an exact measure of the + * content size and may or may not account for any transfer + * encoding of the content. <p> + * + * @return size in bytes + */ + public int getSize() throws MessagingException { + String s = getHeader("X-Sun-Content-Length", null); + try { + return Integer.parseInt(s); + } catch (NumberFormatException ex) { + return -1; + } + } + + /** + * Return the number of lines for the content of this Part. + * Return -1 if this number cannot be determined. <p> + * + * Note that this number may not be an exact measure of the + * content length and may or may not account for any transfer + * encoding of the content. + */ + public int getLineCount() throws MessagingException { + String s = getHeader("X-Sun-Content-Lines", null); + try { + return Integer.parseInt(s); + } catch (NumberFormatException ex) { + return -1; + } + } + + /* + * This is just enough to get us going. + * + * For more complete transformation from V3 to MIME, refer to + * sun_att.c from the Sun IMAP server code. + */ + static class MimeV3Map { + String mime; + String v3; + + MimeV3Map(String mime, String v3) { + this.mime = mime; + this.v3 = v3; + } + + private static MimeV3Map[] mimeV3Table = new MimeV3Map[] { + new MimeV3Map("text/plain", "text"), + new MimeV3Map("text/plain", "default"), + new MimeV3Map("multipart/x-sun-attachment", "X-sun-attachment"), + new MimeV3Map("application/postscript", "postscript-file"), + new MimeV3Map("image/gif", "gif-file") + // audio-file + // cshell-script + }; + + // V3 Content-Type to MIME Content-Type + static String toMime(String s) { + for (int i = 0; i < mimeV3Table.length; i++) { + if (mimeV3Table[i].v3.equalsIgnoreCase(s)) + return mimeV3Table[i].mime; + } + return "application/x-" + s; + } + + // MIME Content-Type to V3 Content-Type + static String toV3(String s) { + for (int i = 0; i < mimeV3Table.length; i++) { + if (mimeV3Table[i].mime.equalsIgnoreCase(s)) + return mimeV3Table[i].v3; + } + return s; + } + } + + /** + * Returns the value of the RFC822 "Content-Type" header field. + * This represents the content-type of the content of this + * BodyPart. This value must not be null. If this field is + * unavailable, "text/plain" should be returned. <p> + * + * This implementation uses <code>getHeader(name)</code> + * to obtain the requisite header field. + * + * @return Content-Type of this BodyPart + */ + public String getContentType() throws MessagingException { + String ct = getHeader("Content-Type", null); + if (ct == null) + ct = getHeader("X-Sun-Data-Type", null); + if (ct == null) + ct = "text/plain"; + else if (ct.indexOf('/') < 0) + ct = MimeV3Map.toMime(ct); + return ct; + } + + /** + * Returns the value of the "Content-Transfer-Encoding" header + * field. Returns <code>null</code> if the header is unavailable + * or its value is absent. <p> + * + * This implementation uses <code>getHeader(name)</code> + * to obtain the requisite header field. + * + * @see #headers + */ + public String getEncoding() throws MessagingException { + String enc = super.getEncoding(); + if (enc == null) + enc = getHeader("X-Sun-Encoding-Info", null); + return enc; + } + + /** + * Returns the "Content-Description" header field of this BodyPart. + * This typically associates some descriptive information with + * this part. Returns null if this field is unavailable or its + * value is absent. <p> + * + * If the Content-Description field is encoded as per RFC 2047, + * it is decoded and converted into Unicode. If the decoding or + * conversion fails, the raw data is returned as-is <p> + * + * This implementation uses <code>getHeader(name)</code> + * to obtain the requisite header field. + * + * @return content-description + */ + public String getDescription() throws MessagingException { + String desc = super.getDescription(); + if (desc == null) + desc = getHeader("X-Sun-Data-Description", null); + return desc; + } + + /** + * Set the "Content-Description" header field for this BodyPart. + * If the description parameter is <code>null</code>, then any + * existing "Content-Description" fields are removed. <p> + * + * If the description contains non US-ASCII characters, it will + * be encoded using the platform's default charset. If the + * description contains only US-ASCII characters, no encoding + * is done and it is used as-is. + * + * @param description content-description + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this BodyPart is + * obtained from a READ_ONLY folder. + */ + public void setDescription(String description) throws MessagingException { + throw new MethodNotSupportedException("SunV3BodyPart not writable"); + } + + /** + * Set the "Content-Description" header field for this BodyPart. + * If the description parameter is <code>null</code>, then any + * existing "Content-Description" fields are removed. <p> + * + * If the description contains non US-ASCII characters, it will + * be encoded using the specified charset. If the description + * contains only US-ASCII characters, no encoding is done and + * it is used as-is + * + * @param description Description + * @param charset Charset for encoding + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this BodyPart is + * obtained from a READ_ONLY folder. + */ + public void setDescription(String description, String charset) + throws MessagingException { + throw new MethodNotSupportedException("SunV3BodyPart not writable"); + } + + /** + * Get the filename associated with this BodyPart. <p> + * + * Returns the value of the "filename" parameter from the + * "Content-Disposition" header field of this BodyPart. If its + * not available, returns the value of the "name" parameter from + * the "Content-Type" header field of this BodyPart. + * Returns <code>null</code> if both are absent. + * + * @return filename + */ + public String getFileName() throws MessagingException { + String name = super.getFileName(); + if (name == null) + name = getHeader("X-Sun-Data-Name", null); + return name; + } + + /** + * Set the filename associated with this BodyPart, if possible. <p> + * + * Sets the "filename" parameter of the "Content-Disposition" + * header field of this BodyPart. + * + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this BodyPart is + * obtained from a READ_ONLY folder. + */ + public void setFileName(String filename) throws MessagingException { + throw new MethodNotSupportedException("SunV3BodyPart not writable"); + } + + /** + * This method provides the mechanism to set this BodyPart's content. + * The given DataHandler object should wrap the actual content. + * + * @param dh The DataHandler for the content + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this BodyPart is + * obtained from a READ_ONLY folder. + */ + public void setDataHandler(DataHandler dh) + throws MessagingException { + throw new MethodNotSupportedException("SunV3BodyPart not writable"); + } + + /** + * Output the BodyPart as a RFC822 format stream. + * + * @exception MessagingException + * @exception IOException if an error occurs writing to the + * stream or if an error is generated + * by the javax.activation layer. + * @see javax.activation.DataHandler#writeTo() + */ + public void writeTo(OutputStream os) + throws IOException, MessagingException { + throw new MethodNotSupportedException("SunV3BodyPart writeTo"); + } + + /** + * This is the method that has the 'smarts' to query the 'content' + * and update the appropriate headers. Typical headers that get + * set here are: Content-Type, Content-Encoding, boundary (for + * multipart). Now, the tricky part here is when to actually + * activate this method: + * + * - A Message being crafted by a mail-application will certainly + * need to activate this method at some point to fill up its internal + * headers. Typically this is triggered off by our writeTo() method. + * + * - A message read-in from a MessageStore will have obtained + * all its headers from the store, and so does'nt need this. + * However, if this message is editable and if any edits have + * been made to either the content or message-structure, we might + * need to resync our headers. Typically this is triggered off by + * the Message.saveChanges() methods. + */ + protected void updateHeaders() throws MessagingException { + throw new MethodNotSupportedException("SunV3BodyPart updateHeaders"); + } +}
diff --git a/mbox/src/main/java/com/sun/mail/mbox/SunV3Multipart.java b/mbox/src/main/java/com/sun/mail/mbox/SunV3Multipart.java new file mode 100644 index 0000000..8a642f1 --- /dev/null +++ b/mbox/src/main/java/com/sun/mail/mbox/SunV3Multipart.java
@@ -0,0 +1,201 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.mbox; + +import javax.mail.*; +import javax.mail.internet.*; +import javax.activation.*; +import java.util.*; +import java.io.*; +import java.nio.charset.StandardCharsets; +import com.sun.mail.util.LineInputStream; + +/** + * The SunV3Multipart class is an implementation of the abstract Multipart + * class that uses SunV3 conventions for the multipart data. <p> + * + * @author Bill Shannon + */ + +public class SunV3Multipart extends MimeMultipart { + private boolean parsing; + + /** + * Constructs a SunV3Multipart object and its bodyparts from the + * given DataSource. <p> + * + * @param ds DataSource, can be a MultipartDataSource + */ + public SunV3Multipart(DataSource ds) throws MessagingException { + super(ds); + } + + /** + * Set the subtype. Throws MethodNotSupportedException. + * + * @param subtype Subtype + */ + public void setSubType(String subtype) throws MessagingException { + throw new MethodNotSupportedException( + "can't change SunV3Multipart subtype"); + } + + /** + * Get the BodyPart referred to by the given ContentID (CID). + * Throws MethodNotSupportException. + */ + public synchronized BodyPart getBodyPart(String CID) + throws MessagingException { + throw new MethodNotSupportedException( + "SunV3Multipart doesn't support Content-ID"); + } + + /** + * Update headers. Throws MethodNotSupportException. + */ + protected void updateHeaders() throws MessagingException { + throw new MethodNotSupportedException("SunV3Multipart not writable"); + } + + /** + * Iterates through all the parts and outputs each SunV3 part + * separated by a boundary. + */ + public void writeTo(OutputStream os) + throws IOException, MessagingException { + throw new MethodNotSupportedException( + "SunV3Multipart writeTo not supported"); + } + + private static final String boundary = "----------"; + + /* + * Parse the contents of this multipart message and create the + * child body parts. + */ + protected synchronized void parse() throws MessagingException { + /* + * If the data has already been parsed, or we're in the middle of + * parsing it, there's nothing to do. The latter will occur when + * we call addBodyPart, which will call parse again. We really + * want to be able to call super.super.addBodyPart. + */ + if (parsed || parsing) + return; + + InputStream in = null; + + try { + in = ds.getInputStream(); + if (!(in instanceof ByteArrayInputStream) && + !(in instanceof BufferedInputStream)) + in = new BufferedInputStream(in); + } catch (IOException ex) { + throw new MessagingException("No inputstream from datasource"); + } catch (RuntimeException ex) { + throw new MessagingException("No inputstream from datasource"); + } + + byte[] bndbytes = boundary.getBytes(StandardCharsets.ISO_8859_1); + int bl = bndbytes.length; + + String line; + parsing = true; + try { + /* + * Skip any kind of junk until we get to the first + * boundary line. + */ + LineInputStream lin = new LineInputStream(in); + while ((line = lin.readLine()) != null) { + if (line.trim().equals(boundary)) + break; + } + if (line == null) + throw new MessagingException("Missing start boundary"); + + /* + * Read and process body parts until we see the + * terminating boundary line (or EOF). + */ + for (;;) { + /* + * Collect the headers for this body part. + */ + InternetHeaders headers = new InternetHeaders(in); + + if (!in.markSupported()) + throw new MessagingException("Stream doesn't support mark"); + + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + int b; + + /* + * Read and save the content bytes in buf. + */ + while ((b = in.read()) >= 0) { + if (b == '\r' || b == '\n') { + /* + * Found the end of a line, check whether the + * next line is a boundary. + */ + int i; + in.mark(bl + 4 + 1); // "4" for possible "--\r\n" + if (b == '\r' && in.read() != '\n') { + in.reset(); + in.mark(bl + 4); + } + // read bytes, matching against the boundary + for (i = 0; i < bl; i++) + if (in.read() != bndbytes[i]) + break; + if (i == bl) { + int b2 = in.read(); + // check for end of line + if (b2 == '\n') + break; // got it! break out of the while loop + if (b2 == '\r') { + in.mark(1); + if (in.read() != '\n') + in.reset(); + break; // got it! break out of the while loop + } + } + // failed to match, reset and proceed normally + in.reset(); + } + buf.write(b); + } + + /* + * Create a SunV3BodyPart to represent this body part. + */ + SunV3BodyPart body = + new SunV3BodyPart(headers, buf.toByteArray()); + addBodyPart(body); + if (b < 0) + break; + } + } catch (IOException e) { + throw new MessagingException("IO Error"); // XXX + } finally { + parsing = false; + } + + parsed = true; + } +}
diff --git a/mbox/src/main/java/com/sun/mail/mbox/TempFile.java b/mbox/src/main/java/com/sun/mail/mbox/TempFile.java new file mode 100644 index 0000000..25f4bd8 --- /dev/null +++ b/mbox/src/main/java/com/sun/mail/mbox/TempFile.java
@@ -0,0 +1,183 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.mbox; + +import java.util.*; +import java.net.*; +import java.io.*; +import java.security.*; + +import com.sun.mail.util.PropUtil; +import javax.mail.util.SharedFileInputStream; + +/** + * A temporary file used to cache messages. + */ +class TempFile { + + private File file; // the temp file name + private WritableSharedFile sf; + + /** + * Create a temp file in the specified directory (if not null). + * The file will be deleted when the JVM exits. + */ + public TempFile(File dir) throws IOException { + file = File.createTempFile("mbox.", ".mbox", dir); + // XXX - need JDK 6 to set permissions on the file to owner-only + file.deleteOnExit(); + sf = new WritableSharedFile(file); + } + + /** + * Return a stream for appending to the temp file. + */ + public AppendStream getAppendStream() throws IOException { + return sf.getAppendStream(); + } + + /** + * Return a stream for reading from part of the file. + */ + public InputStream newStream(long start, long end) { + return sf.newStream(start, end); + } + + public long length() { + return file.length(); + } + + /** + * Close and remove this temp file. + */ + public void close() { + try { + sf.close(); + } catch (IOException ex) { + // ignore it + } + file.delete(); + } + + protected void finalize() throws Throwable { + try { + close(); + } finally { + super.finalize(); + } + } +} + +/** + * A subclass of SharedFileInputStream that also allows writing. + */ +class WritableSharedFile extends SharedFileInputStream { + private RandomAccessFile raf; + private AppendStream af; + + public WritableSharedFile(File file) throws IOException { + super(file); + try { + raf = new RandomAccessFile(file, "rw"); + } catch (IOException ex) { + // if anything goes wrong opening the writable file, + // close the readable file too + super.close(); + } + } + + /** + * Return the writable version of this file. + */ + public RandomAccessFile getWritableFile() { + return raf; + } + + /** + * Close the readable and writable files. + */ + public void close() throws IOException { + try { + super.close(); + } finally { + raf.close(); + } + } + + /** + * Update the size of the readable file after writing + * to the file. Updates the length to be the current + * size of the file. + */ + synchronized long updateLength() throws IOException { + datalen = in.length(); + af = null; + return datalen; + } + + /** + * Return a new AppendStream, but only if one isn't in active use. + */ + public synchronized AppendStream getAppendStream() throws IOException { + if (af != null) + throw new IOException( + "file cache only supports single threaded access"); + af = new AppendStream(this); + return af; + } +} + +/** + * A stream for writing to the temp file, and when done + * can return a stream for reading the data just written. + * NOTE: We assume that only one thread is writing to the + * file at a time. + */ +class AppendStream extends OutputStream { + private final WritableSharedFile tf; + private RandomAccessFile raf; + private final long start; + private long end; + + public AppendStream(WritableSharedFile tf) throws IOException { + this.tf = tf; + raf = tf.getWritableFile(); + start = raf.length(); + raf.seek(start); + } + + public void write(int b) throws IOException { + raf.write(b); + } + + public void write(byte[] b) throws IOException { + raf.write(b); + } + + public void write(byte[] b, int off, int len) throws IOException { + raf.write(b, off, len); + } + + public synchronized void close() throws IOException { + end = tf.updateLength(); + raf = null; // no more writing allowed + } + + public synchronized InputStream getInputStream() throws IOException { + return tf.newStream(start, end); + } +}
diff --git a/mbox/src/main/java/com/sun/mail/mbox/UNIXFile.java b/mbox/src/main/java/com/sun/mail/mbox/UNIXFile.java new file mode 100644 index 0000000..ab1579e --- /dev/null +++ b/mbox/src/main/java/com/sun/mail/mbox/UNIXFile.java
@@ -0,0 +1,132 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.mbox; + +import java.io.File; +import java.io.FileDescriptor; +import java.util.StringTokenizer; + +public class UNIXFile extends File { + protected static final boolean loaded; + protected static final int lockType; + + private static final long serialVersionUID = -7972156315284146651L; + + public UNIXFile(String name) { + super(name); + } + + // lock type enum + protected static final int NONE = 0; + protected static final int NATIVE = 1; + protected static final int JAVA = 2; + + static { + String lt = System.getProperty("mail.mbox.locktype", "native"); + int type = NATIVE; + if (lt.equalsIgnoreCase("none")) + type = NONE; + else if (lt.equalsIgnoreCase("java")) + type = JAVA; + lockType = type; + + boolean lloaded = false; + if (lockType == NATIVE) { + try { + System.loadLibrary("mbox"); + lloaded = true; + } catch (UnsatisfiedLinkError e) { + String classpath = System.getProperty("java.class.path"); + String sep = System.getProperty("path.separator"); + String arch = System.getProperty("os.arch"); + StringTokenizer st = new StringTokenizer(classpath, sep); + while (st.hasMoreTokens()) { + String path = st.nextToken(); + if (path.endsWith("/classes") || + path.endsWith("/mail.jar") || + path.endsWith("/javax.mail.jar")) { + int i = path.lastIndexOf('/'); + String libdir = path.substring(0, i + 1) + "lib/"; + String lib = libdir + arch + "/libmbox.so"; + try { + System.load(lib); + lloaded = true; + break; + } catch (UnsatisfiedLinkError e2) { + lib = libdir + "libmbox.so"; + try { + System.load(lib); + lloaded = true; + break; + } catch (UnsatisfiedLinkError e3) { + continue; + } + } + } + } + } + } + loaded = lloaded; + if (loaded) + initIDs(FileDescriptor.class, FileDescriptor.in); + } + + /** + * Return the access time of the file. + */ + public static long lastAccessed(File file) { + return lastAccessed0(file.getPath()); + } + + public long lastAccessed() { + return lastAccessed0(getPath()); + } + + private static native void initIDs(Class<FileDescriptor> fdClass, + FileDescriptor stdin); + + /** + * Lock the file referred to by fd. The string mode is "r" + * for a read lock or "rw" for a write lock. Don't block + * if lock can't be acquired. + */ + public static boolean lock(FileDescriptor fd, String mode) { + return lock(fd, mode, false); + } + + /** + * Lock the file referred to by fd. The string mode is "r" + * for a read lock or "rw" for a write lock. If block is set, + * block waiting for the lock if necessary. + */ + private static boolean lock(FileDescriptor fd, String mode, boolean block) { + //return loaded && lock0(fd, mode); + if (loaded) { + boolean ret; + //System.out.println("UNIXFile.lock(" + fd + ", " + mode + ")"); + ret = lock0(fd, mode, block); + //System.out.println("UNIXFile.lock returns " + ret); + return ret; + } + return false; + } + + private static native boolean lock0(FileDescriptor fd, String mode, + boolean block); + + public static native long lastAccessed0(String name); +}
diff --git a/mbox/src/main/java/com/sun/mail/mbox/UNIXFolder.java b/mbox/src/main/java/com/sun/mail/mbox/UNIXFolder.java new file mode 100644 index 0000000..13dc30a --- /dev/null +++ b/mbox/src/main/java/com/sun/mail/mbox/UNIXFolder.java
@@ -0,0 +1,74 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.mbox; + +import java.io.*; + +public class UNIXFolder extends UNIXFile implements MailFile { + protected transient RandomAccessFile file; + + private static final long serialVersionUID = -254578891263785591L; + + public UNIXFolder(String name) { + super(name); + } + + public boolean lock(String mode) { + try { + file = new RandomAccessFile(this, mode); + switch (lockType) { + case NONE: + return true; + case NATIVE: + default: + return UNIXFile.lock(file.getFD(), mode); + case JAVA: + return file.getChannel(). + tryLock(0L, Long.MAX_VALUE, !mode.equals("rw")) != null; + } + } catch (FileNotFoundException fe) { + return false; + } catch (IOException ie) { + file = null; + return false; + } + } + + public void unlock() { + if (file != null) { + try { + file.close(); + } catch (IOException e) { + // ignore it + } + file = null; + } + } + + public void touchlock() { + } + + public FileDescriptor getFD() { + if (file == null) + return null; + try { + return file.getFD(); + } catch (IOException e) { + return null; + } + } +}
diff --git a/mbox/src/main/java/com/sun/mail/mbox/UNIXInbox.java b/mbox/src/main/java/com/sun/mail/mbox/UNIXInbox.java new file mode 100644 index 0000000..70ce865 --- /dev/null +++ b/mbox/src/main/java/com/sun/mail/mbox/UNIXInbox.java
@@ -0,0 +1,123 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.mbox; + +import java.io.*; + +public class UNIXInbox extends UNIXFolder implements InboxFile { + private final String user; + + private static final long serialVersionUID = 651261842162777620L; + + /* + * Superclass UNIXFile loads the library containing all the + * native code and sets the "loaded" flag if successful. + */ + + public UNIXInbox(String user, String name) { + super(name); + this.user = user; + if (user == null) + throw new NullPointerException("user name is null in UNIXInbox"); + } + + public boolean lock(String mode) { + if (lockType == NATIVE) { + if (!loaded) + return false; + if (!maillock(user, 5)) + return false; + } + if (!super.lock(mode)) { + if (loaded) + mailunlock(); + return false; + } + return true; + } + + public void unlock() { + super.unlock(); + if (loaded) + mailunlock(); + } + + public void touchlock() { + if (loaded) + touchlock0(); + } + + private transient RandomAccessFile lockfile; // the user's ~/.Maillock file + private transient String lockfileName; // its name + + public boolean openLock(String mode) { + if (mode.equals("r")) + return true; + if (lockfileName == null) { + String home = System.getProperty("user.home"); + lockfileName = home + File.separator + ".Maillock"; + } + try { + lockfile = new RandomAccessFile(lockfileName, mode); + boolean ret; + switch (lockType) { + case NONE: + ret = true; + break; + case NATIVE: + default: + ret = UNIXFile.lock(lockfile.getFD(), mode); + break; + case JAVA: + ret = lockfile.getChannel(). + tryLock(0L, Long.MAX_VALUE, !mode.equals("rw")) != null; + break; + } + if (!ret) + closeLock(); + return ret; + } catch (IOException ex) { + } + return false; + } + + public void closeLock() { + if (lockfile == null) + return; + try { + lockfile.close(); + } catch (IOException ex) { + } finally { + lockfile = null; + } + } + + public boolean equals(Object o) { + if (!(o instanceof UNIXInbox)) + return false; + UNIXInbox other = (UNIXInbox)o; + return user.equals(other.user) && super.equals(other); + } + + public int hashCode() { + return super.hashCode() + user.hashCode(); + } + + private native boolean maillock(String user, int retryCount); + private native void mailunlock(); + private native void touchlock0(); +}
diff --git a/mbox/src/main/java/com/sun/mail/remote/POP3RemoteProvider.java b/mbox/src/main/java/com/sun/mail/remote/POP3RemoteProvider.java new file mode 100644 index 0000000..e5e3987 --- /dev/null +++ b/mbox/src/main/java/com/sun/mail/remote/POP3RemoteProvider.java
@@ -0,0 +1,29 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.remote; + +import javax.mail.Provider; + +/** + * The POP3 remote protocol provider. + */ +public class POP3RemoteProvider extends Provider { + public POP3RemoteProvider() { + super(Provider.Type.STORE, "pop3remote", + POP3RemoteStore.class.getName(), "Oracle", null); + } +}
diff --git a/mbox/src/main/java/com/sun/mail/remote/POP3RemoteStore.java b/mbox/src/main/java/com/sun/mail/remote/POP3RemoteStore.java new file mode 100644 index 0000000..fa6b1d4 --- /dev/null +++ b/mbox/src/main/java/com/sun/mail/remote/POP3RemoteStore.java
@@ -0,0 +1,36 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.remote; + +import javax.mail.*; +import com.sun.mail.pop3.POP3Store; + +/** + * A local store that uses POP3 to populate the INBOX. + * + * @author Bill Shannon + */ +public class POP3RemoteStore extends RemoteStore { + + public POP3RemoteStore(Session session, URLName url) { + super(session, url); + } + + protected Store getRemoteStore(Session session, URLName url) { + return new POP3Store(session, url); + } +}
diff --git a/mbox/src/main/java/com/sun/mail/remote/RemoteDefaultFolder.java b/mbox/src/main/java/com/sun/mail/remote/RemoteDefaultFolder.java new file mode 100644 index 0000000..81c0f90 --- /dev/null +++ b/mbox/src/main/java/com/sun/mail/remote/RemoteDefaultFolder.java
@@ -0,0 +1,50 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.remote; + +import javax.mail.*; +import com.sun.mail.mbox.*; + +/** + * The default folder for the "remote" protocol. + * + * @author Bill Shannon + */ +public class RemoteDefaultFolder extends MboxFolder { + + protected RemoteDefaultFolder(RemoteStore store, String name) { + super(store, name); + } + + /** + * Depending on the name of the requested folder, create an + * appropriate <code>Folder</code> subclass. If the name is + * <code>null</code>, create a <code>RemoteDefaultFolder</code>. + * If the name is "INBOX" (ignoring case), create a + * <code>RemoteInbox</code>. Otherwise, create an <code>MboxFolder</code>. + * + * @return the new <code>Folder</code> + */ + protected Folder createFolder(Store store, String name) { + if (name == null) + return new RemoteDefaultFolder((RemoteStore)store, null); + else if (name.equalsIgnoreCase("INBOX")) + return new RemoteInbox((RemoteStore)store, name); + else + return new MboxFolder((MboxStore)store, name); + } +}
diff --git a/mbox/src/main/java/com/sun/mail/remote/RemoteInbox.java b/mbox/src/main/java/com/sun/mail/remote/RemoteInbox.java new file mode 100644 index 0000000..5bfd232 --- /dev/null +++ b/mbox/src/main/java/com/sun/mail/remote/RemoteInbox.java
@@ -0,0 +1,68 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.remote; + +import javax.mail.*; +import com.sun.mail.mbox.*; + +/** + * A remote Inbox folder. The data is actually managed by our subclass + * (<code>MboxFolder</code>). We fetch data from the remote Inbox and + * add it to the local Inbox. + * + * @author Bill Shannon + */ + +public class RemoteInbox extends MboxFolder { + + private RemoteStore mstore; + + protected RemoteInbox(RemoteStore store, String name) { + super(store, name); + this.mstore = store; + } + + /** + * Poll the remote store for any new messages. + */ + public synchronized boolean hasNewMessages() { + try { + mstore.updateInbox(); + } catch (MessagingException ex) { + // ignore it + } + return super.hasNewMessages(); + } + + /** + * Open the folder in the specified mode. + * Poll the remote store for any new messages first. + */ + public synchronized void open(int mode) throws MessagingException { + mstore.updateInbox(); + super.open(mode); + } + + /** + * Return the number of messages in this folder. + * Poll the remote store for any new messages first. + */ + public synchronized int getMessageCount() throws MessagingException { + mstore.updateInbox(); + return super.getMessageCount(); + } +}
diff --git a/mbox/src/main/java/com/sun/mail/remote/RemoteStore.java b/mbox/src/main/java/com/sun/mail/remote/RemoteStore.java new file mode 100644 index 0000000..faf9dd4 --- /dev/null +++ b/mbox/src/main/java/com/sun/mail/remote/RemoteStore.java
@@ -0,0 +1,127 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.remote; + +import java.io.*; +import javax.mail.*; +import com.sun.mail.mbox.*; + +/** + * A wrapper around a local <code>MboxStore</code> that fetches data + * from the Inbox in a remote store and adds it to our local Inbox. + */ +public abstract class RemoteStore extends MboxStore { + + protected Store remoteStore; + protected Folder remoteInbox; + protected Folder inbox; + protected String host, user, password; + protected int port; + protected long lastUpdate = 0; + + public RemoteStore(Session session, URLName url) { + super(session, url); + remoteStore = getRemoteStore(session, url); + } + + /** + * Subclasses override this method to return the appropriate + * <code>Store</code> object. This method will be called by + * the <code>RemoteStore</code> constructor. + */ + protected abstract Store getRemoteStore(Session session, URLName url); + + /** + * Connect to the store. + */ + public void connect(String host, int port, String user, String password) + throws MessagingException { + this.host = host; + this.port = port; + this.user = user; + this.password = password; + updateInbox(); + } + + /** + * Fetch any new mail in the remote INBOX and add it to the local INBOX. + */ + protected void updateInbox() throws MessagingException { + // is it time to do an update yet? + // XXX - polling frequency, rules, etc. should be in properties + if (System.currentTimeMillis() < lastUpdate + (5 * 1000)) + return; + try { + /* + * Connect to the remote store, using the saved + * authentication information. + */ + remoteStore.connect(host, port, user, password); + + /* + * If this store isn't connected yet, do it now, because + * it needs to be connected to get the INBOX folder. + */ + if (!isConnected()) + super.connect(host, port, user, password); + if (remoteInbox == null) + remoteInbox = remoteStore.getFolder("INBOX"); + if (inbox == null) + inbox = getFolder("INBOX"); + remoteInbox.open(Folder.READ_WRITE); + Message[] msgs = remoteInbox.getMessages(); + inbox.appendMessages(msgs); + remoteInbox.setFlags(msgs, new Flags(Flags.Flag.DELETED), true); + remoteInbox.close(true); + remoteStore.close(); + } catch (MessagingException ex) { + try { + if (remoteInbox != null && remoteInbox.isOpen()) + remoteInbox.close(false); + } finally { + if (remoteStore != null && remoteStore.isConnected()) + remoteStore.close(); + } + throw ex; + } + } + + public Folder getDefaultFolder() throws MessagingException { + checkConnected(); + + return new RemoteDefaultFolder(this, null); + } + + public Folder getFolder(String name) throws MessagingException { + checkConnected(); + + if (name.equalsIgnoreCase("INBOX")) + return new RemoteInbox(this, name); + else + return super.getFolder(name); + } + + public Folder getFolder(URLName url) throws MessagingException { + checkConnected(); + return getFolder(url.getFile()); + } + + private void checkConnected() throws MessagingException { + if (!isConnected()) + throw new MessagingException("Not connected"); + } +}
diff --git a/mbox/src/main/resources/META-INF/MANIFEST.MF b/mbox/src/main/resources/META-INF/MANIFEST.MF new file mode 100644 index 0000000..820d8d2 --- /dev/null +++ b/mbox/src/main/resources/META-INF/MANIFEST.MF
@@ -0,0 +1,9 @@ +Manifest-Version: 1.0 +Extension-Name: com.sun.mail.mbox +Specification-Title: com.sun.mail.mbox +Specification-Version: ${mail.spec.version} +Specification-Vendor: ${project.organization.name} +Implementation-Title: com.sun.mail.mbox +Implementation-Version: ${mail.version} +Implementation-Vendor: ${project.organization.name} +Implementation-Vendor-Id: com.sun
diff --git a/mbox/src/main/resources/META-INF/javamail.providers b/mbox/src/main/resources/META-INF/javamail.providers new file mode 100644 index 0000000..c2273dd --- /dev/null +++ b/mbox/src/main/resources/META-INF/javamail.providers
@@ -0,0 +1,2 @@ +protocol=mbox; type=store; class=com.sun.mail.mbox.MboxStore; vendor=Oracle; +protocol=pop3remote; type=store; class=com.sun.mail.remote.POP3RemoteStore; vendor=Oracle;
diff --git a/mbox/src/main/resources/META-INF/services/javax.mail.Provider b/mbox/src/main/resources/META-INF/services/javax.mail.Provider new file mode 100644 index 0000000..663358d --- /dev/null +++ b/mbox/src/main/resources/META-INF/services/javax.mail.Provider
@@ -0,0 +1,2 @@ +com.sun.mail.mbox.MboxProvider +com.sun.mail.remote.POP3RemoteProvider
diff --git a/mbox/src/test/java/com/sun/mail/mbox/MboxFolderExpungeTest.java b/mbox/src/test/java/com/sun/mail/mbox/MboxFolderExpungeTest.java new file mode 100644 index 0000000..594e125 --- /dev/null +++ b/mbox/src/test/java/com/sun/mail/mbox/MboxFolderExpungeTest.java
@@ -0,0 +1,362 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.mbox; + +import java.io.File; +import java.io.IOException; +import java.util.Properties; +import java.util.Date; + +import javax.mail.Session; +import javax.mail.Store; +import javax.mail.Folder; +import javax.mail.Message; +import javax.mail.Flags; +import javax.mail.MessagingException; +import javax.mail.internet.MimeMessage; + +import org.junit.Test; +import org.junit.BeforeClass; +import org.junit.AfterClass; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +/** + * Test expunge of mbox folders. + */ +public final class MboxFolderExpungeTest { + + @BeforeClass + public static void before() { + System.setProperty("mail.mbox.locktype", "none"); + } + + @AfterClass + public static void after() { + System.getProperties().remove("mail.mbox.locktype"); + } + + @Test + public void testRemoveFirst() throws Exception { + Folder f = createTestFolder(); + try { + f.open(Folder.READ_WRITE); + Message m = f.getMessage(1); + m.setFlag(Flags.Flag.DELETED, true); + f.expunge(); + m = f.getMessage(1); + assertEquals("2", ((String)m.getContent()).trim()); + m.setFlag(Flags.Flag.DELETED, true); + m = f.getMessage(2); + assertEquals("3", ((String)m.getContent()).trim()); + f.expunge(); + m = f.getMessage(1); + assertEquals("3", ((String)m.getContent()).trim()); + } catch (Exception ex) { + System.out.println(ex); + //ex.printStackTrace(); + fail(ex.toString()); + } finally { + f.close(false); + f.delete(false); + f.getStore().close(); + } + } + + @Test + public void testRemoveMiddle() throws Exception { + Folder f = createTestFolder(); + try { + f.open(Folder.READ_WRITE); + Message m = f.getMessage(2); + m.setFlag(Flags.Flag.DELETED, true); + f.expunge(); + m = f.getMessage(1); + assertEquals("1", ((String)m.getContent()).trim()); + m = f.getMessage(2); + assertEquals("3", ((String)m.getContent()).trim()); + } catch (Exception ex) { + System.out.println(ex); + //ex.printStackTrace(); + fail(ex.toString()); + } finally { + f.close(false); + f.delete(false); + f.getStore().close(); + } + } + + @Test + public void testRemoveLast() throws Exception { + Folder f = createTestFolder(); + try { + f.open(Folder.READ_WRITE); + Message m = f.getMessage(3); + m.setFlag(Flags.Flag.DELETED, true); + f.expunge(); + m = f.getMessage(1); + assertEquals("1", ((String)m.getContent()).trim()); + m = f.getMessage(2); + assertEquals("2", ((String)m.getContent()).trim()); + } catch (Exception ex) { + System.out.println(ex); + //ex.printStackTrace(); + fail(ex.toString()); + } finally { + f.close(false); + f.delete(false); + f.getStore().close(); + } + } + + @Test + public void testRemoveFirstClose() throws Exception { + Folder f = createTestFolder(); + try { + f.open(Folder.READ_WRITE); + Message m = f.getMessage(1); + m.setFlag(Flags.Flag.DELETED, true); + f.close(true); + f.open(Folder.READ_WRITE); + m = f.getMessage(1); + assertEquals("2", ((String)m.getContent()).trim()); + m.setFlag(Flags.Flag.DELETED, true); + m = f.getMessage(2); + assertEquals("3", ((String)m.getContent()).trim()); + f.close(true); + f.open(Folder.READ_WRITE); + m = f.getMessage(1); + assertEquals("3", ((String)m.getContent()).trim()); + } catch (Exception ex) { + System.out.println(ex); + //ex.printStackTrace(); + fail(ex.toString()); + } finally { + f.close(false); + f.delete(false); + f.getStore().close(); + } + } + + @Test + public void testRemoveMiddleClose() throws Exception { + Folder f = createTestFolder(); + try { + f.open(Folder.READ_WRITE); + Message m = f.getMessage(2); + m.setFlag(Flags.Flag.DELETED, true); + f.close(true); + f.open(Folder.READ_WRITE); + m = f.getMessage(1); + assertEquals("1", ((String)m.getContent()).trim()); + m = f.getMessage(2); + assertEquals("3", ((String)m.getContent()).trim()); + } catch (Exception ex) { + System.out.println(ex); + //ex.printStackTrace(); + fail(ex.toString()); + } finally { + f.close(false); + f.delete(false); + f.getStore().close(); + } + } + + @Test + public void testRemoveLastClose() throws Exception { + Folder f = createTestFolder(); + try { + f.open(Folder.READ_WRITE); + Message m = f.getMessage(3); + m.setFlag(Flags.Flag.DELETED, true); + f.close(true); + f.open(Folder.READ_WRITE); + m = f.getMessage(1); + assertEquals("1", ((String)m.getContent()).trim()); + m = f.getMessage(2); + assertEquals("2", ((String)m.getContent()).trim()); + } catch (Exception ex) { + System.out.println(ex); + //ex.printStackTrace(); + fail(ex.toString()); + } finally { + f.close(false); + f.delete(false); + f.getStore().close(); + } + } + + @Test + public void testRemoveFirstMessages() throws Exception { + Folder f = createTestFolder(); + try { + f.open(Folder.READ_WRITE); + Message[] msgs = f.getMessages(1, 3); + msgs[0].setFlag(Flags.Flag.DELETED, true); + f.expunge(); + assertEquals("2", ((String)msgs[1].getContent()).trim()); + assertEquals("3", ((String)msgs[2].getContent()).trim()); + msgs[1].setFlag(Flags.Flag.DELETED, true); + f.expunge(); + assertEquals("3", ((String)msgs[2].getContent()).trim()); + } catch (Exception ex) { + System.out.println(ex); + //ex.printStackTrace(); + fail(ex.toString()); + } finally { + f.close(false); + f.delete(false); + f.getStore().close(); + } + } + + @Test + public void testRemoveMiddleMessages() throws Exception { + Folder f = createTestFolder(); + try { + f.open(Folder.READ_WRITE); + Message[] msgs = f.getMessages(1, 3); + msgs[1].setFlag(Flags.Flag.DELETED, true); + f.expunge(); + assertEquals("1", ((String)msgs[0].getContent()).trim()); + assertEquals("3", ((String)msgs[2].getContent()).trim()); + } catch (Exception ex) { + System.out.println(ex); + //ex.printStackTrace(); + fail(ex.toString()); + } finally { + f.close(false); + f.delete(false); + f.getStore().close(); + } + } + + @Test + public void testRemoveLastMessages() throws Exception { + Folder f = createTestFolder(); + try { + f.open(Folder.READ_WRITE); + Message[] msgs = f.getMessages(1, 3); + msgs[2].setFlag(Flags.Flag.DELETED, true); + f.expunge(); + assertEquals("1", ((String)msgs[0].getContent()).trim()); + assertEquals("2", ((String)msgs[1].getContent()).trim()); + } catch (Exception ex) { + System.out.println(ex); + //ex.printStackTrace(); + fail(ex.toString()); + } finally { + f.close(false); + f.delete(false); + f.getStore().close(); + } + } + + @Test + public void testNewMessagesAfterExpunge() throws Exception { + Folder f = createTestFolder(); + try { + f.open(Folder.READ_WRITE); + Message[] msgs = f.getMessages(1, 3); + msgs[0].setFlag(Flags.Flag.DELETED, true); + f.expunge(); + f.appendMessages(new Message[] { createMessage(null, 4) }); + assertEquals(3, f.getMessageCount()); + Message m = f.getMessage(3); + assertEquals("4", ((String)m.getContent()).trim()); + } catch (Exception ex) { + System.out.println(ex); + //ex.printStackTrace(); + fail(ex.toString()); + } finally { + f.close(false); + f.delete(false); + f.getStore().close(); + } + } + + @Test + public void testNewMessagesAfterClose() throws Exception { + Folder f = createTestFolder(); + try { + f.open(Folder.READ_WRITE); + Message[] msgs = f.getMessages(1, 3); + msgs[0].setFlag(Flags.Flag.DELETED, true); + f.close(true); + f.appendMessages(new Message[] { createMessage(null, 4) }); + f.open(Folder.READ_WRITE); + assertEquals(3, f.getMessageCount()); + Message m = f.getMessage(3); + assertEquals("4", ((String)m.getContent()).trim()); + } catch (Exception ex) { + System.out.println(ex); + //ex.printStackTrace(); + fail(ex.toString()); + } finally { + f.close(false); + f.delete(false); + f.getStore().close(); + } + } + + /** + * Create a temp file to use as a test folder and populate it + * with 3 messages. + */ + private Folder createTestFolder() { + Properties properties = new Properties(); + Session session = Session.getInstance(properties); + //session.setDebug(true); + + Folder folder = null; + try { + Store store = session.getStore("mbox"); + File temp = File.createTempFile("mbox", ".mbx"); + temp.deleteOnExit(); + store.connect(); + folder = store.getFolder(temp.getAbsolutePath()); + folder.create(Folder.HOLDS_MESSAGES); + Message[] msgs = new Message[3]; + for (int i = 0; i < 3; i++) + msgs[i] = createMessage(session, i + 1); + folder.appendMessages(msgs); + } catch (Exception ex) { + System.out.println(ex); + //ex.printStackTrace(); + fail(ex.toString()); + } + return folder; + } + + /** + * Create a test message. + */ + private Message createMessage(Session session, int msgno) + throws MessagingException { + MimeMessage msg = new MimeMessage(session); + msg.setFrom("test@example.com"); + msg.setSentDate(new Date()); + String subject = "test "; + // ensure each message is a different length + for (int i = 0; i < msgno; i++) + subject += "test "; + msg.setSubject(subject + msgno); + msg.setText(msgno + "\n"); + msg.saveChanges(); + return msg; + } +}
diff --git a/mbox/src/test/java/com/sun/mail/mbox/MboxFolderTest.java b/mbox/src/test/java/com/sun/mail/mbox/MboxFolderTest.java new file mode 100644 index 0000000..24cdc86 --- /dev/null +++ b/mbox/src/test/java/com/sun/mail/mbox/MboxFolderTest.java
@@ -0,0 +1,88 @@ +/* + * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.mbox; + +import java.io.File; +import java.io.PrintWriter; +import java.util.Properties; + +import javax.mail.Session; +import javax.mail.Store; +import javax.mail.Folder; + +import org.junit.Test; +import org.junit.BeforeClass; +import org.junit.AfterClass; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +/** + * Test of mbox folders. + */ +public final class MboxFolderTest { + + @BeforeClass + public static void before() { + System.setProperty("mail.mbox.locktype", "none"); + } + + @AfterClass + public static void after() { + System.getProperties().remove("mail.mbox.locktype"); + } + + /** + * Test that a mailbox that has garbage at the beginning + * (such as a gratuitous blank line) is handled without + * crashing and without corrupting the mailbox. + */ + @Test + public void testGarbageAtStartOfFolder() throws Exception { + Folder f = null; + try { + File temp = File.createTempFile("mbox", ".mbx"); + temp.deleteOnExit(); + PrintWriter pw = new PrintWriter(temp); + pw.println(); + pw.println("From - Tue Aug 23 11:56:51 2011"); + pw.println(); + pw.println("test"); + pw.println(); + pw.close(); + long size = temp.length(); + + Properties properties = new Properties(); + Session session = Session.getInstance(properties); + Store store = session.getStore("mbox"); + store.connect(); + f = store.getFolder(temp.getAbsolutePath()); + f.open(Folder.READ_WRITE); + assertEquals(0, f.getMessageCount()); + f.close(true); + assertEquals(size, temp.length()); + } catch (Exception ex) { + System.out.println(ex); + //ex.printStackTrace(); + fail(ex.toString()); + } finally { + if (f != null) { + f.delete(false); + f.getStore().close(); + } + } + } +}
diff --git a/outlook/pom.xml b/outlook/pom.xml new file mode 100644 index 0000000..60a1332 --- /dev/null +++ b/outlook/pom.xml
@@ -0,0 +1,74 @@ +<?xml version="1.0" encoding="iso-8859-1"?> +<!-- + + Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 + http://maven.apache.org/maven-v4_0_0.xsd"> + <parent> + <groupId>com.sun.mail</groupId> + <artifactId>all</artifactId> + <version>1.6.3</version> + </parent> + <modelVersion>4.0.0</modelVersion> + <groupId>com.sun.mail</groupId> + <artifactId>outlook</artifactId> + <packaging>jar</packaging> + <name>JavaMail API demo Outlook message support</name> + + <build> + <plugins> + <!-- + Need to disable the maven-bundle-plugin because the + new version doesn't like classes in the default package. + --> + <plugin> + <groupId>org.apache.felix</groupId> + <artifactId>maven-bundle-plugin</artifactId> + <executions> + <execution> + <id>osgi-manifest</id> + <phase>none</phase> + </execution> + </executions> + </plugin> + + <!-- + Because the maven-jar-plugin depends on the manifest file + created by the maven-bundle-plugin, we need to disable it too. + --> + <plugin> + <artifactId>maven-jar-plugin</artifactId> + <executions> + <execution> + <id>default-jar</id> + <phase>none</phase> + </execution> + </executions> + </plugin> + </plugins> + </build> + + <dependencies> + <dependency> + <groupId>com.sun.mail</groupId> + <artifactId>jakarta.mail</artifactId> + </dependency> + </dependencies> +</project>
diff --git a/outlook/src/main/java/MSBodyPart.java b/outlook/src/main/java/MSBodyPart.java new file mode 100644 index 0000000..4f64c06 --- /dev/null +++ b/outlook/src/main/java/MSBodyPart.java
@@ -0,0 +1,101 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +import java.io.*; +import javax.activation.*; +import javax.mail.*; +import javax.mail.internet.*; + +/** + * A special MimeBodyPart used with MSMessage. + * + * @author John Mani + * @author Bill Shannon + */ +public class MSBodyPart extends MimeBodyPart { + private int start; + private int end; + private String type = UNKNOWN; + private String disposition; + private String encoding; + private String filename = UNKNOWN; + + private static final String UNKNOWN = "UNKNOWN"; + + public MSBodyPart(byte[] content, int start, int end, + String disposition, String encoding) { + this.content = content; + this.start = start; + this.end = end; + this.disposition = disposition; + this.encoding = encoding; + } + + public String getContentType() throws MessagingException { + // try to figure this out from the filename extension + if (type == UNKNOWN) + processBegin(); + return type; + } + + public String getEncoding() throws MessagingException { + return encoding; + } + + public String getDisposition() throws MessagingException { + return disposition; + } + + public String getFileName() throws MessagingException { + // get filename from the "begin" line + if (filename == UNKNOWN) + processBegin(); + return filename; + } + + protected InputStream getContentStream() { + return new ByteArrayInputStream(content, start, end - start); + } + + /** + * Process the "begin" line to extract the filename, + * and from it determine the Content-Type. + */ + private void processBegin() { + InputStream in = getContentStream(); + try { + BufferedReader r = new BufferedReader(new InputStreamReader(in)); + String begin = r.readLine(); + // format is "begin 666 filename.txt" + if (begin != null && begin.regionMatches(true, 0, "begin ", 0, 6)) { + int i = begin.indexOf(' ', 6); + if (i > 0) { + filename = begin.substring(i + 1); + FileTypeMap map = FileTypeMap.getDefaultFileTypeMap(); + type = map.getContentType(filename); + if (type == null) + type = "application/octet-stream"; + } + } + } catch (IOException ex) { + // ignore + } finally { + try { + in.close(); + } catch (IOException ex) { + // ignore it + } + if (filename == UNKNOWN) + filename = null; + if (type == UNKNOWN || type == null) + type = "text/plain"; + } + } +}
diff --git a/outlook/src/main/java/MSMessage.java b/outlook/src/main/java/MSMessage.java new file mode 100644 index 0000000..6d93878 --- /dev/null +++ b/outlook/src/main/java/MSMessage.java
@@ -0,0 +1,210 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +import java.io.*; +import javax.mail.*; +import javax.mail.internet.*; +import javax.activation.*; + +/** + * This class models a UUEncoded Message sent from MS Outlook etc. <p> + * + * The message structure looks like this := + * [text body] [uuencoded attachment]* + * <p> + * i.e., an optional text/plain main-body, followed by zero or more + * UUENCODE-ed attachments. + * + * @author John Mani + * @author Bill Shannon + * @see javax.mail.internet.MimeMessage + */ + +public class MSMessage extends MimeMessage { + private String type; + + /** + * Constructor that converts a MimeMessage object into a MSMessage. + * + * @exception MessagingException if the given MimeMessage + * is not a non-MIME MS message, or if an + * IOException occurs when accessing the given + * MimeMessage object + */ + public MSMessage(Session session, MimeMessage msg) + throws MessagingException { + super(session); + + if (!isMSMessage(msg)) // sanity check + throw new MessagingException("Not an MS message"); + + class FastByteArrayOutputStream extends ByteArrayOutputStream { + ByteArrayInputStream toByteArrayInputStream() { + return new ByteArrayInputStream(buf, 0, count); + } + } + + // extract the bytes of the given message + // ByteArrayOutputStream bos = new ByteArrayOutputStream(); + FastByteArrayOutputStream bos = new FastByteArrayOutputStream(); + try { + msg.writeTo(bos); + } catch (IOException ioex) { + throw new MessagingException("IOException", ioex); + } catch (Exception ex) { + throw new MessagingException("Exception", ex); + } + //parse(new ByteArrayInputStream(bos.toByteArray())); + parse(bos.toByteArrayInputStream()); + } + + /** + * Constructor to create a MSMessage from the given InputStream. + */ + public MSMessage(Session session, InputStream is) + throws MessagingException { + super(session); // setup headerstore etc + parse(is); + } + + // parse input stream + protected void parse(InputStream is) throws MessagingException { + // Create a buffered input stream for efficiency + if (!(is instanceof ByteArrayInputStream) && + !(is instanceof BufferedInputStream)) + is = new BufferedInputStream(is); + + // Load headerstore + headers.load(is); + + /* + * Load the content into a byte[]. + * This byte[] is shared among the bodyparts. + */ + try { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + int b; + // XXX - room for performance improvement + while ((b = is.read()) != -1) + bos.write(b); + content = bos.toByteArray(); + } catch (IOException ioex) { + throw new MessagingException("IOException", ioex); + } + + /* + * Check whether this is multipart. + */ + boolean isMulti = false; + // check for presence of X-MS-Attachment header + String[] att = getHeader("X-MS-Attachment"); + if (att != null && att.length > 0) + isMulti = true; + else { + /* + * Fall back to scanning the content. + * We scan the content until we find a sequence that looks + * like the start of a uuencoded block, i.e., "<newline>begin". + * If found, we claim that this is a multipart message. + */ + for (int i = 0; i < content.length; i++) { + int b = content[i] & 0xff; // mask higher byte + if (b == '\r' || b == '\n') { + // start of a new line + if ((i + 5) < content.length) { + // can there be a "begin" now? + String s = toString(content, i+1, i+6); + if (s.equalsIgnoreCase("begin")) { + isMulti= true; + break; + } + } + } + } + } + + if (isMulti) { + type = "multipart/mixed"; + dh = new DataHandler(new MSMultipartDataSource(this, content)); + } else { + type = "text/plain"; // charset = ? + dh = new DataHandler(new MimePartDataSource(this)); + } + + modified = false; + } + + /** + * Return content-type + */ + public String getContentType() throws MessagingException { + return type; + } + + /** + * Return content-disposition + */ + public String getDisposition() throws MessagingException { + return "inline"; + } + + /** + * Return content-transfer-encoding + */ + public String getEncoding() throws MessagingException { + return "7bit"; + } + + /** + * Check whether the given MimeMessage object represents a + * non-MIME message sent by Outlook. Such a message will + * have no MIME-Version header, may have an X-Mailer header + * that includes the word "Microsoft", and will have at least + * one X-MS-Attachment header. + */ + public static boolean isMSMessage(MimeMessage msg) + throws MessagingException { + // Check whether the MIME header is present + if (msg.getHeader("MIME-Version") != null) + // MIME-Version header present, should be a MIME message + return false; + + /* + * XXX - disabled X-Mailer check because many sample messages + * I saw didn't have an X-Mailer header at all. + */ + if (false) { + // Check X-Mailer + String mailer = msg.getHeader("X-mailer", null); + if (mailer == null) // No X-mailer ? + return false; // Oh well ! + if (mailer.indexOf("Microsoft") == -1) // Not MS stuff ? + return false; + } + + // Check X-MS-Attachment header + // XXX - not all such messages have this header + String[] att = msg.getHeader("X-MS-Attachment"); + if (att == null || att.length == 0) + return false; + + return true; + } + + // convert given byte array of ASCII characters to string + static String toString(byte[] b, int start, int end) { + int size = end - start; + char[] theChars = new char[size]; + + for (int i = 0, j = start; i < size; ) + theChars[i++] = (char)b[j++]; + return new String(theChars); + } +}
diff --git a/outlook/src/main/java/MSMultipartDataSource.java b/outlook/src/main/java/MSMultipartDataSource.java new file mode 100644 index 0000000..3c46fc3 --- /dev/null +++ b/outlook/src/main/java/MSMultipartDataSource.java
@@ -0,0 +1,105 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +import java.util.*; +import javax.mail.*; +import javax.mail.internet.*; + +/** + * A special MultipartDataSource used with MSMessage. + * + * @author John Mani + * @author Bill Shannon + */ +public class MSMultipartDataSource extends MimePartDataSource + implements MultipartDataSource { + //private List<MSBodyPart> parts; + private List parts; + + public MSMultipartDataSource(MimePart part, byte[] content) + throws MessagingException { + super(part); + //parts = new ArrayList<MSBodyPart>(); + parts = new ArrayList(); + + /* + * Parse the text of the message to find the attachments. + * + * Currently we just look for the lines that mark the + * begin and end of uuencoded data, but this can be + * fooled by similar text in the message body. Instead, + * we could use the Encoding header, which indicates how + * many lines are in each body part. For example: + * + * Encoding: 41 TEXT, 38 UUENCODE, 3155 UUENCODE, 1096 UUENCODE + * + * Similarly, we could get the filenames of the attachments + * from the X-MS-Attachment headers. For example: + * + * X-MS-Attachment: ATT00000.htx 0 00-00-1980 00:00 + * X-MS-Attachment: Serengeti 2GG.mpp 0 00-00-1980 00:00 + * X-MS-Attachment: project team update 031298.doc 0 00-00-1980 00:00 + * + * (Note that there might be unquoted spaces in the filename.) + */ + int pos = startsWith(content, 0, "begin"); + if (pos == -1) + throw new MessagingException("invalid multipart"); + + if (pos > 0) // we have an unencoded main body part + parts.add(new MSBodyPart(content, 0, pos, "inline", "7bit")); + else // no main body part + pos = 0; + + // now collect all the uuencoded individual body parts + int start; + for (;;) { + start = startsWith(content, pos, "begin"); + if (start == -1) + break; + pos = startsWith(content, start, "end"); + if (pos == -1) + break; + pos += 3; // skip to the end of "end" + parts.add(new MSBodyPart(content, start, pos, + "attachment", "uuencode")); + } + } + + public int getCount() { + return parts.size(); + } + + public BodyPart getBodyPart(int index) throws MessagingException { + return (BodyPart)parts.get(index); + } + + /** + * This method scans the given byte[], beginning at "start", for + * lines that begin with the sequence "seq". If found, the start + * position of the sequence within the byte[] is returned. + */ + private int startsWith(byte[] content, int start, String seq) { + int slen = seq.length(); + boolean bol = true; + for (int i = start; i < content.length; i++) { + if (bol) { + if ((i + slen) < content.length) { + String s = MSMessage.toString(content, i, i + slen); + if (s.equalsIgnoreCase(seq)) + return i; + } + } + int b = content[i] & 0xff; + bol = b == '\r' || b == '\n'; + } + return -1; + } +}
diff --git a/outlook/src/main/java/README.txt b/outlook/src/main/java/README.txt new file mode 100644 index 0000000..36cdea5 --- /dev/null +++ b/outlook/src/main/java/README.txt
@@ -0,0 +1,9 @@ +The classes in this directory allow processing old style non-MIME +messages created by Microsoft Outlook. Use them like this: + + if (MSMessage.isMSMessage(msg)) + msg = new MSMessage(session, msg); + +Note that these classes are not particularly efficient or optimized, +but they show how to process these non-MIME messages and make them +look like MIME messages.
diff --git a/parent-distrib/pom.xml b/parent-distrib/pom.xml new file mode 100644 index 0000000..9a32f76 --- /dev/null +++ b/parent-distrib/pom.xml
@@ -0,0 +1,168 @@ +<?xml version="1.0" encoding="iso-8859-1"?> +<!-- + + Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 + http://maven.apache.org/maven-v4_0_0.xsd"> + <parent> + <groupId>com.sun.mail</groupId> + <artifactId>all</artifactId> + <version>1.6.3</version> + </parent> + <modelVersion>4.0.0</modelVersion> + <groupId>com.sun.mail</groupId> + <artifactId>parent-distrib</artifactId> + <packaging>pom</packaging> + <name>JavaMail API ${project.artifactId} provider</name> + + <properties> + <mail.classes>com/sun/mail/${project.artifactId}/**</mail.classes> + </properties> + + <profiles> + <!-- + A special profile for compiling with JDK 9. + --> + <profile> + <id>9</id> + <activation> + <jdk>9</jdk> + </activation> + <build> + <plugins> + <plugin> + <artifactId>maven-compiler-plugin</artifactId> + <executions> + <execution> + <id>default-compile</id> + <configuration> + <release>9</release> + <source>9</source> + <target>9</target> + <compilerArgs> + <arg>-Xlint</arg> + <arg>-Xlint:-options</arg> + <arg>-Xlint:-path</arg> + <!-- + Too many finalize warnings. + <arg>-Werror</arg> + --> + </compilerArgs> + <!-- un-exclude module-info.java --> + <excludes combine.self="override"> + </excludes> + </configuration> + </execution> + </executions> + </plugin> + <!-- + Require JDK 9. + --> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-enforcer-plugin</artifactId> + <executions> + <execution> + <id>enforce-version</id> + <goals> + <goal>enforce</goal> + </goals> + <configuration> + <rules> + <requireMavenVersion> + <version>[3.0.3,)</version> + </requireMavenVersion> + <requireJavaVersion> + <version>9</version> + </requireJavaVersion> + </rules> + </configuration> + </execution> + </executions> + </plugin> + </plugins> + </build> + </profile> + </profiles> + + <build> + <plugins> + <plugin> + <artifactId>maven-dependency-plugin</artifactId> + <executions> + <execution> + <!-- download the binaries --> + <id>get-binaries</id> + <phase>generate-sources</phase> + <goals> + <goal>unpack</goal> + </goals> + </execution> + <execution> + <!-- download the sources --> + <id>get-sources</id> + <phase>generate-sources</phase> + <goals> + <goal>unpack</goal> + </goals> + <configuration> + <artifactItems> + <artifactItem> + <groupId>com.sun.mail</groupId> + <artifactId>jakarta.mail</artifactId> + <version>${mail.version}</version> + <classifier>sources</classifier> + <outputDirectory> + ${project.build.directory}/sources + </outputDirectory> + </artifactItem> + </artifactItems> + </configuration> + </execution> + </executions> + <configuration> + <artifactItems> + <artifactItem> + <groupId>com.sun.mail</groupId> + <artifactId>jakarta.mail</artifactId> + <version>${mail.version}</version> + </artifactItem> + </artifactItems> + <outputDirectory> + ${project.build.outputDirectory} + </outputDirectory> + <includes> + META-INF/LICENSE.txt, + ${mail.classes}, + ${mail.extraClasses} + </includes> + </configuration> + </plugin> + </plugins> + </build> + + <dependencies> + <dependency> + <groupId>com.sun.mail</groupId> + <artifactId>mailapi</artifactId> + <version>${mail.version}</version> + </dependency> + </dependencies> +</project>
diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..58efdc0 --- /dev/null +++ b/pom.xml
@@ -0,0 +1,919 @@ +<?xml version="1.0" encoding="iso-8859-1"?> +<!-- + + Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 + http://maven.apache.org/maven-v4_0_0.xsd"> + <parent> + <groupId>org.eclipse.ee4j</groupId> + <artifactId>project</artifactId> + <version>1.0.4</version> + </parent> + <modelVersion>4.0.0</modelVersion> + <groupId>com.sun.mail</groupId> + <artifactId>all</artifactId> + <packaging>pom</packaging> + <version>1.6.3</version> + <name>JavaMail API distribution</name> + <description>${project.name}</description> + <url>http://eclipse-ee4j.github.io/javamail</url> + + <scm> + <connection>scm:git:ssh://git@github.com/eclipse-ee4j/javamail.git</connection> + <developerConnection>scm:git:ssh://git@github.com/eclipse-ee4j/javamail.git</developerConnection> + <url>https://github.com/eclipse-ee4j/javamail</url> + </scm> + + <issueManagement> + <system>GitHub</system> + <url>https://github.com/eclipse-ee4j/javamail/issues</url> + </issueManagement> + + <licenses> + <license> + <name>EPL 2.0</name> + <url>http://www.eclipse.org/legal/epl-2.0</url> + <distribution>repo</distribution> + </license> + <license> + <name>GPL2 w/ CPE</name> + <url>https://www.gnu.org/software/classpath/license.html</url> + <distribution>repo</distribution> + </license> + <license> + <name>EDL 1.0</name> + <url>http://www.eclipse.org/org/documents/edl-v10.php</url> + <distribution>repo</distribution> + </license> + </licenses> + + <organization> + <name>Oracle</name> + <url>http://www.oracle.com</url> + </organization> + + <properties> + <mail.version>1.6.3</mail.version> + <!-- like mail.version, but with underscores instead of dots --> + <mail.zipversion>1_6_3</mail.zipversion> + <mail.spec.version>1.6</mail.spec.version> + <activation.version>1.2.1</activation.version> + <!-- defaults that are overridden in mail module --> + <mail.extensionName> + ${project.groupId}.${project.artifactId} + </mail.extensionName> + <mail.specificationTitle> + ${project.groupId}.${project.artifactId} + </mail.specificationTitle> + <mail.implementationTitle> + ${project.groupId}.${project.artifactId} + </mail.implementationTitle> + <mail.bundle.symbolicName> + ${project.groupId}.${project.artifactId} + </mail.bundle.symbolicName> + <mail.packages.export> + javax.mail.*; version=${mail.spec.version} + </mail.packages.export> + <mail.packages.import> + javax.activation;version=!, + javax.security.sasl;resolution:=optional, + sun.security.util;resolution:=optional, + * + </mail.packages.import> + <mail.packages.private> + com.sun.mail.* + </mail.packages.private> + <mail.probeFile/> + <!-- for the osgiversion-maven-plugin --> + <hk2.plugin.version>2.0.0</hk2.plugin.version> + <javac.path>/opt/jdk1.7/bin/javac</javac.path> + <project.build.sourceEncoding>iso-8859-1</project.build.sourceEncoding> + <findbugs.threshold> + High + </findbugs.threshold> + <findbugs.version> + 3.1.0 + </findbugs.version> + <findbugs.skip> + true + </findbugs.skip> + <findbugs.exclude/> + <copyright-plugin.version>1.46</copyright-plugin.version> + </properties> + + <developers> + <developer> + <id>shannon</id> + <name>Bill Shannon</name> + <email>bill.shannon@oracle.com</email> + <organization>Oracle</organization> + <roles> + <role>lead</role> + </roles> + </developer> + </developers> + + <!-- following to enable use of "mvn site:stage" --> + <distributionManagement> + <site> + <id>oracle.com</id> + <url>file:/tmp</url> <!-- not used --> + </site> + </distributionManagement> + + <modules> + <module>mail</module> + <module>mailapi</module> + <module>mailapijar</module> + <module>smtp</module> + <module>imap</module> + <module>gimap</module> + <module>pop3</module> + <module>dsn</module> + <module>mailhandler</module> + <module>android</module> + </modules> + + <profiles> + <!-- + This profile contains modules that should only be built + but not installed or deployed. + --> + <profile> + <id>build-only</id> + <modules> + <module>mbox</module> + <module>demo</module> + <module>client</module> + <module>servlet</module> + <module>webapp</module> + <module>taglib</module> + <module>logging</module> + <module>outlook</module> + <module>javadoc</module> + <module>publish</module> + </modules> + <activation> + <activeByDefault>true</activeByDefault> + </activation> + </profile> + + <!-- + This profile is used for deploying a JavaMail final release. + + Activating this profile manually for deployment causes + the above profile to be deactivated, which works around + an apparent bug in maven that prevents me from manually + deactivating a profile. This profile purposely has none + of the modules I don't want to be deployed. + --> + <profile> + <id>deploy-release</id> + <modules> + <module>parent-distrib</module> + </modules> + <build> + <pluginManagement> + <plugins> + <plugin> + <!-- Enables step-by-step staging deployment --> + <groupId>org.sonatype.plugins</groupId> + <artifactId>nexus-staging-maven-plugin</artifactId> + <version>1.6.7</version> + </plugin> + <plugin> + <!-- Newer version then in parent --> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-gpg-plugin</artifactId> + <version>1.6</version> + </plugin> + </plugins> + </pluginManagement> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-surefire-plugin</artifactId> + <configuration> + <skip>true</skip> + </configuration> + </plugin> + + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-javadoc-plugin</artifactId> + <executions> + <execution> + <id>attach-javadocs</id> + <goals> + <goal>jar</goal> + </goals> + </execution> + </executions> + </plugin> + + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-gpg-plugin</artifactId> + <executions> + <execution> + <id>sign-artifacts</id> + <phase>verify</phase> + <goals> + <goal>sign</goal> + </goals> + </execution> + </executions> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-deploy-plugin</artifactId> + <configuration> + <skip>true</skip> <!-- To prefer nexus-staging-maven-plugin --> + </configuration> + </plugin> + <plugin> + <groupId>org.sonatype.plugins</groupId> + <artifactId>nexus-staging-maven-plugin</artifactId> + <extensions>true</extensions> + <executions> + <execution> + <id>default-deploy</id> + <phase>deploy</phase> + <goals> + <goal>deploy</goal> + </goals> + </execution> + </executions> + <configuration> + <serverId>ossrh</serverId> + <nexusUrl>https://oss.sonatype.org/</nexusUrl> + <autoReleaseAfterClose>false</autoReleaseAfterClose> + </configuration> + </plugin> + </plugins> + </build> + </profile> + + <!-- + This profile is used for deploying a JavaMail SNAPSHOT release. + It's identical to the above deploy-release profile except that + artifacts aren't signed. + --> + <profile> + <id>deploy-snapshot</id> + <modules> + <module>parent-distrib</module> + </modules> + <build> + <pluginManagement> + <plugins> + <plugin> + <!-- Enables step-by-step staging deployment --> + <groupId>org.sonatype.plugins</groupId> + <artifactId>nexus-staging-maven-plugin</artifactId> + <version>1.6.7</version> + </plugin> + </plugins> + </pluginManagement> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-surefire-plugin</artifactId> + <configuration> + <skip>true</skip> + </configuration> + </plugin> + + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-javadoc-plugin</artifactId> + <executions> + <execution> + <id>attach-javadocs</id> + <goals> + <goal>jar</goal> + </goals> + </execution> + </executions> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-deploy-plugin</artifactId> + <configuration> + <skip>true</skip> <!-- To prefer nexus-staging-maven-plugin --> + </configuration> + </plugin> + <plugin> + <groupId>org.sonatype.plugins</groupId> + <artifactId>nexus-staging-maven-plugin</artifactId> + <extensions>true</extensions> + <executions> + <execution> + <id>default-deploy</id> + <phase>deploy</phase> + <goals> + <goal>deploy</goal> + </goals> + </execution> + </executions> + <configuration> + <serverId>ossrh</serverId> + <nexusUrl>https://oss.sonatype.org/</nexusUrl> + <autoReleaseAfterClose>false</autoReleaseAfterClose> + </configuration> + </plugin> + </plugins> + </build> + </profile> + + <!-- + A special profile for compiling with the real JDK 1.7 + compiler, to make sure there are no accidental dependencies + on JDK 1.8 or newer APIs. Set the property javac.path to the path + to the JDK 1.7 compiler, e.g., + "mvn -P1.7 -Djavac.path=/opt/jdk1.7/bin/javac". + --> + <profile> + <id>1.7</id> + <build> + <plugins> + <plugin> + <artifactId>maven-compiler-plugin</artifactId> + <executions> + <execution> + <id>default-compile</id> + <configuration> + <fork>true</fork> + <executable>${javac.path}</executable> + <compilerVersion>1.7</compilerVersion> + <source>1.7</source> + <target>1.7</target> + </configuration> + </execution> + </executions> + </plugin> + <!-- + Only require JDK 1.7. + --> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-enforcer-plugin</artifactId> + <executions> + <execution> + <id>enforce-version</id> + <goals> + <goal>enforce</goal> + </goals> + <configuration> + <rules> + <requireMavenVersion> + <version>[3.0.3,)</version> + </requireMavenVersion> + <requireJavaVersion> + <version>1.7</version> + </requireJavaVersion> + </rules> + </configuration> + </execution> + </executions> + </plugin> + </plugins> + </build> + </profile> + + <!-- + A special profile for compiling with JDK 9. + --> + <profile> + <id>9</id> + <activation> + <jdk>9</jdk> + </activation> + <build> + <plugins> + <plugin> + <artifactId>maven-compiler-plugin</artifactId> + <executions> + <execution> + <id>default-compile</id> + <configuration> + <compilerArgs> + <arg>-Xlint</arg> + <arg>-Xlint:-options</arg> + <arg>-Xlint:-path</arg> + <!-- + Too many finalize warnings. + <arg>-Werror</arg> + --> + </compilerArgs> + </configuration> + </execution> + </executions> + </plugin> + <!-- + Require JDK 9. + --> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-enforcer-plugin</artifactId> + <executions> + <execution> + <id>enforce-version</id> + <goals> + <goal>enforce</goal> + </goals> + <configuration> + <rules> + <requireMavenVersion> + <version>[3.0.3,)</version> + </requireMavenVersion> + <requireJavaVersion> + <version>9</version> + </requireJavaVersion> + </rules> + </configuration> + </execution> + </executions> + </plugin> + </plugins> + </build> + </profile> + </profiles> + + <build> + <defaultGoal>install</defaultGoal> + <plugins> + <!-- + Make sure we're using the correct version of maven. + --> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-enforcer-plugin</artifactId> + <version>3.0.0-M1</version> + <executions> + <execution> + <id>enforce-version</id> + <goals> + <goal>enforce</goal> + </goals> + <configuration> + <rules> + <requireMavenVersion> + <version>[3.0.3,)</version> + </requireMavenVersion> + <requireJavaVersion> + <version>1.8</version> + </requireJavaVersion> + </rules> + </configuration> + </execution> + </executions> + </plugin> + + <!-- + This plugin is reponsible for packaging artifacts + as OSGi bundles. Please refer to + http://felix.apache.org/site/apache-felix-maven-bundle-plugin-bnd.html + for more information about how to use this plugin. + --> + <plugin> + <groupId>org.apache.felix</groupId> + <artifactId>maven-bundle-plugin</artifactId> + <configuration> + <instructions> + <Bundle-SymbolicName> + ${mail.bundle.symbolicName} + </Bundle-SymbolicName> + <Export-Package> + ${mail.packages.export} + </Export-Package> + <Import-Package> + ${mail.packages.import} + </Import-Package> + <Private-Package> + ${mail.packages.private} + </Private-Package> + <DynamicImport-Package> + * + </DynamicImport-Package> + </instructions> + <niceManifest>true</niceManifest> + </configuration> + <!-- + Since we don't change the packaging type to bundle, we + need to configure the plugin to execute the manifest goal + during the process-classes phase of the build life cycle. + --> + <executions> + <execution> + <id>osgi-manifest</id> + <phase>process-classes</phase> + <goals> + <goal>manifest</goal> + </goals> + </execution> + </executions> + </plugin> + + <!-- + Since we don't want a qualifier like b05 or SNAPSHOT to + appear in the OSGi package version attribute, we use + the following plugin to populate a project property + with an OSGi version that is equivalent to the maven + version without the qualifier. + --> + <plugin> + <groupId>org.glassfish.hk2</groupId> + <artifactId>osgiversion-maven-plugin</artifactId> + <version>${hk2.plugin.version}</version> + <configuration> + <dropVersionComponent>qualifier</dropVersionComponent> + <versionPropertyName>mail.osgiversion</versionPropertyName> + </configuration> + <executions> + <execution> + <id>compute-osgi-version</id> + <goals> + <goal>compute-osgi-version</goal> + </goals> + </execution> + </executions> + </plugin> + + <!-- + Use the 1.8 compiler for JavaMail itself and the test classes, + but restrict it to 1.7 source and target. The 1.8 compiler is + used *only* to pick up the definition of the Repeatable + annotation, needed by MailServiceDefinition. + --> + <plugin> + <artifactId>maven-compiler-plugin</artifactId> + <executions> + <execution> + <id>default-compile</id> + <configuration> + <source>1.7</source> + <target>1.7</target> + <!-- + XXX - workaround for bug in maven compiler + plugin versions 3.0 - 3.3 (at least): + https://issues.apache.org/jira/browse/MCOMPILER-209 + --> + <useIncrementalCompilation>false</useIncrementalCompilation> + <excludes> + <exclude> + module-info.java + </exclude> + </excludes> + </configuration> + </execution> + <execution> + <id>default-testCompile</id> + <configuration> + <source>1.7</source> + <target>1.7</target> + </configuration> + </execution> + </executions> + </plugin> + + <plugin> + <artifactId>maven-jar-plugin</artifactId> + <configuration> + <finalName>${project.artifactId}</finalName> + <archive> + <!-- + Configure the maven-jar-plugin to pick up + META-INF/MANIFEST.MF that's generated by + the maven-bundle-plugin. + --> + <manifestFile> + ${project.build.outputDirectory}/META-INF/MANIFEST.MF + </manifestFile> + <manifestEntries> + <Extension-Name> + ${mail.extensionName} + </Extension-Name> + <Specification-Title> + ${mail.specificationTitle} + </Specification-Title> + <Specification-Version> + ${mail.spec.version} + </Specification-Version> + <Specification-Vendor> + ${project.organization.name} + </Specification-Vendor> + <Implementation-Title> + ${mail.implementationTitle} + </Implementation-Title> + <Implementation-Version> + ${project.version} + </Implementation-Version> + <Implementation-Vendor> + ${project.organization.name} + </Implementation-Vendor> + <Implementation-Vendor-Id> + com.sun + </Implementation-Vendor-Id> + <Probe-Provider-XML-File-Names> + ${mail.probeFile} + </Probe-Provider-XML-File-Names> + </manifestEntries> + </archive> + <excludes> + <exclude>**/*.java</exclude> + </excludes> + </configuration> + </plugin> + + <!-- + Tell the source plugin about the sources that may have + been downloaded by the maven-dependency-plugin. + + Also, need this plugin to define target/classes as another + source directory so that the filtered Version.java + that's copied there will also be compiled when using + the latest version of the maven-compiler-plugin. + --> + + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>build-helper-maven-plugin</artifactId> + <executions> + <execution> + <id>add-source</id> + <phase>generate-sources</phase> + <goals> + <goal>add-source</goal> + </goals> + <configuration> + <sources> + <source> <!-- for dependencies --> + ${project.build.directory}/sources + </source> + <source> <!-- for Version.java --> + target/classes + </source> + </sources> + </configuration> + </execution> + </executions> + </plugin> + + <!-- + Configure the source plugin here so that it will know + about the sources that may have been downloaded by the + maven-dependency-plugin and configured by the + build-helper-maven-plugin. + --> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-source-plugin</artifactId> + <executions> + <execution> + <id>attach-sources</id> + <goals> + <goal>jar-no-fork</goal> + </goals> + </execution> + </executions> + <configuration> + <includePom>true</includePom> + <!-- + Since we added the classes directory using the + build-helper-maven-plugin above, we need to exclude + the class files from the source jar file. + --> + <excludes> + <exclude>**/*.class</exclude> + </excludes> + </configuration> + </plugin> + +<!-- not used + <plugin> + <artifactId>maven-release-plugin</artifactId> + <configuration> + <arguments>-P deploy</arguments> + </configuration> + </plugin> +--> + </plugins> + + <pluginManagement> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + <version>3.7.0</version> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-surefire-plugin</artifactId> + <version>2.4.3</version> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-jar-plugin</artifactId> + <!-- need at least this version to make excludes work --> + <version>2.4</version> + </plugin> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>build-helper-maven-plugin</artifactId> + <version>1.7</version> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-assembly-plugin</artifactId> + <version>2.4</version> + </plugin> + <plugin> + <!-- + By default, disable the SpotBugs plugin for all modules. + It's enabled in the modules where we actually want to + run it. + --> + <groupId>com.github.spotbugs</groupId> + <artifactId>spotbugs-maven-plugin</artifactId> + <version>${findbugs.version}</version> + <configuration> + <skip>${findbugs.skip}</skip> + <threshold>${findbugs.threshold}</threshold> + <findbugsXmlWithMessages>true</findbugsXmlWithMessages> + <excludeFilterFile> + ${findbugs.exclude} + </excludeFilterFile> + </configuration> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-enforcer-plugin</artifactId> + <version>3.0.0-M1</version> + </plugin> + <plugin> + <groupId>org.apache.felix</groupId> + <artifactId>maven-bundle-plugin</artifactId> + <version>3.5.0</version> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-source-plugin</artifactId> + <version>2.1.2</version> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-javadoc-plugin</artifactId> + <version>3.0.0-M1</version> + <configuration> + <!-- make all the APIs available for javadoc --> + <additionalDependencies> + <additionalDependency> + <groupId>com.sun.mail</groupId> + <artifactId>jakarta.mail</artifactId> + <version>${mail.version}</version> + </additionalDependency> + <additionalDependency> + <groupId>com.sun.mail</groupId> + <artifactId>gimap</artifactId> + <version>${mail.version}</version> + </additionalDependency> + </additionalDependencies> + <sourceFileExcludes> + <sourceFileExclude> + module-info.java + </sourceFileExclude> + </sourceFileExcludes> + </configuration> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-war-plugin</artifactId> + <version>2.2</version> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-project-info-reports-plugin</artifactId> + <version>2.7</version> + </plugin> + <plugin> + <groupId>org.glassfish.copyright</groupId> + <artifactId>glassfish-copyright-maven-plugin</artifactId> + <version>${copyright-plugin.version}</version> + <configuration> + <scm>git</scm> + <scmOnly>true</scmOnly> + <excludeFile> + copyright-exclude + </excludeFile> + </configuration> + </plugin> + <plugin> + <groupId>org.sonatype.plugins</groupId> + <artifactId>nexus-staging-maven-plugin</artifactId> + <extensions>true</extensions> + <configuration> + <serverId>ossrh</serverId> + <nexusUrl>https://oss.sonatype.org/</nexusUrl> + </configuration> + </plugin> + </plugins> + </pluginManagement> + </build> + + <dependencyManagement> + <dependencies> + <dependency> + <groupId>com.sun.activation</groupId> + <artifactId>jakarta.activation</artifactId> + <version>${activation.version}</version> + </dependency> + <dependency> + <groupId>com.sun.mail</groupId> + <artifactId>jakarta.mail</artifactId> + <version>${mail.version}</version> + </dependency> + <dependency> + <groupId>com.sun.mail</groupId> + <artifactId>dsn</artifactId> + <version>${mail.version}</version> + </dependency> + <dependency> + <groupId>com.sun.mail</groupId> + <artifactId>gimap</artifactId> + <version>${mail.version}</version> + </dependency> + <dependency> + <groupId>com.sun.mail</groupId> + <artifactId>mbox</artifactId> + <version>${mail.version}</version> + </dependency> + <dependency> + <groupId>com.sun.mail</groupId> + <artifactId>taglib</artifactId> + <version>${mail.version}</version> + </dependency> + <dependency> + <groupId>javax.servlet</groupId> + <artifactId>servlet-api</artifactId> + <version>2.5</version> + </dependency> + <dependency> + <groupId>javax.servlet.jsp</groupId> + <artifactId>jsp-api</artifactId> + <version>2.1</version> + </dependency> + </dependencies> + </dependencyManagement> + + <!-- + I'm leaving this here but commented out to remind myself + NOT to add this back in because it breaks the Android + version of JavaMail. + + <dependencies> + <dependency> + <groupId>com.sun.activation</groupId> + <artifactId>jakarta.activation</artifactId> + </dependency> + </dependencies> + --> + + <reporting> + <plugins> + <!-- + Configure SpotBugs to run with "mvn site" and + generate html output that can be viewed directly. + --> + <plugin> + <groupId>com.github.spotbugs</groupId> + <artifactId>spotbugs-maven-plugin</artifactId> + <version>${findbugs.version}</version> + <configuration> + <skip>${findbugs.skip}</skip> + <threshold>${findbugs.threshold}</threshold> + <excludeFilterFile> + ${findbugs.exclude} + </excludeFilterFile> + </configuration> + </plugin> + </plugins> + </reporting> +</project>
diff --git a/pop3/pom.xml b/pop3/pom.xml new file mode 100644 index 0000000..09d8cf7 --- /dev/null +++ b/pop3/pom.xml
@@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="iso-8859-1"?> +<!-- + + Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 + http://maven.apache.org/maven-v4_0_0.xsd"> + <parent> + <groupId>com.sun.mail</groupId> + <artifactId>parent-distrib</artifactId> + <version>1.6.3</version> + <relativePath>../parent-distrib/pom.xml</relativePath> + </parent> + <modelVersion>4.0.0</modelVersion> + <groupId>com.sun.mail</groupId> + <artifactId>pop3</artifactId> + <packaging>jar</packaging> + <name>JavaMail API pop3 provider</name> + + <properties> + <mail.packages.export> + com.sun.mail.pop3; version=${mail.osgiversion} + </mail.packages.export> + </properties> +</project>
diff --git a/pop3/src/main/java/module-info.java b/pop3/src/main/java/module-info.java new file mode 100644 index 0000000..cc0e83e --- /dev/null +++ b/pop3/src/main/java/module-info.java
@@ -0,0 +1,25 @@ +/* + * Copyright (c) 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +module com.sun.mail.pop3 { + exports com.sun.mail.pop3; + provides javax.mail.Provider with + com.sun.mail.pop3.POP3Provider, com.sun.mail.pop3.POP3SSLProvider; + + requires jakarta.mail; + requires java.logging; + requires java.security.sasl; +}
diff --git a/pop3/src/main/resources/META-INF/MANIFEST.MF b/pop3/src/main/resources/META-INF/MANIFEST.MF new file mode 100644 index 0000000..ef209b2 --- /dev/null +++ b/pop3/src/main/resources/META-INF/MANIFEST.MF
@@ -0,0 +1,9 @@ +Manifest-Version: 1.0 +Extension-Name: com.sun.mail.pop3 +Specification-Title: com.sun.mail.pop3 +Specification-Version: ${mail.spec.version} +Specification-Vendor: ${project.organization.name} +Implementation-Title: com.sun.mail.pop3 +Implementation-Version: ${mail.version} +Implementation-Vendor: ${project.organization.name} +Implementation-Vendor-Id: com.sun
diff --git a/pop3/src/main/resources/META-INF/javamail.providers b/pop3/src/main/resources/META-INF/javamail.providers new file mode 100644 index 0000000..b6de993 --- /dev/null +++ b/pop3/src/main/resources/META-INF/javamail.providers
@@ -0,0 +1,3 @@ +# JavaMail POP3 provider Oracle +protocol=pop3; type=store; class=com.sun.mail.pop3.POP3Store; vendor=Oracle; +protocol=pop3s; type=store; class=com.sun.mail.pop3.POP3SSLStore; vendor=Oracle;
diff --git a/pop3/src/main/resources/META-INF/services/javax.mail.Provider b/pop3/src/main/resources/META-INF/services/javax.mail.Provider new file mode 100644 index 0000000..1ef4dd4 --- /dev/null +++ b/pop3/src/main/resources/META-INF/services/javax.mail.Provider
@@ -0,0 +1,2 @@ +com.sun.mail.pop3.POP3Provider +com.sun.mail.pop3.POP3SSLProvider
diff --git a/project.properties b/project.properties new file mode 100644 index 0000000..f1cba98 --- /dev/null +++ b/project.properties
@@ -0,0 +1,19 @@ +# +# Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. +# +# This program and the accompanying materials are made available under the +# terms of the Eclipse Public License v. 2.0, which is available at +# http://www.eclipse.org/legal/epl-2.0. +# +# This Source Code may also be made available under the following Secondary +# Licenses when the conditions for such availability set forth in the +# Eclipse Public License v. 2.0 are satisfied: GNU General Public License, +# version 2 with the GNU Classpath Exception, which is available at +# https://www.gnu.org/software/classpath/license.html. +# +# SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +# + +glassfish.module.name=mail +maven.mode.online=false +activation.jar=$HOME/ext/activation.jar
diff --git a/project.xml b/project.xml new file mode 100644 index 0000000..d3aa318 --- /dev/null +++ b/project.xml
@@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="iso-8859-1"?> +<!-- + + Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<project> + <extend>../bootstrap/project.xml</extend> + <pomVersion>3</pomVersion> + <id>${glassfish.module.name}</id> + <artifactId>mail</artifactId> + <name>mail</name> + <groupId>${glassfish.image.name}</groupId> + <currentVersion>9.0-SNAPSHOT</currentVersion> + <organization> + <name>Sun Microsystems Inc.</name> + <url>http://www.sun.com/</url> + <logo>http://java.net/j2ee</logo> + </organization> + <inceptionYear>2005</inceptionYear> + <package>com.sun</package> + <logo>http://java.sun.com/j2ee</logo> + <description>SJSAS 9.0 Glassfish project.</description> + <shortDescription>Glassfish 9.0</shortDescription> + <url>http://java.sun.com/j2ee</url> + <issueTrackingUrl>Bugster</issueTrackingUrl> + <siteAddress>http://bugster</siteAddress> + <distributionDirectory>/java/re/sjsas_pe/</distributionDirectory> + <repository> + <connection>scm:${glassfish.cvsroot}</connection> + <url>${glassfish.viewcvs}</url> + </repository> +</project> +
diff --git a/publish/pom.xml b/publish/pom.xml new file mode 100644 index 0000000..0533057 --- /dev/null +++ b/publish/pom.xml
@@ -0,0 +1,109 @@ +<?xml version="1.0" encoding="iso-8859-1"?> +<!-- + + Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 + http://maven.apache.org/maven-v4_0_0.xsd"> + <parent> + <groupId>com.sun.mail</groupId> + <artifactId>all</artifactId> + <version>1.6.3</version> + </parent> + <modelVersion>4.0.0</modelVersion> + <groupId>com.sun.mail</groupId> + <artifactId>publish</artifactId> + <packaging>pom</packaging> + <version>1.6.3</version> + <name>JavaMail API publish project</name> + + <build> + <plugins> + <!-- + This is the rule that collects the release artifacts and + creates the samples zip file for a release. + --> + <plugin> + <artifactId>maven-assembly-plugin</artifactId> + <inherited>false</inherited> + <executions> + <execution> + <phase>package</phase> + <goals> + <goal>single</goal> + </goals> + <configuration> + <finalName>javamail</finalName> + <descriptors> + <descriptor>publish.xml</descriptor> + <descriptor>samples.xml</descriptor> + </descriptors> + </configuration> + </execution> + </executions> + </plugin> + </plugins> + </build> + + <dependencies> + <dependency> + <groupId>com.sun.mail</groupId> + <artifactId>jakarta.mail</artifactId> + <version>1.6.3</version> + </dependency> + <!-- + <dependency> + <groupId>com.sun.mail</groupId> + <artifactId>demo</artifactId> + <version>1.6.3</version> + </dependency> + <dependency> + <groupId>com.sun.mail</groupId> + <artifactId>client</artifactId> + <version>1.6.3</version> + </dependency> + <dependency> + <groupId>com.sun.mail</groupId> + <artifactId>servlet</artifactId> + <version>1.6.3</version> + </dependency> + <dependency> + <groupId>com.sun.mail</groupId> + <artifactId>webapp</artifactId> + <type>war</type> + <version>1.6.3</version> + </dependency> + <dependency> + <groupId>com.sun.mail</groupId> + <artifactId>taglib</artifactId> + <version>1.6.3</version> + </dependency> + <dependency> + <groupId>com.sun.mail</groupId> + <artifactId>logging</artifactId> + <version>1.6.3</version> + </dependency> + <dependency> + <groupId>com.sun.mail</groupId> + <artifactId>outlook</artifactId> + <version>1.6.3</version> + </dependency> + --> + </dependencies> +</project>
diff --git a/publish/publish.xml b/publish/publish.xml new file mode 100644 index 0000000..a612348 --- /dev/null +++ b/publish/publish.xml
@@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="iso-8859-1"?> +<!-- + + Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<assembly> + <id>publish</id> + <formats> + <format>dir</format> + </formats> + <includeBaseDirectory>false</includeBaseDirectory> + <dependencySets> + <!-- include the main jakarta.mail.jar file --> + <dependencySet> + <includes> + <include>com.sun.mail:jakarta.mail</include> + </includes> + <useTransitiveDependencies>false</useTransitiveDependencies> + <unpack>false</unpack> + <outputFileNameMapping> + jakarta.mail.jar + </outputFileNameMapping> + </dependencySet> + </dependencySets> + + <!-- include docs, specs, and javadocs --> + <fileSets> + <fileSet> + <directory>../doc/release</directory> + <outputDirectory></outputDirectory> + <filtered>true</filtered> + <includes> + <include>*.txt</include> + </includes> + </fileSet> + <fileSet> + <directory>../doc/spec</directory> + <outputDirectory>docs</outputDirectory> + </fileSet> + <fileSet> + <directory>../javadoc/target/site/apidocs</directory> + <outputDirectory>docs/api</outputDirectory> + </fileSet> + </fileSets> +</assembly>
diff --git a/publish/samples.xml b/publish/samples.xml new file mode 100644 index 0000000..648f88f --- /dev/null +++ b/publish/samples.xml
@@ -0,0 +1,82 @@ +<?xml version="1.0" encoding="iso-8859-1"?> +<!-- + + Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<assembly> + <id>samples</id> + <formats> + <format>zip</format> + </formats> + <baseDirectory>javamail-samples</baseDirectory> + <fileSets> + <fileSet> + <directory>../demo/src/main/java</directory> + <outputDirectory>/</outputDirectory> + <excludes> + <exclude>internal/**</exclude> + </excludes> + </fileSet> + <fileSet> + <directory>../client/src/main/java</directory> + <outputDirectory>client</outputDirectory> + </fileSet> + <fileSet> + <directory>../webapp/src/main/java</directory> + <outputDirectory>webapp/src/classes</outputDirectory> + </fileSet> + <fileSet> + <directory>../webapp/src/main/webapp</directory> + <outputDirectory>webapp/src/docroot</outputDirectory> + </fileSet> + <fileSet> + <directory>../webapp/</directory> + <outputDirectory>webapp</outputDirectory> + <includes> + <include>build.*</include> + <include>webapp.README.txt</include> + </includes> + </fileSet> + <fileSet> + <directory>../taglib/src/main/java</directory> + <outputDirectory>webapp/src/taglib</outputDirectory> + </fileSet> + <fileSet> + <directory>../taglib/src/main/resources</directory> + <outputDirectory>webapp/src/taglib</outputDirectory> + </fileSet> + <fileSet> + <directory>../outlook/src/main/java</directory> + <outputDirectory>outlook</outputDirectory> + </fileSet> + <fileSet> + <directory>../logging/src/main/java</directory> + <outputDirectory>logging</outputDirectory> + </fileSet> + </fileSets> + <dependencySets> + <!-- include sources from the demo modules --> + <!-- + <dependencySet> + <includes> + <include>com.sun.mail:demo:jar:sources</include> + </includes> + <unpack>true</unpack> + </dependencySet> + --> + </dependencySets> +</assembly>
diff --git a/servlet/pom.xml b/servlet/pom.xml new file mode 100644 index 0000000..e77f59f --- /dev/null +++ b/servlet/pom.xml
@@ -0,0 +1,78 @@ +<?xml version="1.0" encoding="iso-8859-1"?> +<!-- + + Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 + http://maven.apache.org/maven-v4_0_0.xsd"> + <parent> + <groupId>com.sun.mail</groupId> + <artifactId>all</artifactId> + <version>1.6.3</version> + </parent> + <modelVersion>4.0.0</modelVersion> + <groupId>com.sun.mail</groupId> + <artifactId>servlet</artifactId> + <packaging>jar</packaging> + <name>JavaMail API demo servlet</name> + + <build> + <plugins> + <!-- + Need to disable the maven-bundle-plugin because the + new version doesn't like classes in the default package. + --> + <plugin> + <groupId>org.apache.felix</groupId> + <artifactId>maven-bundle-plugin</artifactId> + <executions> + <execution> + <id>osgi-manifest</id> + <phase>none</phase> + </execution> + </executions> + </plugin> + + <!-- + Because the maven-jar-plugin depends on the manifest file + created by the maven-bundle-plugin, we need to disable it too. + --> + <plugin> + <artifactId>maven-jar-plugin</artifactId> + <executions> + <execution> + <id>default-jar</id> + <phase>none</phase> + </execution> + </executions> + </plugin> + </plugins> + </build> + + <dependencies> + <dependency> + <groupId>javax.servlet</groupId> + <artifactId>servlet-api</artifactId> + </dependency> + <dependency> + <groupId>com.sun.mail</groupId> + <artifactId>jakarta.mail</artifactId> + </dependency> + </dependencies> +</project>
diff --git a/servlet/src/main/java/JavaMail.html b/servlet/src/main/java/JavaMail.html new file mode 100644 index 0000000..0ce0f96 --- /dev/null +++ b/servlet/src/main/java/JavaMail.html
@@ -0,0 +1,116 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2//EN"> +<HTML> +<!-- + + Copyright (c) 1998, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Distribution License v. 1.0, which is available at + http://www.eclipse.org/org/documents/edl-v10.php. + + SPDX-License-Identifier: BSD-3-Clause + +--> + +<HEAD> + <META HTTP-EQUIV="Content-Type" CONTENT="text/html;CHARSET=iso-8859-1"> + <TITLE>JavaMail</TITLE> +</HEAD> + +<BODY BGCOLOR="#CCCCFF"> + +<FORM ACTION="/servlet/JavaMailServlet" METHOD="POST" + ENCTYPE="application/x-www-form-urlencoded"> +<P ALIGN="CENTER"><B><FONT SIZE="5" FACE="Arial, Helvetica"> +Welcome to JavaMail +</FONT></B></P> + +<P ALIGN="CENTER"><B><FONT SIZE="5" FACE="Arial, Helvetica"> +HTML Email Reader Demo +</FONT></B></P> +<CENTER> +<P> +<TABLE BORDER="0" WIDTH="100%"> + <TR> + <TD WIDTH="40%"> + <P ALIGN="RIGHT"><FONT FACE="Arial, Helvetica"> + IMAP Hostname:</FONT> + </TD> + <TD WIDTH="60%"><INPUT TYPE="TEXT" NAME="hostname" SIZE="25"> + </TD> + </TR> + <TR> + <TD WIDTH="40%"> + <P ALIGN="RIGHT"><FONT FACE="Arial, Helvetica"> + Username:</FONT> + </TD> + <TD WIDTH="60%"><INPUT TYPE="TEXT" NAME="username" SIZE="25"> + </TD> + </TR> + <TR> + <TD WIDTH="40%"> + <P ALIGN="RIGHT"><FONT FACE="Arial, Helvetica"> + Password:</FONT> + </TD> + <TD WIDTH="60%"><INPUT TYPE="PASSWORD" NAME="password" + SIZE="25"></TD> + </TR> +</TABLE> +<INPUT TYPE="SUBMIT" VALUE="Login"> +<INPUT TYPE="RESET" NAME="Reset" VALUE="Reset"></P> +</CENTER> +<P> +<HR ALIGN="CENTER"> +</P> +<P><B><I><FONT FACE="Arial, Helvetica">Overview:</FONT></I></B></P> + +<FONT SIZE="2" FACE="Arial, Helvetica"> +<b>THIS IS A DEMO!!!</b> Please see the JavaMailServlet.README.txt +file for information on how to +compile and run the servlet.<br> <br> +This is a servlet that demonstrates the use of JavaMail APIs +in a 3-tier application. It allows the user to login to an +IMAP store, list all the messages in the INBOX folder, view +selected messages, compose and send a message, and logout. +</FONT> + +<P><B><I><FONT FACE="Arial, Helvetica">Features:</FONT></I></B></P> + +<UL> + <LI><FONT SIZE="2" FACE="Arial, Helvetica"> + HTML access to your IMAP mailbox</FONT> + <LI><FONT SIZE="2" FACE="Arial, Helvetica"> + Proxy-able anywhere HTTP can be proxied</FONT> + <LI><FONT SIZE="2" FACE="Arial, Helvetica">FAST!</FONT> + <LI><FONT SIZE="2" FACE="Arial, Helvetica">Easy to use</FONT> + <LI><FONT SIZE="2" FACE="Arial, Helvetica"> + Uses web browser's content handling capabilities</FONT> + <LI><FONT SIZE="2" FACE="Arial, Helvetica"> + Ultra small: 1 servlet (2 class files) and 1 html file (this one)</FONT> +</UL> + +<P><B><I><FONT FACE="Arial, Helvetica">Limitations:</FONT></I></B></P> + +<UL> + <LI><FONT SIZE="2" FACE="Arial, Helvetica"> + Only INBOX support (no user folders)</FONT> + <LI><FONT SIZE="2" FACE="Arial, Helvetica"> + Can't delete, copy, move, print, save, forward, reply to, search in + messages --<BR> + but it could be done</FONT> + <LI><FONT SIZE="2" FACE="Arial, Helvetica"> + Doesn't check for new messages (have to log out and log back it)</FONT> +</UL> + +<P> +<HR ALIGN="CENTER"> +</P> + +<P><FONT FACE="Arial, Helvetica">Feedback to </FONT> +<A HREF="mailto:javamail_ww@oracle.com"> +<FONT FACE="Arial, Helvetica">javamail_ww@oracle.com</FONT></A> +</FORM> + +</BODY> + +</HTML>
diff --git a/servlet/src/main/java/JavaMailServlet.java b/servlet/src/main/java/JavaMailServlet.java new file mode 100644 index 0000000..74c0775 --- /dev/null +++ b/servlet/src/main/java/JavaMailServlet.java
@@ -0,0 +1,660 @@ +/* + * Copyright (c) 1998, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +import java.io.*; +import java.util.*; +import java.text.*; + +import javax.servlet.*; +import javax.servlet.http.*; +import javax.mail.*; +import javax.mail.Part; +import javax.mail.internet.*; +import javax.activation.*; + + +/** + * This is a servlet that demonstrates the use of JavaMail APIs + * in a 3-tier application. It allows the user to login to an + * IMAP store, list all the messages in the INBOX folder, view + * selected messages, compose and send a message, and logout. + * <p> + * Please note: This is NOT an example of how to write servlets! + * This is simply to show that JavaMail can be used in a servlet. + * <p> + * For more information on this servlet, see the + * JavaMailServlet.README.txt file. + * <p> + * For more information on servlets, see + * <a href="http://java.sun.com/products/java-server/servlets/index.html"> + * http://java.sun.com/products/java-server/servlets/index.html</a> + * + * @author Max Spivak + */ +public class JavaMailServlet extends HttpServlet implements SingleThreadModel { + String protocol = "imap"; + String mbox = "INBOX"; + + + /** + * This method handles the "POST" submission from two forms: the + * login form and the message compose form. The login form has the + * following parameters: <code>hostname</code>, <code>username</code>, + * and <code>password</code>. The <code>send</code> parameter denotes + * that the method is processing the compose form submission. + */ + public void doPost(HttpServletRequest req, HttpServletResponse res) + throws ServletException, IOException { + + // get the session + HttpSession ssn = req.getSession(true); + + String send = req.getParameter("send"); + String host = req.getParameter("hostname"); + String user = req.getParameter("username"); + String passwd = req.getParameter("password"); + URLName url = new URLName(protocol, host, -1, mbox, user, passwd); + + ServletOutputStream out = res.getOutputStream(); + res.setContentType("text/html"); + out.println("<html><body bgcolor=\"#CCCCFF\">"); + + if (send != null) { + // process message sending + send(req, res, out, ssn); + + } else { + // initial login + + // create + MailUserData mud = new MailUserData(url); + ssn.putValue("javamailservlet", mud); + + try { + Properties props = System.getProperties(); + props.put("mail.smtp.host", host); + Session session = Session.getDefaultInstance(props, null); + session.setDebug(false); + Store store = session.getStore(url); + store.connect(); + Folder folder = store.getDefaultFolder(); + if (folder == null) + throw new MessagingException("No default folder"); + + folder = folder.getFolder(mbox); + if (folder == null) + throw new MessagingException("Invalid folder"); + + folder.open(Folder.READ_WRITE); + int totalMessages = folder.getMessageCount(); + Message[] msgs = folder.getMessages(); + FetchProfile fp = new FetchProfile(); + fp.add(FetchProfile.Item.ENVELOPE); + folder.fetch(msgs, fp); + + // track who logged in + System.out.println("Login from: " + store.getURLName()); + + // save stuff into MUD + mud.setSession(session); + mud.setStore(store); + mud.setFolder(folder); + + // splash + out.print("<center>"); + out.print("<font face=\"Arial,Helvetica\" font size=+3>"); + out.println("<b>Welcome to JavaMail!</b></font></center><p>"); + + // folder table + out.println("<table width=\"50%\" border=0 align=center>"); + // folder name column header + out.print("<tr><td width=\"75%\" bgcolor=\"#ffffcc\">"); + out.print("<font face=\"Arial,Helvetica\" font size=-1>"); + out.println("<b>FolderName</b></font></td><br>"); + // msg count column header + out.print("<td width=\"25%\" bgcolor=\"#ffffcc\">"); + out.print("<font face=\"Arial,Helvetica\" font size=-1>"); + out.println("<b>Messages</b></font></td><br>"); + out.println("</tr>"); + // folder name + out.print("<tr><td width=\"75%\" bgcolor=\"#ffffff\">"); + out.print("<a href=\"" + HttpUtils.getRequestURL(req) + "\">" + + "Inbox" + "</a></td><br>"); + // msg count + out.println("<td width=\"25%\" bgcolor=\"#ffffff\">" + + totalMessages + "</td>"); + out.println("</tr>"); + out.println("</table"); + } catch (Exception ex) { + out.println(ex.toString()); + } finally { + out.println("</body></html>"); + out.close(); + } + } + } + + + /** + * This method handles the GET requests for the client. + */ + public void doGet (HttpServletRequest req, HttpServletResponse res) + throws ServletException, IOException { + + HttpSession ses = req.getSession(false); // before we write to out + ServletOutputStream out = res.getOutputStream(); + MailUserData mud = getMUD(ses); + + if (mud == null) { + res.setContentType("text/html"); + out.println("<html><body>Please Login (no session)</body></html>"); + out.close(); + return; + } + + if (!mud.getStore().isConnected()) { + res.setContentType("text/html"); + out.println("<html><body>Not Connected To Store</body></html>"); + out.close(); + return; + } + + + // mux that takes a GET request, based on parameters figures + // out what it should do, and routes it to the + // appropriate method + + // get url parameters + String msgStr = req.getParameter("message"); + String logout = req.getParameter("logout"); + String compose = req.getParameter("compose"); + String part = req.getParameter("part"); + int msgNum = -1; + int partNum = -1; + + // process url params + if (msgStr != null) { + // operate on message "msgStr" + msgNum = Integer.parseInt(msgStr); + + if (part == null) { + // display message "msgStr" + res.setContentType("text/html"); + displayMessage(mud, req, out, msgNum); + + } else { + // display part "part" in message "msgStr" + partNum = Integer.parseInt(part); + displayPart(mud, msgNum, partNum, out, res); + } + + } else if (compose != null) { + // display compose form + compose(mud, res, out); + + } else if (logout != null) { + // process logout + try { + mud.getFolder().close(false); + mud.getStore().close(); + ses.invalidate(); + out.println("<html><body>Logged out OK</body></html>"); + } catch (MessagingException mex) { + out.println(mex.toString()); + } + + } else { + // display headers + displayHeaders(mud, req, out); + } + } + + /* main method to display messages */ + private void displayMessage(MailUserData mud, HttpServletRequest req, + ServletOutputStream out, int msgNum) + throws IOException { + + out.println("<html>"); + out.println("<HEAD><TITLE>JavaMail Servlet</TITLE></HEAD>"); + out.println("<BODY bgcolor=\"#ccccff\">"); + out.print("<center><font face=\"Arial,Helvetica\" "); + out.println("font size=\"+3\"><b>"); + out.println("Message " + (msgNum+1) + " in folder " + + mud.getStore().getURLName() + + "/INBOX</b></font></center><p>"); + + try { + Message msg = mud.getFolder().getMessage(msgNum); + + // first, display this message's headers + displayMessageHeaders(mud, msg, out); + + // and now, handle the content + Object o = msg.getContent(); + + //if (o instanceof String) { + if (msg.isMimeType("text/plain")) { + out.println("<pre>"); + out.println((String)o); + out.println("</pre>"); + //} else if (o instanceof Multipart){ + } else if (msg.isMimeType("multipart/*")) { + Multipart mp = (Multipart)o; + int cnt = mp.getCount(); + for (int i = 0; i < cnt; i++) { + displayPart(mud, msgNum, mp.getBodyPart(i), i, req, out); + } + } else { + out.println(msg.getContentType()); + } + + } catch (MessagingException mex) { + out.println(mex.toString()); + } + + out.println("</BODY></html>"); + out.close(); + } + + /** + * This method displays a message part. <code>text/plain</code> + * content parts are displayed inline. For all other parts, + * a URL is generated and displayed; clicking on the URL + * brings up the part in a separate page. + */ + private void displayPart(MailUserData mud, int msgNum, Part part, + int partNum, HttpServletRequest req, + ServletOutputStream out) + throws IOException { + + if (partNum != 0) + out.println("<p><hr>"); + + try { + + String sct = part.getContentType(); + if (sct == null) { + out.println("invalid part"); + return; + } + ContentType ct = new ContentType(sct); + + if (partNum != 0) + out.println("<b>Attachment Type:</b> " + + ct.getBaseType() + "<br>"); + + if (ct.match("text/plain")) { + // display text/plain inline + out.println("<pre>"); + out.println((String)part.getContent()); + out.println("</pre>"); + + } else { + // generate a url for this part + String s; + if ((s = part.getFileName()) != null) + out.println("<b>Filename:</b> " + s + "<br>"); + s = null; + if ((s = part.getDescription()) != null) + out.println("<b>Description:</b> " + s + "<br>"); + + out.println("<a href=\"" + + HttpUtils.getRequestURL(req) + + "?message=" + + msgNum + "&part=" + + partNum + "\">Display Attachment</a>"); + } + } catch (MessagingException mex) { + out.println(mex.toString()); + } + } + + /** + * This method gets the stream from for a given msg part and + * pushes it out to the browser with the correct content type. + * Used to display attachments and relies on the browser's + * content handling capabilities. + */ + private void displayPart(MailUserData mud, int msgNum, + int partNum, ServletOutputStream out, + HttpServletResponse res) + throws IOException { + + Part part = null; + + try { + Message msg = mud.getFolder().getMessage(msgNum); + + Multipart mp = (Multipart)msg.getContent(); + part = mp.getBodyPart(partNum); + + String sct = part.getContentType(); + if (sct == null) { + out.println("invalid part"); + return; + } + ContentType ct = new ContentType(sct); + + res.setContentType(ct.getBaseType()); + InputStream is = part.getInputStream(); + int i; + while ((i = is.read()) != -1) + out.write(i); + out.flush(); + out.close(); + } catch (MessagingException mex) { + out.println(mex.toString()); + } + } + + /** + * This is a utility message that pretty-prints the message + * headers for message that is being displayed. + */ + private void displayMessageHeaders(MailUserData mud, Message msg, + ServletOutputStream out) + throws IOException { + + try { + out.println("<b>Date:</b> " + msg.getSentDate() + "<br>"); + + Address[] fr = msg.getFrom(); + if (fr != null) { + boolean tf = true; + out.print("<b>From:</b> "); + for (int i = 0; i < fr.length; i++) { + out.print(((tf) ? " " : ", ") + getDisplayAddress(fr[i])); + tf = false; + } + out.println("<br>"); + } + + Address[] to = msg.getRecipients(Message.RecipientType.TO); + if (to != null) { + boolean tf = true; + out.print("<b>To:</b> "); + for (int i = 0; i < to.length; i++) { + out.print(((tf) ? " " : ", ") + getDisplayAddress(to[i])); + tf = false; + } + out.println("<br>"); + } + + Address[] cc = msg.getRecipients(Message.RecipientType.CC); + if (cc != null) { + boolean cf = true; + out.print("<b>CC:</b> "); + for (int i = 0; i < cc.length; i++) { + out.print(((cf) ? " " : ", ") + getDisplayAddress(cc[i])); + cf = false; + } + out.println("<br>"); + } + + out.print("<b>Subject:</b> " + + ((msg.getSubject() !=null) ? msg.getSubject() : "") + + "<br>"); + + } catch (MessagingException mex) { + out.println(msg.toString()); + } + } + + /** + * This method displays the URL's for the available commands and the + * INBOX headerlist + */ + private void displayHeaders(MailUserData mud, + HttpServletRequest req, + ServletOutputStream out) + throws IOException { + + SimpleDateFormat df = new SimpleDateFormat("EE M/d/yy"); + + out.println("<html>"); + out.println("<HEAD><TITLE>JavaMail Servlet</TITLE></HEAD>"); + out.println("<BODY bgcolor=\"#ccccff\"><hr>"); + out.print("<center><font face=\"Arial,Helvetica\" font size=\"+3\">"); + out.println("<b>Folder " + mud.getStore().getURLName() + + "/INBOX</b></font></center><p>"); + + // URL's for the commands that are available + out.println("<font face=\"Arial,Helvetica\" font size=\"+3\"><b>"); + out.println("<a href=\"" + + HttpUtils.getRequestURL(req) + + "?logout=true\">Logout</a>"); + out.println("<a href=\"" + + HttpUtils.getRequestURL(req) + + "?compose=true\" target=\"compose\">Compose</a>"); + out.println("</b></font>"); + out.println("<hr>"); + + // List headers in a table + out.print("<table cellpadding=1 cellspacing=1 "); // table + out.println("width=\"100%\" border=1>"); // settings + + // sender column header + out.println("<tr><td width=\"25%\" bgcolor=\"ffffcc\">"); + out.println("<font face=\"Arial,Helvetica\" font size=\"+1\">"); + out.println("<b>Sender</b></font></td>"); + // date column header + out.println("<td width=\"15%\" bgcolor=\"ffffcc\">"); + out.println("<font face=\"Arial,Helvetica\" font size=\"+1\">"); + out.println("<b>Date</b></font></td>"); + // subject column header + out.println("<td bgcolor=\"ffffcc\">"); + out.println("<font face=\"Arial,Helvetica\" font size=\"+1\">"); + out.println("<b>Subject</b></font></td></tr>"); + + try { + Folder f = mud.getFolder(); + int msgCount = f.getMessageCount(); + Message m = null; + // for each message, show its headers + for (int i = 1; i <= msgCount; i++) { + m = f.getMessage(i); + + // if message has the DELETED flag set, don't display it + if (m.isSet(Flags.Flag.DELETED)) + continue; + + // from + out.println("<tr valigh=middle>"); + out.print("<td width=\"25%\" bgcolor=\"ffffff\">"); + out.println("<font face=\"Arial,Helvetica\">" + + ((m.getFrom() != null) ? + m.getFrom()[0].toString() : + "" ) + + "</font></td>"); + + // date + out.print("<td nowrap width=\"15%\" bgcolor=\"ffffff\">"); + out.println("<font face=\"Arial,Helvetica\">" + + df.format((m.getSentDate()!=null) ? + m.getSentDate() : m.getReceivedDate()) + + "</font></td>"); + + // subject & link + out.print("<td bgcolor=\"ffffff\">"); + out.println("<font face=\"Arial,Helvetica\">" + + "<a href=\"" + + HttpUtils.getRequestURL(req) + + "?message=" + + i + "\">" + + ((m.getSubject() != null) ? + m.getSubject() : + "<i>No Subject</i>") + + "</a>" + + "</font></td>"); + out.println("</tr>"); + } + } catch (MessagingException mex) { + out.println("<tr><td>" + mex.toString() + "</td></tr>"); + mex.printStackTrace(); + } + + out.println("</table>"); + out.println("</BODY></html>"); + out.flush(); + out.close(); + } + + /** + * This method handles the request when the user hits the + * <i>Compose</i> link. It send the compose form to the browser. + */ + private void compose(MailUserData mud, HttpServletResponse res, + ServletOutputStream out) + throws IOException { + + res.setContentType("text/html"); + out.println(composeForm); + out.close(); + } + + /** + * This method processes the send request from the compose form + */ + private void send(HttpServletRequest req, HttpServletResponse res, + ServletOutputStream out, HttpSession ssn) + throws IOException { + + String to = req.getParameter("to"); + String cc = req.getParameter("cc"); + String subj = req.getParameter("subject"); + String text = req.getParameter("text"); + + try { + MailUserData mud = getMUD(ssn); + if (mud == null) + throw new Exception("trying to send, but not logged in"); + + Message msg = new MimeMessage(mud.getSession()); + InternetAddress[] toAddrs = null, ccAddrs = null; + + if (to != null) { + toAddrs = InternetAddress.parse(to, false); + msg.setRecipients(Message.RecipientType.TO, toAddrs); + } else + throw new MessagingException("No \"To\" address specified"); + + if (cc != null) { + ccAddrs = InternetAddress.parse(cc, false); + msg.setRecipients(Message.RecipientType.CC, ccAddrs); + } + + if (subj != null) + msg.setSubject(subj); + + URLName u = mud.getURLName(); + msg.setFrom(new InternetAddress(u.getUsername() + "@" + + u.getHost())); + + if (text != null) + msg.setText(text); + + Transport.send(msg); + + out.println("<h1>Message sent successfully</h1></body></html>"); + out.close(); + + } catch (Exception mex) { + out.println("<h1>Error sending message.</h1>"); + out.println(mex.toString()); + out.println("<br></body></html>"); + } + } + + + // utility method; returns a string suitable for msg header display + private String getDisplayAddress(Address a) { + String pers = null; + String addr = null; + if (a instanceof InternetAddress && + ((pers = ((InternetAddress)a).getPersonal()) != null)) { + + addr = pers + " "+"<"+((InternetAddress)a).getAddress()+">"; + } else + addr = a.toString(); + + return addr; + } + + // utility method; retrieve the MailUserData + // from the HttpSession and return it + private MailUserData getMUD(HttpSession ses) throws IOException { + MailUserData mud = null; + + if (ses == null) { + return null; + } else { + if ((mud = (MailUserData)ses.getValue("javamailservlet")) == null){ + return null; + } + } + return mud; + } + + + public String getServletInfo() { + return "A mail reader servlet"; + } + + /** + * This is the HTML code for the compose form. Another option would + * have been to use a separate html page. + */ + private static String composeForm = "<HTML><HEAD><TITLE>JavaMail Compose</TITLE></HEAD><BODY BGCOLOR=\"#CCCCFF\"><FORM ACTION=\"/servlet/JavaMailServlet\" METHOD=\"POST\"><input type=\"hidden\" name=\"send\" value=\"send\"><P ALIGN=\"CENTER\"><B><FONT SIZE=\"4\" FACE=\"Verdana, Arial, Helvetica\">JavaMail Compose Message</FONT></B><P><TABLE BORDER=\"0\" WIDTH=\"100%\"><TR><TD WIDTH=\"16%\" HEIGHT=\"22\"> <P ALIGN=\"RIGHT\"><B><FONT FACE=\"Verdana, Arial, Helvetica\">To:</FONT></B></TD><TD WIDTH=\"84%\" HEIGHT=\"22\"><INPUT TYPE=\"TEXT\" NAME=\"to\" SIZE=\"30\"> <FONT SIZE=\"1\" FACE=\"Verdana, Arial, Helvetica\"> (separate addresses with commas)</FONT></TD></TR><TR><TD WIDTH=\"16%\"><P ALIGN=\"RIGHT\"><B><FONT FACE=\"Verdana, Arial, Helvetica\">CC:</FONT></B></TD><TD WIDTH=\"84%\"><INPUT TYPE=\"TEXT\" NAME=\"cc\" SIZE=\"30\"> <FONT SIZE=\"1\" FACE=\"Verdana, Arial, Helvetica\"> (separate addresses with commas)</FONT></TD></TR><TR><TD WIDTH=\"16%\"><P ALIGN=\"RIGHT\"><B><FONT FACE=\"Verdana, Arial, Helvetica\">Subject:</FONT></B></TD><TD WIDTH=\"84%\"><INPUT TYPE=\"TEXT\" NAME=\"subject\" SIZE=\"55\"></TD></TR><TR><TD WIDTH=\"16%\"> </TD><TD WIDTH=\"84%\"><TEXTAREA NAME=\"text\" ROWS=\"15\" COLS=\"53\"></TEXTAREA></TD></TR><TR><TD WIDTH=\"16%\" HEIGHT=\"32\"> </TD><TD WIDTH=\"84%\" HEIGHT=\"32\"><INPUT TYPE=\"SUBMIT\" NAME=\"Send\" VALUE=\"Send\"><INPUT TYPE=\"RESET\" NAME=\"Reset\" VALUE=\"Reset\"></TD></TR></TABLE></FORM></BODY></HTML>"; + +} + + +/** + * This class is used to store session data for each user's session. It + * is stored in the HttpSession. + */ +class MailUserData { + URLName url; + Session session; + Store store; + Folder folder; + + public MailUserData(URLName urlname) { + url = urlname; + } + + public URLName getURLName() { + return url; + } + + public Session getSession() { + return session; + } + + public void setSession(Session s) { + session = s; + } + + public Store getStore() { + return store; + } + + public void setStore(Store s) { + store = s; + } + + public Folder getFolder() { + return folder; + } + + public void setFolder(Folder f) { + folder = f; + } +}
diff --git a/servlet/src/main/java/README.txt b/servlet/src/main/java/README.txt new file mode 100644 index 0000000..ad2896a --- /dev/null +++ b/servlet/src/main/java/README.txt
@@ -0,0 +1,145 @@ + JavaMail Servlet + ~~~~~~~~~~~~~~~~ + +Overview: +========= + +JavaMailServlet should not be taken as a demo of how to use the Java +Servlet API. It is rather an example of how the JavaMail APIs could +be used in a server in a three-tier environment described by the +following diagram: + + +-----------+ +-----------+ +-----------+ + | IMAP | | | | | + | Server |<-IMAP->| JavaMail |<-HTTP->| WWW | + +-----------+ | Servlet |--HTML->| Browser | + | SMTP |<-SMTP->| | | | + | Server | | | | | + +-----------+ +-----------+ +-----------+ + + +The JavaMailServlet supports the following functionality: + * login to an IMAP server + * list all the messages in the INBOX folder + * view the selected message + * compose and send a message + + +Setting up and running the demo: +================================ +Note: These instructions explain how to compile and run the servlet +demo with Java Web Server (JWS). The procedure should be similar with +other web servers that support the Java Servlet API. + + 1. Download the latest version of the Java Web Server from + http://www.sun.com/software/jwebserver/index.html and + install according to the included documentation. Make + sure JWS is listening to requests on port 80 (you may + need to modify it from default port 8080; use the JWS + Admin Applet). Make sure you can load the Java Web + Server welcome page when you connect with your browser + to the machine that the server is installed on. Also, + make sure your web browser has cookie support turned on. + + 2. Set your classpath to include the following: + * mail.jar: in the JavaMail API distribution + * activation.jar: in the JAF distribution + * jws.jar: in the /lib/ directory in JWS installation + + 3. In javamail-1.1/demo/servlet directory, compile the + JavaMailServlet.java file. That produces two class files, + JavaMailServlet.class and MailUserData.class. Copy these + class files to the /servlets/ directory in the JWS + installation. + + 4. Copy the mail.jar and activation.jar to the /lib/ + directory in the JWS installation. + + 5. Copy the JavaMail.html file to the /public_html/ + directory in the JWS installation. + + 6. Restart Java Web Server to pick up the new jar files + added to its lib directory. Check again that you can + load the default JWS page to verify that the server + is working fine. + + 7. Using a web browser, go to + http://<hostname>/JavaMail.html and login to a + valid IMAP account. From here on, you can view + messages in your INBOX and create and send new + messages. + + + +JavaMailServlet Design: +======================= + +The following is a brief description of JavaMailServlet class. It +is not intended to serve as an example of how to develop servlets; +see http://java.sun.com/products/servlet for information on the Java +Servlet API. You may find it useful to refer to JavaMailServlet.java +source while reading this. + +The JavaMailServlet uses two primary methods to process all +requests: doPost() and doGet(). doPost() processes submissions +from the login and compose forms. When the user logs in, the +doPost() method gets a JavaMail Session and uses the values +of the "hostname", "username" and "password" parameters to login +to the IMAP Store and get the INBOX Folder. To preserve state +between multiple HTTP requests, the necessary information +(Session, Store, Folder, URLName) are collected in the +MailUserData object which is stored using JWS's Session +technology (don't confuse HttpSession and JavaMail's Session-- +they are different). Finally, the doPost() method outputs +a table listing the INBOX and the number of messages in it. + +Clicking on the "INBOX" link calls the doGet() method +which displays a table of message headers. (See the doGet() +and displayHeaders() methods.) + +Clicking on a message generates a request to the servlet with +the message sequence number as a parameter. The doGet() method +receives the request and calls the displayMessage() method +passing it the message sequence number to display. The +displayMessage() method first lists the message headers +by calling the displayMessageHeaders() utility method. +For text/plain messages, the message content is then displayed +as a string wrapped in HTML <pre>...</pre> tags. For multipart +messages, each part is passed to the displayPart() method. + +There are two displayPart() methods. The one with signature + displayPart(MailUserData mud, int msgNum, Part part, + int partNum, HttpServletRequest req, + ServletOutputStream out) +is called from the displayMessage() method for each part. For +any part with text/plain content type, the content is output +as a string wrapped in HTML <pre>...</pre> tags. For other +content types, a link representing the part is displayed, +along with the part filename and description, if available. + +Clicking in the part link generates a request to the servlet +with two parameters: the message sequence number and the +part number. The doGet() method interprets the parameters and +invokes the second displayPart() method with the signature + displayPart(MailUserData mud, int msgNum, + int partNum, ServletOutputStream out, + HttpServletResponse res) +This method retrieves the specified part from the message and +streams it to the web browser, preceded by the MIME content type +of this part. For example, if the part has a MIME type image/gif, +the method will set the servlet response MIME content type to +"image/gif" and then follow it with the bytes of the actual +image. This leverages the web browser's content handling +ability to display different media types. + +Message composition and sending is very similar to message +viewing. When the "Compose" link is clicked in the headerlist +page, the servlet outputs the HTML source for the compose +form stored in the composeForm variable. The user then fills +in the destination address, subject, text, and presses +send. This sends a POST request to the servlet, which +invokes the doPost() method. doPost() calls the servlet's +send() method, which creates a new MimeMessage and fills +it with data retrieved from the POST request. The message +is then sent to its destination using the Transport.send() +static method.
diff --git a/siggen b/siggen new file mode 100755 index 0000000..8b95080 --- /dev/null +++ b/siggen
@@ -0,0 +1,48 @@ +#!/bin/sh +# +# Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. +# +# This program and the accompanying materials are made available under the +# terms of the Eclipse Public License v. 2.0, which is available at +# http://www.eclipse.org/legal/epl-2.0. +# +# This Source Code may also be made available under the following Secondary +# Licenses when the conditions for such availability set forth in the +# Eclipse Public License v. 2.0 are satisfied: GNU General Public License, +# version 2 with the GNU Classpath Exception, which is available at +# https://www.gnu.org/software/classpath/license.html. +# +# SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +# + +# +# Generate a new signature file. +# +# Usage: siggen javax.mail.jar mail.sig +# +SIGTEST=${SIGTEST:-/java/re/sigtest/4.0/promoted/fcs/latest/binaries/sigtest-4.0} +SIGTEST_JAR=$SIGTEST/lib/sigtestdev.jar +JAVA_HOME=${JAVA_HOME:-/opt/jdk1.8} +PKG=javax.mail +USAGE="siggen [-p package] javax.mail.jar mail.sig" + +while getopts p: opt +do + case $opt in + p) PKG="$OPTARG";; + \?) echo $USAGE; exit 1;; + esac +done +shift `expr $OPTIND - 1` + +ver=$($JAVA_HOME/bin/java -version 2>&1 | sed -e 's/.*"\(.*\)".*/\1/;q') +case "$ver" in +1.[0-9]*) xjimage=; + cp="$1:${JAVA_HOME}/jre/lib/rt.jar:${JAVA_HOME}/jre/lib/jce.jar";; +*) xjimage="-xjimage ${JAVA_HOME}/bin/jimage"; + cp="$1:${JAVA_HOME}/lib/modules";; +esac + +${JAVA_HOME}/bin/java -jar $SIGTEST_JAR setup -static \ + -classpath "$cp" $xjimage \ + -filename "$2" -package "$PKG" -nonclosedfile
diff --git a/sigtest b/sigtest new file mode 100755 index 0000000..0ec867a --- /dev/null +++ b/sigtest
@@ -0,0 +1,67 @@ +#!/bin/sh +# +# Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. +# +# This program and the accompanying materials are made available under the +# terms of the Eclipse Public License v. 2.0, which is available at +# http://www.eclipse.org/legal/epl-2.0. +# +# This Source Code may also be made available under the following Secondary +# Licenses when the conditions for such availability set forth in the +# Eclipse Public License v. 2.0 are satisfied: GNU General Public License, +# version 2 with the GNU Classpath Exception, which is available at +# https://www.gnu.org/software/classpath/license.html. +# +# SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +# + +# +# Compare API against a signature file. +# +# Usage: sigtest javax.mail.jar mail.sig +# +SIGTEST=${SIGTEST:-/java/re/sigtest/4.0/promoted/fcs/latest/binaries/sigtest-4.0} +# The apicheck program isn't working correctly, use sigtest instead. +#APICHECK_JAR=$SIGTEST/lib/apicheck.jar +SIGTEST_JAR=$SIGTEST/lib/sigtestdev.jar +JAVA_HOME=${JAVA_HOME:-/opt/jdk1.8} +USAGE="sigtest [-b] [-p package] javax.mail.jar mail.sig" +PKG=javax.mail +BACKWARD= +PLATSIG=/tmp/$$.platform.sig + +while getopts bp: opt +do + case $opt in + p) PKG="$OPTARG";; + b) BACKWARD=-backward;; + \?) echo $USAGE; exit 1;; + esac +done +shift `expr $OPTIND - 1` + +JAR="$1" +SIG="$2" +shift 2 + +ver=$($JAVA_HOME/bin/java -version 2>&1 | sed -e 's/.*"\(.*\)".*/\1/;q') +case "$ver" in +1.[0-9]*) xjimage=; + cp="${JAVA_HOME}/jre/lib/rt.jar:${JAVA_HOME}/jre/lib/jce.jar";; +*) xjimage="-xjimage ${JAVA_HOME}/bin/jimage"; + cp="${JAVA_HOME}/lib/modules";; +esac + +echo '*** Generate JDK signatures ***' +${JAVA_HOME}/bin/java -jar $SIGTEST_JAR setup \ + -classpath "$cp" $xjimage \ + -filename "$PLATSIG" -package java -package javax -keepfile \ + -verbose nowarn +echo +echo '*** Check JavaMail signatures ***' +${JAVA_HOME}/bin/java -jar $SIGTEST_JAR test -static \ + -classpath "$PLATSIG:$JAR" \ + -filename "$SIG" -package "$PKG" -out /dev/stdout \ + -nomerge -checkvalue -mode src $BACKWARD "$@" + +rm "$PLATSIG"
diff --git a/smtp/pom.xml b/smtp/pom.xml new file mode 100644 index 0000000..c05d437 --- /dev/null +++ b/smtp/pom.xml
@@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="iso-8859-1"?> +<!-- + + Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 + http://maven.apache.org/maven-v4_0_0.xsd"> + <parent> + <groupId>com.sun.mail</groupId> + <artifactId>parent-distrib</artifactId> + <version>1.6.3</version> + <relativePath>../parent-distrib/pom.xml</relativePath> + </parent> + <modelVersion>4.0.0</modelVersion> + <groupId>com.sun.mail</groupId> + <artifactId>smtp</artifactId> + <packaging>jar</packaging> + <name>JavaMail API smtp provider</name> + + <properties> + <mail.packages.export> + com.sun.mail.smtp; version=${mail.osgiversion} + </mail.packages.export> + </properties> +</project>
diff --git a/smtp/src/main/java/module-info.java b/smtp/src/main/java/module-info.java new file mode 100644 index 0000000..544105d --- /dev/null +++ b/smtp/src/main/java/module-info.java
@@ -0,0 +1,25 @@ +/* + * Copyright (c) 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +module com.sun.mail.smtp { + exports com.sun.mail.smtp; + provides javax.mail.Provider with + com.sun.mail.smtp.SMTPProvider, com.sun.mail.smtp.SMTPSSLProvider; + + requires jakarta.mail; + requires java.logging; + requires java.security.sasl; +}
diff --git a/smtp/src/main/resources/META-INF/MANIFEST.MF b/smtp/src/main/resources/META-INF/MANIFEST.MF new file mode 100644 index 0000000..1843e1b --- /dev/null +++ b/smtp/src/main/resources/META-INF/MANIFEST.MF
@@ -0,0 +1,9 @@ +Manifest-Version: 1.0 +Extension-Name: com.sun.mail.smtp +Specification-Title: com.sun.mail.smtp +Specification-Version: ${mail.spec.version} +Specification-Vendor: ${project.organization.name} +Implementation-Title: com.sun.mail.smtp +Implementation-Version: ${mail.version} +Implementation-Vendor: ${project.organization.name} +Implementation-Vendor-Id: com.sun
diff --git a/smtp/src/main/resources/META-INF/javamail.address.map b/smtp/src/main/resources/META-INF/javamail.address.map new file mode 100644 index 0000000..4ab5572 --- /dev/null +++ b/smtp/src/main/resources/META-INF/javamail.address.map
@@ -0,0 +1 @@ +rfc822=smtp
diff --git a/smtp/src/main/resources/META-INF/javamail.providers b/smtp/src/main/resources/META-INF/javamail.providers new file mode 100644 index 0000000..6ad6c98 --- /dev/null +++ b/smtp/src/main/resources/META-INF/javamail.providers
@@ -0,0 +1,3 @@ +# JavaMail SMTP provider Oracle +protocol=smtp; type=transport; class=com.sun.mail.smtp.SMTPTransport; vendor=Oracle; +protocol=smtps; type=transport; class=com.sun.mail.smtp.SMTPSSLTransport; vendor=Oracle;
diff --git a/smtp/src/main/resources/META-INF/services/javax.mail.Provider b/smtp/src/main/resources/META-INF/services/javax.mail.Provider new file mode 100644 index 0000000..cd5fb14 --- /dev/null +++ b/smtp/src/main/resources/META-INF/services/javax.mail.Provider
@@ -0,0 +1,2 @@ +com.sun.mail.smtp.SMTPProvider +com.sun.mail.smtp.SMTPSSLProvider
diff --git a/taglib/pom.xml b/taglib/pom.xml new file mode 100644 index 0000000..8e6f509 --- /dev/null +++ b/taglib/pom.xml
@@ -0,0 +1,50 @@ +<?xml version="1.0" encoding="iso-8859-1"?> +<!-- + + Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 + http://maven.apache.org/maven-v4_0_0.xsd"> + <parent> + <groupId>com.sun.mail</groupId> + <artifactId>all</artifactId> + <version>1.6.3</version> + </parent> + <modelVersion>4.0.0</modelVersion> + <groupId>com.sun.mail</groupId> + <artifactId>taglib</artifactId> + <packaging>jar</packaging> + <name>JavaMail API demo taglib</name> + + <dependencies> + <dependency> + <groupId>javax.servlet.jsp</groupId> + <artifactId>jsp-api</artifactId> + </dependency> + <!-- XXX - javax.servlet.jsp should depend on this --> + <dependency> + <groupId>javax.servlet</groupId> + <artifactId>servlet-api</artifactId> + </dependency> + <dependency> + <groupId>com.sun.mail</groupId> + <artifactId>jakarta.mail</artifactId> + </dependency> + </dependencies> +</project>
diff --git a/taglib/src/main/java/demo/AttachmentInfo.java b/taglib/src/main/java/demo/AttachmentInfo.java new file mode 100644 index 0000000..fc44d57 --- /dev/null +++ b/taglib/src/main/java/demo/AttachmentInfo.java
@@ -0,0 +1,117 @@ +/* + * Copyright (c) 2001, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package demo; + +import java.io.*; +import java.util.*; + +import javax.mail.*; +import javax.mail.internet.*; + +/** + * Used to store attachment information. + */ +public class AttachmentInfo { + private Part part; + private int num; + + + /** + * Returns the attachment's content type. + */ + public String getAttachmentType() throws MessagingException { + String contentType; + if ((contentType = part.getContentType()) == null) + return "invalid part"; + else + return contentType; + } + + /** + * Returns the attachment's content (if it is plain text). + */ + public String getContent() throws IOException, MessagingException { + if (hasMimeType("text/plain")) + return (String)part.getContent(); + else + return ""; + } + + /** + * Returns the attachment's description. + */ + public String getDescription() throws MessagingException { + String description; + if ((description = part.getDescription()) != null) + return description; + else + return ""; + } + + /** + * Returns the attachment's filename. + */ + public String getFilename() throws MessagingException { + String filename; + if ((filename = part.getFileName()) != null) + return filename; + else + return ""; + } + + /** + * Returns the attachment number. + */ + public String getNum() { + return (Integer.toString(num)); + } + + /** + * Method for checking if the attachment has a description. + */ + public boolean hasDescription() throws MessagingException { + return (part.getDescription() != null); + } + + /** + * Method for checking if the attachment has a filename. + */ + public boolean hasFilename() throws MessagingException { + return (part.getFileName() != null); + } + + /** + * Method for checking if the attachment has the desired mime type. + */ + public boolean hasMimeType(String mimeType) throws MessagingException { + return part.isMimeType(mimeType); + } + + /** + * Method for checking the content disposition. + */ + public boolean isInline() throws MessagingException { + if (part.getDisposition() != null) + return part.getDisposition().equals(Part.INLINE); + else + return true; + } + + /** + * Method for mapping a message part to this AttachmentInfo class. + */ + public void setPart(int num, Part part) + throws MessagingException, ParseException { + + this.part = part; + this.num = num; + } +}
diff --git a/taglib/src/main/java/demo/ListAttachmentsTEI.java b/taglib/src/main/java/demo/ListAttachmentsTEI.java new file mode 100644 index 0000000..e08a15f --- /dev/null +++ b/taglib/src/main/java/demo/ListAttachmentsTEI.java
@@ -0,0 +1,34 @@ +/* + * Copyright (c) 2001, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package demo; + +import javax.servlet.jsp.*; +import javax.servlet.jsp.tagext.*; + +/** + * Extra information class to support the scripting variable created by the + * ListAttachmentsTag class. The scope of the variable is limited to the body + * of the tag. + */ +public class ListAttachmentsTEI extends TagExtraInfo { + + public ListAttachmentsTEI() { + super(); + } + + public VariableInfo[] getVariableInfo(TagData data) { + VariableInfo info = new VariableInfo(data.getId(),"AttachmentInfo", + true, VariableInfo.NESTED); + VariableInfo[] varInfo = { info }; + return varInfo; + } +} +
diff --git a/taglib/src/main/java/demo/ListAttachmentsTag.java b/taglib/src/main/java/demo/ListAttachmentsTag.java new file mode 100644 index 0000000..8522866 --- /dev/null +++ b/taglib/src/main/java/demo/ListAttachmentsTag.java
@@ -0,0 +1,101 @@ +/* + * Copyright (c) 2001, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package demo; + +import java.io.*; +import java.util.*; +import javax.mail.*; +import javax.mail.internet.*; +import javax.servlet.jsp.*; +import javax.servlet.jsp.tagext.*; + +/** + * Custom tag for listing message attachments. The scripting variable is only + * within the body of the tag. + */ +public class ListAttachmentsTag extends BodyTagSupport { + private String messageinfo; + private int partNum = 1; + private int numParts = 0; + private AttachmentInfo attachmentinfo; + private MessageInfo messageInfo; + private Multipart multipart; + + /** + * messageinfo attribute getter method. + */ + public String getMessageinfo() { + return messageinfo; + } + + /** + * messageinfo attribute setter method. + */ + public void setMessageinfo(String messageinfo) { + this.messageinfo = messageinfo; + } + + /** + * Method for processing the start of the tag. + */ + public int doStartTag() throws JspException { + messageInfo = (MessageInfo)pageContext.getAttribute(getMessageinfo()); + attachmentinfo = new AttachmentInfo(); + + try { + multipart = (Multipart)messageInfo.getMessage().getContent(); + numParts = multipart.getCount(); + } catch (Exception ex) { + throw new JspException(ex.getMessage()); + } + + getPart(); + + return BodyTag.EVAL_BODY_TAG; + } + + /** + * Method for processing the body content of the tag. + */ + public int doAfterBody() throws JspException { + + BodyContent body = getBodyContent(); + try { + body.writeOut(getPreviousOut()); + } catch (IOException e) { + throw new JspTagException("IterationTag: " + e.getMessage()); + } + + // clear up so the next time the body content is empty + body.clearBody(); + + partNum++; + if (partNum < numParts) { + getPart(); + return BodyTag.EVAL_BODY_TAG; + } else { + return BodyTag.SKIP_BODY; + } + } + + /** + * Helper method for retrieving message parts. + */ + private void getPart() throws JspException { + try { + attachmentinfo.setPart(partNum, multipart.getBodyPart(partNum)); + pageContext.setAttribute(getId(), attachmentinfo); + } catch (Exception ex) { + throw new JspException(ex.getMessage()); + } + } +} +
diff --git a/taglib/src/main/java/demo/ListMessagesTEI.java b/taglib/src/main/java/demo/ListMessagesTEI.java new file mode 100644 index 0000000..73ebaef --- /dev/null +++ b/taglib/src/main/java/demo/ListMessagesTEI.java
@@ -0,0 +1,34 @@ +/* + * Copyright (c) 2001, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package demo; + +import javax.servlet.jsp.*; +import javax.servlet.jsp.tagext.*; + +/** + * Extra information class to support the scripting variable created by the + * ListMessagesTag class. The scope of the variable is limited to the body + * of the tag. + */ +public class ListMessagesTEI extends TagExtraInfo { + + public ListMessagesTEI() { + super(); + } + + public VariableInfo[] getVariableInfo(TagData data) { + VariableInfo info = new VariableInfo(data.getId(),"MessageInfo", + true, VariableInfo.NESTED); + VariableInfo[] varInfo = { info }; + return varInfo; + } +} +
diff --git a/taglib/src/main/java/demo/ListMessagesTag.java b/taglib/src/main/java/demo/ListMessagesTag.java new file mode 100644 index 0000000..9aa1152 --- /dev/null +++ b/taglib/src/main/java/demo/ListMessagesTag.java
@@ -0,0 +1,118 @@ +/* + * Copyright (c) 2001, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package demo; + +import java.io.*; +import java.util.*; +import javax.mail.*; +import javax.mail.internet.*; +import javax.mail.search.*; +import javax.servlet.jsp.*; +import javax.servlet.jsp.tagext.*; + +/** + * Custom tag for listing messages. The scripting variable is only + * within the body of the tag. + */ +public class ListMessagesTag extends BodyTagSupport { + private String folder; + private String session; + private int msgNum = 0; + private int messageCount = 0; + private Message message; + private Message[] messages; + private MessageInfo messageinfo; + + /** + * folder attribute getter method. + */ + public String getFolder() { + return folder; + } + + /** + * session attribute getter method. + */ + public String getSession() { + return session; + } + + /** + * folder setter method. + */ + public void setFolder(String folder) { + this.folder = folder; + } + + /** + * session attribute setter method. + */ + public void setSession(String session) { + this.session = session; + } + + /** + * Method for processing the start of the tag. + */ + public int doStartTag() throws JspException { + messageinfo = new MessageInfo(); + + try { + Folder folder = (Folder)pageContext.getAttribute( + getFolder(), PageContext.SESSION_SCOPE); + FlagTerm ft = new FlagTerm(new Flags(Flags.Flag.DELETED), false); + messages = folder.search(ft); + messageCount = messages.length; + msgNum = 0; + } catch (Exception ex) { + throw new JspException(ex.getMessage()); + } + + if (messageCount > 0) { + getMessage(); + return BodyTag.EVAL_BODY_TAG; + } else + return BodyTag.SKIP_BODY; + } + + /** + * Method for processing the body content of the tag. + */ + public int doAfterBody() throws JspException { + + BodyContent body = getBodyContent(); + try { + body.writeOut(getPreviousOut()); + } catch (IOException e) { + throw new JspTagException("IterationTag: " + e.getMessage()); + } + + // clear up so the next time the body content is empty + body.clearBody(); + + if (msgNum < messageCount) { + getMessage(); + return BodyTag.EVAL_BODY_TAG; + } else { + return BodyTag.SKIP_BODY; + } + } + + /** + * Helper method for retrieving messages. + */ + private void getMessage() throws JspException { + message = messages[msgNum++]; + messageinfo.setMessage(message); + pageContext.setAttribute(getId(), messageinfo); + } +} +
diff --git a/taglib/src/main/java/demo/MessageInfo.java b/taglib/src/main/java/demo/MessageInfo.java new file mode 100644 index 0000000..cb6c7c3 --- /dev/null +++ b/taglib/src/main/java/demo/MessageInfo.java
@@ -0,0 +1,263 @@ +/* + * Copyright (c) 2001, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package demo; + +import java.text.*; +import java.util.*; +import javax.mail.*; +import javax.mail.internet.*; + +/** + * Used to store message information. + */ +public class MessageInfo { + private Message message; + + + /** + * Returns the bcc field. + */ + public String getBcc() throws MessagingException { + return formatAddresses( + message.getRecipients(Message.RecipientType.BCC)); + } + + /** + * Returns the body of the message (if it's plain text). + */ + public String getBody() throws MessagingException, java.io.IOException { + Object content = message.getContent(); + if (message.isMimeType("text/plain")) { + return (String)content; + } else if (message.isMimeType("multipart/alternative")) { + Multipart mp = (Multipart)message.getContent(); + int numParts = mp.getCount(); + for (int i = 0; i < numParts; ++i) { + if (mp.getBodyPart(i).isMimeType("text/plain")) + return (String)mp.getBodyPart(i).getContent(); + } + return ""; + } else if (message.isMimeType("multipart/*")) { + Multipart mp = (Multipart)content; + if (mp.getBodyPart(0).isMimeType("text/plain")) + return (String)mp.getBodyPart(0).getContent(); + else + return ""; + } else + return ""; + } + + /** + * Returns the cc field. + */ + public String getCc() throws MessagingException { + return formatAddresses( + message.getRecipients(Message.RecipientType.CC)); + } + + /** + * Returns the date the message was sent (or received if the sent date + * is null. + */ + public String getDate() throws MessagingException { + Date date; + SimpleDateFormat df = new SimpleDateFormat("EE M/d/yy"); + if ((date = message.getSentDate()) != null) + return (df.format(date)); + else if ((date = message.getReceivedDate()) != null) + return (df.format(date)); + else + return ""; + } + + /** + * Returns the from field. + */ + public String getFrom() throws MessagingException { + return formatAddresses(message.getFrom()); + } + + /** + * Returns the address to reply to. + */ + public String getReplyTo() throws MessagingException { + Address[] a = message.getReplyTo(); + if (a.length > 0) + return ((InternetAddress)a[0]).getAddress(); + else + return ""; + } + + /** + * Returns the javax.mail.Message object. + */ + public Message getMessage() { + return message; + } + + /** + * Returns the message number. + */ + public String getNum() { + return (Integer.toString(message.getMessageNumber())); + } + + /** + * Returns the received date field. + */ + public String getReceivedDate() throws MessagingException { + if (hasReceivedDate()) + return (message.getReceivedDate().toString()); + else + return ""; + } + + /** + * Returns the sent date field. + */ + public String getSentDate() throws MessagingException { + if (hasSentDate()) + return (message.getSentDate().toString()); + else + return ""; + } + + /** + * Returns the subject field. + */ + public String getSubject() throws MessagingException { + if (hasSubject()) + return message.getSubject(); + else + return ""; + } + + /** + * Returns the to field. + */ + public String getTo() throws MessagingException { + return formatAddresses( + message.getRecipients(Message.RecipientType.TO)); + } + + /** + * Method for checking if the message has attachments. + */ + public boolean hasAttachments() throws java.io.IOException, + MessagingException { + boolean hasAttachments = false; + if (message.isMimeType("multipart/*")) { + Multipart mp = (Multipart)message.getContent(); + if (mp.getCount() > 1) + hasAttachments = true; + } + + return hasAttachments; + } + + /** + * Method for checking if the message has a bcc field. + */ + public boolean hasBcc() throws MessagingException { + return (message.getRecipients(Message.RecipientType.BCC) != null); + } + + /** + * Method for checking if the message has a cc field. + */ + public boolean hasCc() throws MessagingException { + return (message.getRecipients(Message.RecipientType.CC) != null); + } + + /** + * Method for checking if the message has a date field. + */ + public boolean hasDate() throws MessagingException { + return (hasSentDate() || hasReceivedDate()); + } + + /** + * Method for checking if the message has a from field. + */ + public boolean hasFrom() throws MessagingException { + return (message.getFrom() != null); + } + + /** + * Method for checking if the message has the desired mime type. + */ + public boolean hasMimeType(String mimeType) throws MessagingException { + return message.isMimeType(mimeType); + } + + /** + * Method for checking if the message has a received date field. + */ + public boolean hasReceivedDate() throws MessagingException { + return (message.getReceivedDate() != null); + } + + /** + * Method for checking if the message has a sent date field. + */ + public boolean hasSentDate() throws MessagingException { + return (message.getSentDate() != null); + } + + /** + * Method for checking if the message has a subject field. + */ + public boolean hasSubject() throws MessagingException { + return (message.getSubject() != null); + } + + /** + * Method for checking if the message has a to field. + */ + public boolean hasTo() throws MessagingException { + return (message.getRecipients(Message.RecipientType.TO) != null); + } + + /** + * Method for mapping a message to this MessageInfo class. + */ + public void setMessage(Message message) { + this.message = message; + } + + /** + * Utility method for formatting msg header addresses. + */ + private String formatAddresses(Address[] addrs) { + if (addrs == null) + return ""; + StringBuilder strBuf = new StringBuilder(getDisplayAddress(addrs[0])); + for (int i = 1; i < addrs.length; i++) { + strBuf.append(", ").append(getDisplayAddress(addrs[i])); + } + return strBuf.toString(); + } + + /** + * Utility method which returns a string suitable for msg header display. + */ + private String getDisplayAddress(Address a) { + String pers = null; + String addr = null; + if (a instanceof InternetAddress && + ((pers = ((InternetAddress)a).getPersonal()) != null)) { + addr = pers + " "+"<"+((InternetAddress)a).getAddress()+">"; + } else + addr = a.toString(); + return addr; + } +} +
diff --git a/taglib/src/main/java/demo/MessageTEI.java b/taglib/src/main/java/demo/MessageTEI.java new file mode 100644 index 0000000..c829852 --- /dev/null +++ b/taglib/src/main/java/demo/MessageTEI.java
@@ -0,0 +1,34 @@ +/* + * Copyright (c) 2001, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package demo; + +import javax.servlet.jsp.*; +import javax.servlet.jsp.tagext.*; + +/** + * Extra information class to support the scripting variable created by the + * MessagesTag class. The variable exists outside of the tag. + * + */ +public class MessageTEI extends TagExtraInfo { + + public MessageTEI() { + super(); + } + + public VariableInfo[] getVariableInfo(TagData data) { + VariableInfo info = new VariableInfo(data.getId(),"MessageInfo", + true, VariableInfo.AT_END); + VariableInfo[] varInfo = { info }; + return varInfo; + } +} +
diff --git a/taglib/src/main/java/demo/MessageTag.java b/taglib/src/main/java/demo/MessageTag.java new file mode 100644 index 0000000..3c51d77 --- /dev/null +++ b/taglib/src/main/java/demo/MessageTag.java
@@ -0,0 +1,87 @@ +/* + * Copyright (c) 2001, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package demo; + +import java.util.*; +import javax.mail.*; +import javax.mail.internet.*; +import javax.servlet.jsp.*; +import javax.servlet.jsp.tagext.*; + +/** + * Custom tag for retrieving a message. + */ +public class MessageTag extends TagSupport { + private String folder; + private String session; + private int num = 1; + + /** + * folder attribute setter method. + */ + public String getFolder() { + return folder; + } + + /** + * num attribute getter method. + */ + public String getNum() { + return Integer.toString(num); + } + + /** + * session attribute getter method. + */ + public String getSession() { + return session; + } + + /** + * folder setter method. + */ + public void setFolder(String folder) { + this.folder = folder; + } + + /** + * num attribute setter method. + */ + public void setNum(String num) { + this.num = Integer.parseInt(num); + } + + /** + * session attribute setter method. + */ + public void setSession(String session) { + this.session = session; + } + + /** + * Method for processing the start of the tag. + */ + public int doStartTag() throws JspException { + MessageInfo messageinfo = new MessageInfo(); + try { + Folder f = (Folder)pageContext.getAttribute( + getFolder(), PageContext.SESSION_SCOPE); + Message message = f.getMessage(num); + messageinfo.setMessage(message); + pageContext.setAttribute(getId(), messageinfo); + } catch (Exception ex) { + throw new JspException(ex.getMessage()); + } + + return SKIP_BODY; + } +} +
diff --git a/taglib/src/main/java/demo/SendTag.java b/taglib/src/main/java/demo/SendTag.java new file mode 100644 index 0000000..b9e8669 --- /dev/null +++ b/taglib/src/main/java/demo/SendTag.java
@@ -0,0 +1,120 @@ +/* + * Copyright (c) 2001, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package demo; + +import java.util.*; +import java.net.*; +import javax.mail.*; +import javax.mail.internet.*; +import javax.servlet.jsp.*; +import javax.servlet.jsp.tagext.*; + +/** + * Custom tag for sending messages. + */ +public class SendTag extends BodyTagSupport { + private String body; + private String cc; + private String host; + private String recipients; + private String sender; + private String subject; + + /** + * host attribute setter method. + */ + public void setHost(String host) { + this.host = host; + } + + /** + * recipient attribute setter method. + */ + public void setRecipients(String recipients) { + this.recipients = recipients; + } + + /** + * sender attribute setter method. + */ + public void setSender(String sender) { + this.sender = sender; + } + + /** + * cc attribute setter method. + */ + public void setCc(String cc) { + this.cc = cc; + } + + /** + * subject attribute setter method. + */ + public void setSubject(String subject) { + this.subject = subject; + } + + /** + * Method for processing the end of the tag. + */ + public int doEndTag() throws JspException { + Properties props = System.getProperties(); + + try { + if (host != null) + props.put("mail.smtp.host", host); + else if (props.getProperty("mail.smtp.host") == null) + props.put("mail.smtp.host", InetAddress.getLocalHost(). + getHostName()); + } catch (Exception ex) { + throw new JspException(ex.getMessage()); + } + Session session = Session.getDefaultInstance(props, null); + + Message msg = new MimeMessage(session); + InternetAddress[] toAddrs = null, ccAddrs = null; + + try { + if (recipients != null) { + toAddrs = InternetAddress.parse(recipients, false); + msg.setRecipients(Message.RecipientType.TO, toAddrs); + } else + throw new JspException("No recipient address specified"); + + if (sender != null) + msg.setFrom(new InternetAddress(sender)); + else + throw new JspException("No sender address specified"); + + if (cc != null) { + ccAddrs = InternetAddress.parse(cc, false); + msg.setRecipients(Message.RecipientType.CC, ccAddrs); + } + + if (subject != null) + msg.setSubject(subject); + + if ((body = getBodyContent().getString()) != null) + msg.setText(body); + else + msg.setText(""); + + Transport.send(msg); + + } catch (Exception ex) { + throw new JspException(ex.getMessage()); + } + + return(EVAL_PAGE); + } +} +
diff --git a/taglib/src/main/resources/META-INF/taglib.tld b/taglib/src/main/resources/META-INF/taglib.tld new file mode 100644 index 0000000..23c2b8c --- /dev/null +++ b/taglib/src/main/resources/META-INF/taglib.tld
@@ -0,0 +1,127 @@ +<?xml version="1.0" encoding="ISO-8859-1" ?> +<!DOCTYPE taglib PUBLIC "-//Sun Microsystems, Inc.//DTD JSP Tag Library 1.1//EN" "http://java.sun.com/j2ee/dtds/web-jsptaglibrary_1_1.dtd"> +<!-- + + Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<taglib> + <tlibversion>1.0</tlibversion> + <jspversion>1.1</jspversion> + <shortname>javamail</shortname> + <uri>http://java.sun.com/products/javamail/demo/webapp</uri> + <tag> + <name>listattachments</name> + <tagclass>demo.ListAttachmentsTag</tagclass> + <teiclass>demo.ListAttachmentsTEI</teiclass> + <bodycontent>JSP</bodycontent> + <info> + A listattachments tag + </info> + <attribute> + <name>id</name> + <required>true</required> + <rtexprvalue>true</rtexprvalue> + </attribute> + <attribute> + <name>messageinfo</name> + <required>true</required> + <rtexprvalue>true</rtexprvalue> + </attribute> + </tag> + <tag> + <name>listmessages</name> + <tagclass>demo.ListMessagesTag</tagclass> + <teiclass>demo.ListMessagesTEI</teiclass> + <bodycontent>JSP</bodycontent> + <info> + A listmessages tag + </info> + <attribute> + <name>id</name> + <required>true</required> + <rtexprvalue>true</rtexprvalue> + </attribute> + <attribute> + <name>folder</name> + <required>false</required> + <rtexprvalue>true</rtexprvalue> + </attribute> + <attribute> + <name>session</name> + <required>false</required> + <rtexprvalue>true</rtexprvalue> + </attribute> + </tag> + <tag> + <name>message</name> + <tagclass>demo.MessageTag</tagclass> + <teiclass>demo.MessageTEI</teiclass> + <bodycontent>empty</bodycontent> + <info> + A message tag + </info> + <attribute> + <name>id</name> + <required>true</required> + <rtexprvalue>true</rtexprvalue> + </attribute> + <attribute> + <name>folder</name> + <required>false</required> + <rtexprvalue>true</rtexprvalue> + </attribute> + <attribute> + <name>session</name> + <required>false</required> + <rtexprvalue>true</rtexprvalue> + </attribute> + <attribute> + <name>num</name> + <required>true</required> + <rtexprvalue>true</rtexprvalue> + </attribute> + </tag> + <tag> + <name>sendmail</name> + <tagclass>demo.SendTag</tagclass> + <bodycontent>JSP</bodycontent> + <info> + A sendmail tag + </info> + <attribute> + <name>host</name> + <required>false</required> + <rtexprvalue>true</rtexprvalue> + </attribute> + <attribute> + <name>recipients</name> + <required>true</required> + <rtexprvalue>true</rtexprvalue> + </attribute> + <attribute> + <name>sender</name> + <required>true</required> + <rtexprvalue>true</rtexprvalue> + </attribute> + <attribute> + <name>subject</name> + <required>false</required> + <rtexprvalue>true</rtexprvalue> + </attribute> + </tag> +</taglib> +
diff --git a/update_version b/update_version new file mode 100755 index 0000000..28828fe --- /dev/null +++ b/update_version
@@ -0,0 +1,46 @@ +#!/bin/sh +# +# Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. +# +# This program and the accompanying materials are made available under the +# terms of the Eclipse Public License v. 2.0, which is available at +# http://www.eclipse.org/legal/epl-2.0. +# +# This Source Code may also be made available under the following Secondary +# Licenses when the conditions for such availability set forth in the +# Eclipse Public License v. 2.0 are satisfied: GNU General Public License, +# version 2 with the GNU Classpath Exception, which is available at +# https://www.gnu.org/software/classpath/license.html. +# +# SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +# + +# +# update version number in pom files, e.g., to change to release +# version number in preparation for release, or to change back to +# snapshot version number after release. +# +# Usage: update_version new-version +# +new=$1 +# find the second version string in pom.xml, which is the old version +# this is almost the same as "mvn versions:set -DnewVersion=$1" +# except that misses mbox/native/pom.xml and oldmail/pom.xml +old=`grep '<version>.*</version>' pom.xml | sed -n 2p | \ + sed -e 's:.*<version>\(.*\)</version>.*:\1:'` +for file in `find . -name 'pom.xml' ` +do + ed - "$file" <<-EOF + g:<version>$old</version>:s::<version>$new</version>: + w + q + EOF +done +zipold=`echo "$old" | sed -e 's/\\./_/g'` +zipnew=`echo "$new" | sed -e 's/\\./_/g'` +ed - pom.xml <<-EOF +g:<mail.version>$old</mail.version>:s::<mail.version>$new</mail.version>: +g:<mail.zipversion>$zipold</mail.zipversion>:s::<mail.zipversion>$zipnew</mail.zipversion>: +w +q +EOF
diff --git a/webapp/build.bat b/webapp/build.bat new file mode 100644 index 0000000..918716c --- /dev/null +++ b/webapp/build.bat
@@ -0,0 +1,42 @@ +@echo off +REM +REM Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. +REM +REM This program and the accompanying materials are made available under the +REM terms of the Eclipse Public License v. 2.0, which is available at +REM http://www.eclipse.org/legal/epl-2.0. +REM +REM This Source Code may also be made available under the following Secondary +REM Licenses when the conditions for such availability set forth in the +REM Eclipse Public License v. 2.0 are satisfied: GNU General Public License, +REM version 2 with the GNU Classpath Exception, which is available at +REM https://www.gnu.org/software/classpath/license.html. +REM +REM SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +REM +mkdir src\docroot\WEB-INF\classes +mkdir src\docroot\WEB-INF\classes\demo +mkdir src\docroot\WEB-INF\lib +cd src\classes +echo compiling classes directory +javac -d ..\docroot\WEB-INF\classes demo\*.java +cd ..\taglib +echo compiling lib directory +javac -classpath "..\docroot\WEB-INF\classes;%CLASSPATH%" demo\*.java +echo creating tag library archive +jar cvf ..\docroot\WEB-INF\lib\taglib.jar META-INF demo\*.class +del demo\*.class +cd ..\docroot +echo creating web archive +jar cvf ..\..\javamail.war index.html *.jsp WEB-INF +cd WEB-INF\classes\demo +del *.* +cd .. +rmdir demo +cd .. +rmdir classes +cd lib +del *.* +cd .. +rmdir lib +cd ..\..\..
diff --git a/webapp/build.sh b/webapp/build.sh new file mode 100644 index 0000000..1468b76 --- /dev/null +++ b/webapp/build.sh
@@ -0,0 +1,27 @@ +# +# Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. +# +# This program and the accompanying materials are made available under the +# terms of the Eclipse Distribution License v. 1.0, which is available at +# http://www.eclipse.org/org/documents/edl-v10.php. +# +# SPDX-License-Identifier: BSD-3-Clause +# + +mkdir src/docroot/WEB-INF/classes +mkdir src/docroot/WEB-INF/lib +cd src/classes +echo "compiling classes directory" +javac -d ../docroot/WEB-INF/classes demo/*.java +cd ../taglib +echo "compiling lib directroy" +javac -classpath ../docroot/WEB-INF/classes:$CLASSPATH demo/*.java +echo "creating tag library archive" +jar cvf ../docroot/WEB-INF/lib/taglib.jar META-INF demo/*.class +rm demo/*.class +cd ../docroot +echo "creating web archive" +jar cvf ../../javamail.war index.html *.jsp WEB-INF +rm -r WEB-INF/classes +rm -r WEB-INF/lib +cd ../..
diff --git a/webapp/pom.xml b/webapp/pom.xml new file mode 100644 index 0000000..7156ed7 --- /dev/null +++ b/webapp/pom.xml
@@ -0,0 +1,69 @@ +<?xml version="1.0" encoding="iso-8859-1"?> +<!-- + + Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 + http://maven.apache.org/maven-v4_0_0.xsd"> + <parent> + <groupId>com.sun.mail</groupId> + <artifactId>all</artifactId> + <version>1.6.3</version> + </parent> + <modelVersion>4.0.0</modelVersion> + <groupId>com.sun.mail</groupId> + <artifactId>webapp</artifactId> + <packaging>war</packaging> + <name>JavaMail API demo webapp</name> + + <build> + <plugins> + <plugin> + <artifactId>maven-war-plugin</artifactId> + <configuration> + <warName>webapp</warName> + <packagingExcludes> + WEB-INF/lib/activation*.jar, + WEB-INF/lib/javax.mail*.jar, + WEB-INF/lib/jsp*.jar, + WEB-INF/lib/servlet*.jar + </packagingExcludes> + <outputFileNameMapping> + @{artifactId}@.@{extension}@ + </outputFileNameMapping> + </configuration> + </plugin> + </plugins> + </build> + + <dependencies> + <dependency> + <groupId>com.sun.mail</groupId> + <artifactId>taglib</artifactId> + </dependency> + <dependency> + <groupId>javax.servlet</groupId> + <artifactId>servlet-api</artifactId> + </dependency> + <dependency> + <groupId>com.sun.mail</groupId> + <artifactId>jakarta.mail</artifactId> + </dependency> + </dependencies> +</project>
diff --git a/webapp/src/main/java/demo/AttachmentServlet.java b/webapp/src/main/java/demo/AttachmentServlet.java new file mode 100644 index 0000000..72cc823 --- /dev/null +++ b/webapp/src/main/java/demo/AttachmentServlet.java
@@ -0,0 +1,74 @@ +/* + * Copyright (c) 2001, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package demo; + +import java.io.*; +import javax.mail.*; +import javax.mail.internet.*; +import javax.servlet.*; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +/** + * This servlet gets the input stream for a given msg part and + * pushes it out to the browser with the correct content type. + * Used to display attachments and relies on the browser's + * content handling capabilities. + */ +public class AttachmentServlet extends HttpServlet { + + /** + * This method handles the GET requests from the client. + */ + public void doGet(HttpServletRequest request, HttpServletResponse response) + throws IOException, ServletException { + + HttpSession session = request.getSession(); + ServletOutputStream out = response.getOutputStream(); + int msgNum = Integer.parseInt(request.getParameter("message")); + int partNum = Integer.parseInt(request.getParameter("part")); + MailUserBean mailuser = (MailUserBean)session.getAttribute("mailuser"); + + // check to be sure we're still logged in + if (mailuser.isLoggedIn()) { + try { + Message msg = mailuser.getFolder().getMessage(msgNum); + + Multipart multipart = (Multipart)msg.getContent(); + Part part = multipart.getBodyPart(partNum); + + String sct = part.getContentType(); + if (sct == null) { + out.println("invalid part"); + return; + } + ContentType ct = new ContentType(sct); + + response.setContentType(ct.getBaseType()); + InputStream is = part.getInputStream(); + int i; + while ((i = is.read()) != -1) + out.write(i); + out.flush(); + out.close(); + + } catch (MessagingException ex) { + throw new ServletException(ex.getMessage()); + } + } else { + getServletConfig().getServletContext(). + getRequestDispatcher("/index.html"). + forward(request, response); + } + } +}
diff --git a/webapp/src/main/java/demo/FilterServlet.java b/webapp/src/main/java/demo/FilterServlet.java new file mode 100644 index 0000000..220dc74 --- /dev/null +++ b/webapp/src/main/java/demo/FilterServlet.java
@@ -0,0 +1,62 @@ +/* + * Copyright (c) 2001, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package demo; + +import java.io.*; +import javax.servlet.*; +import javax.servlet.http.*; + +/** + * This servlet is used to determine whether the user is logged in before + * forwarding the request to the selected URL. + */ +public class FilterServlet extends HttpServlet { + + /** + * This method handles the "POST" submission from two forms: the + * login form and the message compose form. + */ + public void doPost(HttpServletRequest request, + HttpServletResponse response) + throws IOException, ServletException { + + String servletPath = request.getServletPath(); + servletPath = servletPath.concat(".jsp"); + + getServletConfig().getServletContext(). + getRequestDispatcher("/" + servletPath).forward(request, response); + } + + /** + * This method handles the GET requests from the client. + */ + public void doGet(HttpServletRequest request, + HttpServletResponse response) + throws IOException, ServletException { + + // check to be sure we're still logged in + // before forwarding the request. + HttpSession session = request.getSession(); + MailUserBean mailuser = (MailUserBean)session.getAttribute("mailuser"); + String servletPath = request.getServletPath(); + servletPath = servletPath.concat(".jsp"); + + if (mailuser.isLoggedIn()) + getServletConfig().getServletContext(). + getRequestDispatcher("/" + servletPath). + forward(request, response); + else + getServletConfig().getServletContext(). + getRequestDispatcher("/index.html"). + forward(request, response); + } +} +
diff --git a/webapp/src/main/java/demo/MailUserBean.java b/webapp/src/main/java/demo/MailUserBean.java new file mode 100644 index 0000000..f39e6a4 --- /dev/null +++ b/webapp/src/main/java/demo/MailUserBean.java
@@ -0,0 +1,189 @@ +/* + * Copyright (c) 2001, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package demo; + +import java.util.*; +import javax.mail.*; +import javax.naming.*; + +/** + * This JavaBean is used to store mail user information. + */ +public class MailUserBean { + private Folder folder; + private String hostname; + private String username; + private String password; + private Session session; + private Store store; + private URLName url; + private String protocol = "imap"; + private String mbox = "INBOX"; + + public MailUserBean(){} + + /** + * Returns the javax.mail.Folder object. + */ + public Folder getFolder() { + return folder; + } + + /** + * Returns the number of messages in the folder. + */ + public int getMessageCount() throws MessagingException { + return folder.getMessageCount(); + } + + /** + * hostname getter method. + */ + public String getHostname() { + return hostname; + } + + /** + * hostname setter method. + */ + public void setHostname(String hostname) { + this.hostname = hostname; + } + + /** + * username getter method. + */ + public String getUsername() { + return username; + } + + /** + * username setter method. + */ + public void setUsername(String username) { + this.username = username; + } + + /** + * password getter method. + */ + public String getPassword() { + return password; + } + + /** + * password setter method. + */ + public void setPassword(String password) { + this.password = password; + } + + /** + * session getter method. + */ + public Session getSession() { + return session; + } + + /** + * session setter method. + */ + public void setSession(Session session) { + this.session = session; + } + + /** + * store getter method. + */ + public Store getStore() { + return store; + } + + /** + * store setter method. + */ + public void setStore(Store store) { + this.store = store; + } + + /** + * url getter method. + */ + public URLName getUrl() { + return url; + } + + /** + * Method for checking if the user is logged in. + */ + public boolean isLoggedIn() { + return store.isConnected(); + } + + /** + * Method used to login to the mail host. + */ + public void login() throws Exception { + url = new URLName(protocol, getHostname(), -1, mbox, + getUsername(), getPassword()); + /* + * First, try to get the session from JNDI, + * as would be done under J2EE. + */ + try { + InitialContext ic = new InitialContext(); + Context ctx = (Context)ic.lookup("java:comp/env"); + session = (Session)ctx.lookup("MySession"); + } catch (Exception ex) { + // ignore it + } + + // if JNDI fails, try the old way that should work everywhere + if (session == null) { + Properties props = null; + try { + props = System.getProperties(); + } catch (SecurityException sex) { + props = new Properties(); + } + session = Session.getInstance(props, null); + } + store = session.getStore(url); + store.connect(); + folder = store.getFolder(url); + + folder.open(Folder.READ_WRITE); + } + + /** + * Method used to login to the mail host. + */ + public void login(String hostname, String username, String password) + throws Exception { + + this.hostname = hostname; + this.username = username; + this.password = password; + + login(); + } + + /** + * Method used to logout from the mail host. + */ + public void logout() throws MessagingException { + folder.close(false); + store.close(); + store = null; + session = null; + } +} +
diff --git a/webapp/src/main/webapp/WEB-INF/web.xml b/webapp/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000..c3f88bb --- /dev/null +++ b/webapp/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,66 @@ +<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.2//EN" "http://java.sun.com/j2ee/dtds/web-app_2_2.dtd"> +<!-- + + Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Distribution License v. 1.0, which is available at + http://www.eclipse.org/org/documents/edl-v10.php. + + SPDX-License-Identifier: BSD-3-Clause + +--> + +<web-app> + <display-name>JspDemo</display-name> + <description>no description</description> + <servlet> + <servlet-name>FilterServlet</servlet-name> + <display-name>FilterServlet</display-name> + <description>no description</description> + <servlet-class>demo.FilterServlet</servlet-class> + </servlet> + <servlet> + <servlet-name>AttachmentServlet</servlet-name> + <display-name>AttachmentServlet</display-name> + <description>no description</description> + <servlet-class>demo.AttachmentServlet</servlet-class> + </servlet> + <servlet-mapping> + <servlet-name>FilterServlet</servlet-name> + <url-pattern>/compose</url-pattern> + </servlet-mapping> + <servlet-mapping> + <servlet-name>FilterServlet</servlet-name> + <url-pattern>/errordetails</url-pattern> + </servlet-mapping> + <servlet-mapping> + <servlet-name>FilterServlet</servlet-name> + <url-pattern>/login</url-pattern> + </servlet-mapping> + <servlet-mapping> + <servlet-name>FilterServlet</servlet-name> + <url-pattern>/logout</url-pattern> + </servlet-mapping> + <servlet-mapping> + <servlet-name>FilterServlet</servlet-name> + <url-pattern>/send</url-pattern> + </servlet-mapping> + <servlet-mapping> + <servlet-name>FilterServlet</servlet-name> + <url-pattern>/messageheaders</url-pattern> + </servlet-mapping> + <servlet-mapping> + <servlet-name>FilterServlet</servlet-name> + <url-pattern>/messagecontent</url-pattern> + </servlet-mapping> + <servlet-mapping> + <servlet-name>AttachmentServlet</servlet-name> + <url-pattern>/attachment</url-pattern> + </servlet-mapping> + <resource-ref> + <res-ref-name>MySession</res-ref-name> + <res-type>javax.mail.Session</res-type> + <res-auth>Container</res-auth> + </resource-ref> +</web-app>
diff --git a/webapp/src/main/webapp/compose.jsp b/webapp/src/main/webapp/compose.jsp new file mode 100644 index 0000000..3f08b62 --- /dev/null +++ b/webapp/src/main/webapp/compose.jsp
@@ -0,0 +1,65 @@ +<%-- + + Copyright (c) 2001, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Distribution License v. 1.0, which is available at + http://www.eclipse.org/org/documents/edl-v10.php. + + SPDX-License-Identifier: BSD-3-Clause + +--%> + +<%@ page language="java" %> +<%@ page errorPage="errorpage.jsp" %> + +<html> +<head> + <title>JavaMail compose</title> +</head> + +<body bgcolor="#ccccff"> +<form ACTION="send" METHOD=POST> +<input type="hidden" name="send" value="send"> +<p align="center"> +<b><font size="4" face="Verdana, Arial, Helvetica"> +JavaMail Compose Message</font></b> +<p> +<table border="0" width="100%"> +<tr> +<td width="16%" height="22"> +<p align="right"> +<b><font face="Verdana, Arial, Helvetica">To:</font></b></td> +<td width="84%" height="22"> +<% if (request.getParameter("to") != null) { %> +<input type="text" name="to" value="<%= request.getParameter("to") %>" size="30"> +<% } else { %> +<input type="text" name="to" size="30"> +<% } %> +<font size="1" face="Verdana, Arial, Helvetica"> + (separate addresses with commas)</font></td></tr> +<tr> +<td width="16%"><p align="right"> +<b><font face="Verdana, Arial, Helvetica">From:</font></b></td> +<td width="84%"> +<input type="text" name="from" size="30"> +<font size="1" face="Verdana, Arial, Helvetica"> + (separate addresses with commas)</font></td></tr> +<tr> +<td width="16%"><p align="right"> +<b><font face="Verdana, Arial, Helvetica">Subject:</font></b></td> +<td width="84%"> +<input type="text" name="subject" size="55"></td></tr> +<tr> +<td width="16%"> </td> +<td width="84%"><textarea name="text" rows="15" cols="53"></textarea></td></tr> +<tr> +<td width="16%" height="32"> </td> +<td width="84%" height="32"> +<input type="submit" name="Send" value="Send"> +<input type="reset" name="Reset" value="Reset"></td></tr> +</table> +</form> +</body> +</html> +
diff --git a/webapp/src/main/webapp/errordetails.jsp b/webapp/src/main/webapp/errordetails.jsp new file mode 100644 index 0000000..c946216 --- /dev/null +++ b/webapp/src/main/webapp/errordetails.jsp
@@ -0,0 +1,22 @@ +<%-- + + Copyright (c) 2001, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Distribution License v. 1.0, which is available at + http://www.eclipse.org/org/documents/edl-v10.php. + + SPDX-License-Identifier: BSD-3-Clause + +--%> + +<%@ page isErrorPage="true" %> +<html> +<head> +<title>JavaMail errordetails</title> +</head> +<body bgcolor="white"> +<%= session.getValue("details") %> +</body> +</html> +
diff --git a/webapp/src/main/webapp/errorpage.jsp b/webapp/src/main/webapp/errorpage.jsp new file mode 100644 index 0000000..4df0407 --- /dev/null +++ b/webapp/src/main/webapp/errorpage.jsp
@@ -0,0 +1,26 @@ +<%-- + + Copyright (c) 2001, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Distribution License v. 1.0, which is available at + http://www.eclipse.org/org/documents/edl-v10.php. + + SPDX-License-Identifier: BSD-3-Clause + +--%> + +<%@ page isErrorPage="true" %> +<html> +<head> +<title>JavaMail errorpage</title> +</head> +<body bgcolor="white"> +<form ACTION="errordetails" METHOD=POST> +<% session.putValue("details", exception.toString()); %> +<h2>An error occured while attempting to perform the operation you requested. +</h2> +<input type="submit" name="Error Details" value="Error Details"> +</body> +</html> +
diff --git a/webapp/src/main/webapp/folders.jsp b/webapp/src/main/webapp/folders.jsp new file mode 100644 index 0000000..9eef9c0 --- /dev/null +++ b/webapp/src/main/webapp/folders.jsp
@@ -0,0 +1,45 @@ +<%-- + + Copyright (c) 2001, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Distribution License v. 1.0, which is available at + http://www.eclipse.org/org/documents/edl-v10.php. + + SPDX-License-Identifier: BSD-3-Clause + +--%> + +<%@ page language="java" import="javax.mail.*, demo.MailUserBean" %> +<%@ page errorPage="errorpage.jsp" %> +<jsp:useBean id="mailuser" scope="session" class="demo.MailUserBean" /> + +<html> +<head> + <title>JavaMail folders</title> +</head> + +<body bgcolor="#ccccff"> + +<center> +<font face="Arial,Helvetica" font size=+3> +<b>Welcome to JavaMail!</b> </font> </center> +<table width="50%" border=0 align=center> +<tr> +<td width="75%" bgcolor="#ffffcc"> +<font face="Arial, Helvetica" font size=-1> +<b>FolderName</b></font></td><br> +<td width="25%" bgcolor="#ffffcc"> +<font face="Arial, Helvetica" font size=-1> +<b>Messages</b></font></td><br> +</tr> +<tr> +<td width="75%" bgcolor="#ffffff"> +<a href="messageheaders">Inbox</a></td><br> +<td width="25%" bgcolor="#ffffff"> +<%= mailuser.getMessageCount() %> +</tr> +</table> +</body> +</html> +
diff --git a/webapp/src/main/webapp/index.html b/webapp/src/main/webapp/index.html new file mode 100644 index 0000000..9a6db21 --- /dev/null +++ b/webapp/src/main/webapp/index.html
@@ -0,0 +1,93 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2//EN"> +<HTML> +<!-- + + Copyright (c) 1998, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Distribution License v. 1.0, which is available at + http://www.eclipse.org/org/documents/edl-v10.php. + + SPDX-License-Identifier: BSD-3-Clause + +--> + +<HEAD> + <META HTTP-EQUIV="Content-Type" CONTENT="text/html;CHARSET=iso-8859-1"> + <TITLE>JavaMail</TITLE> +</HEAD> + +<BODY BGCOLOR="#CCCCFF"> + +<FORM ACTION="login" METHOD=POST ENCTYPE="application/x-www-form-urlencoded"> +<P ALIGN="CENTER"><B><FONT SIZE="5" FACE="Arial, Helvetica">Welcome to JavaMail</FONT></B></P> + +<P ALIGN="CENTER"><B><FONT SIZE="5" FACE="Arial, Helvetica">HTML Email Reader Demo</FONT></B></P> +<CENTER> +<P> +<TABLE BORDER="0" WIDTH="100%"> + <TR> + <TD WIDTH="40%"> + <P ALIGN="RIGHT"><FONT FACE="Arial, Helvetica">IMAP Hostname:</FONT> + </TD> + <TD WIDTH="60%"><INPUT TYPE="TEXT" NAME="hostname" SIZE="25"></TD> + </TR> + <TR> + <TD WIDTH="40%"> + <P ALIGN="RIGHT"><FONT FACE="Arial, Helvetica">Username:</FONT> + </TD> + <TD WIDTH="60%"><INPUT TYPE="TEXT" NAME="username" SIZE="25"></TD> + </TR> + <TR> + <TD WIDTH="40%"> + <P ALIGN="RIGHT"><FONT FACE="Arial, Helvetica">Password:</FONT> + </TD> + <TD WIDTH="60%"><INPUT TYPE="PASSWORD" NAME="password" SIZE="25"></TD> + </TR> +</TABLE> +<INPUT TYPE="SUBMIT" VALUE="Login"><INPUT TYPE="RESET" NAME="Reset" VALUE="Reset"></P> +</CENTER> +<P> +<HR ALIGN="CENTER"> +</P> +<P><B><I><FONT FACE="Arial, Helvetica">Overview:</FONT></I></B></P> + +<FONT SIZE="2" FACE="Arial, Helvetica"> +<b>THIS IS A DEMO!!!</b> Please see the webapp.README.txt +file for information on how to +compile and run the web application.<br> <br> +This demo illustrates the use of JavaMail APIs in a 3-tiered web +application. It allows the user to login to an +IMAP store, list all the messages in the INBOX folder, view +selected messages, compose and send a message, and logout. +</FONT> + +<P><B><I><FONT FACE="Arial, Helvetica">Features:</FONT></I></B></P> + +<UL> + <LI><FONT SIZE="2" FACE="Arial, Helvetica">HTML access to your IMAP mailbox</FONT> + <LI><FONT SIZE="2" FACE="Arial, Helvetica">Proxy-able anywhere HTTP can be proxied</FONT> + <LI><FONT SIZE="2" FACE="Arial, Helvetica">Easy to use</FONT> + <LI><FONT SIZE="2" FACE="Arial, Helvetica">Uses web browser's content handling capabilities</FONT> +</UL> + +<P><B><I><FONT FACE="Arial, Helvetica">Limitations:</FONT></I></B></P> + +<UL> + <LI><FONT SIZE="2" FACE="Arial, Helvetica">Only INBOX support (no user folders)</FONT> + <LI><FONT SIZE="2" FACE="Arial, Helvetica">Can't delete, copy, move, print, save, forward, reply to, search in + messages --<BR> + but it could be done</FONT> + <LI><FONT SIZE="2" FACE="Arial, Helvetica">Doesn't check for new messages (have to log out and log back it)</FONT> +</UL> + +<P> +<HR ALIGN="CENTER"> +</P> + +<P><FONT FACE="Arial, Helvetica">Feedback to </FONT><A HREF="mailto:javamail_ww@oracle.com"><FONT FACE="Arial, Helvetica">javamail_ww@oracle.com</FONT></A> +</FORM> + +</BODY> + +</HTML>
diff --git a/webapp/src/main/webapp/login.jsp b/webapp/src/main/webapp/login.jsp new file mode 100644 index 0000000..39b7702 --- /dev/null +++ b/webapp/src/main/webapp/login.jsp
@@ -0,0 +1,35 @@ +<%-- + + Copyright (c) 2001, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Distribution License v. 1.0, which is available at + http://www.eclipse.org/org/documents/edl-v10.php. + + SPDX-License-Identifier: BSD-3-Clause + +--%> + +<%@ page language="java" import="demo.MailUserBean" %> +<%@ page errorPage="errorpage.jsp" %> +<jsp:useBean id="mailuser" scope="session" class="demo.MailUserBean" /> + +<html> +<head> + <title>JavaMail login</title> +</head> + +<body bgcolor="white"> + +<% + mailuser.login(request.getParameter("hostname"), + request.getParameter("username"), + request.getParameter("password")); + session.setAttribute("folder", mailuser.getFolder()); +%> + +<jsp:forward page="folders.jsp" /> + +</body> +</html> +
diff --git a/webapp/src/main/webapp/logout.jsp b/webapp/src/main/webapp/logout.jsp new file mode 100644 index 0000000..0217e98 --- /dev/null +++ b/webapp/src/main/webapp/logout.jsp
@@ -0,0 +1,28 @@ +<%-- + + Copyright (c) 2001, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Distribution License v. 1.0, which is available at + http://www.eclipse.org/org/documents/edl-v10.php. + + SPDX-License-Identifier: BSD-3-Clause + +--%> + +<%@ page language="java" import="demo.MailUserBean" %> +<%@ page errorPage="errorpage.jsp" %> +<jsp:useBean id="mailuser" scope="session" class="demo.MailUserBean" /> + +<html> +<head> + <title>JavaMail logout</title> +</head> + +<% mailuser.logout(); %> + +<body> +<h2>Logged out OK</h2><a href=index.html>click here to login</a> +</body> +</html> +
diff --git a/webapp/src/main/webapp/messagecontent.jsp b/webapp/src/main/webapp/messagecontent.jsp new file mode 100644 index 0000000..d75c9af --- /dev/null +++ b/webapp/src/main/webapp/messagecontent.jsp
@@ -0,0 +1,96 @@ +<%-- + + Copyright (c) 2001, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Distribution License v. 1.0, which is available at + http://www.eclipse.org/org/documents/edl-v10.php. + + SPDX-License-Identifier: BSD-3-Clause + +--%> + +<%@ page language="java" import="demo.MessageInfo, demo.AttachmentInfo" %> +<%@ page errorPage="errorpage.jsp" %> +<%@ taglib uri="http://java.sun.com/products/javamail/demo/webapp" + prefix="javamail" %> + + +<html> +<head> + <title>JavaMail messagecontent</title> +</head> + +<javamail:message +id="msginfo" +folder="folder" +num="<%= request.getParameter(\"message\") %>" +/> + +<body bgcolor="#ccccff"> +<center><font face="Arial,Helvetica" font size="+3"> +<b>Message<sp> +<sp>in folder /INBOX</b></font></center><p> + +<%-- first, display this message's headers --%> +<b>Date:</b> +<%= msginfo.getSentDate() %> +<br> +<% if (msginfo.hasFrom()) { %> +<b>From:</b> +<a href="compose?to=<%= msginfo.getReplyTo() %>" + target="reply<%= msginfo.getNum() %>"> +<%= msginfo.getFrom() %> +</a> +<br> +<% } %> + +<% if (msginfo.hasTo()) { %> +<b>To:</b> +<%= msginfo.getTo() %> +<br> +<% } %> + +<% if (msginfo.hasCc()) { %> +<b>CC:</b> +<%= msginfo.getCc() %> +<br> +<% } %> + +<b>Subject:</b> +<% if (msginfo.hasSubject()) { %> +<%= msginfo.getSubject() %> +<% } %> +<br> +<pre> +<%= msginfo.getBody() %> +</pre> +<% if (msginfo.hasAttachments()) { %> +<javamail:listattachments + id="attachment" + messageinfo="msginfo"> +<p><hr> +<b>Attachment Type:</b> +<%= attachment.getAttachmentType() %> +<br> +<% if (attachment.hasMimeType("text/plain") && + attachment.isInline()){ %> +<pre> +<%= attachment.getContent() %> +</pre> +<% } else { %> +<b>Filename:</b> +<%= attachment.getFilename() %> +<br> +<b>Description:</b> +<%= attachment.getDescription() %> +<br> +<a href="attachment?message= +<%= msginfo.getNum() %>&part=<%= attachment.getNum() %>"> +Display Attachment</a> +<% } %> +</javamail:listattachments> +<% } %> +</body> +</html> +
diff --git a/webapp/src/main/webapp/messageheaders.jsp b/webapp/src/main/webapp/messageheaders.jsp new file mode 100644 index 0000000..2f6dfdd --- /dev/null +++ b/webapp/src/main/webapp/messageheaders.jsp
@@ -0,0 +1,82 @@ +<%-- + + Copyright (c) 2001, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Distribution License v. 1.0, which is available at + http://www.eclipse.org/org/documents/edl-v10.php. + + SPDX-License-Identifier: BSD-3-Clause + +--%> + +<%@ page language="java" import="demo.MessageInfo" %> +<%@ page errorPage="errorpage.jsp" %> +<%@ taglib uri="http://java.sun.com/products/javamail/demo/webapp" + prefix="javamail" %> + +<html> +<head> + <title>JavaMail messageheaders</title> +</head> + +<body bgcolor="#ccccff"><hr> + +<center><font face="Arial,Helvetica" font size="+3"> +<b>Folder INBOX</b></font></center><p> + +<font face="Arial,Helvetica" font size="+3"> +<b><a href="logout">Logout</a> +<a href="compose" target="compose">Compose</a> +</b></font> +<hr> + +<table cellpadding=1 cellspacing=1 width="100%" border=1> +<tr> +<td width="25%" bgcolor="ffffcc"> +<font face="Arial,Helvetica" font size="+1"> +<b>Sender</b></font></td> +<td width="15%" bgcolor="ffffcc"> +<font face="Arial,Helvetica" font size="+1"> +<b>Date</b></font></td> +<td bgcolor="ffffcc"> +<font face="Arial,Helvetica" font size="+1"> +<b>Subject</b></font></td> +</tr> +<javamail:listmessages + id="msginfo" + folder="folder"> +<%-- from --%> +<tr valign=middle> +<td width="25%" bgcolor="ffffff"> +<font face="Arial,Helvetica"> +<% if (msginfo.hasFrom()) { %> +<%= msginfo.getFrom() %> +</font> +<% } else { %> +<font face="Arial,Helvetica,sans-serif"> +Unknown +<% } %> +</font></td> +<%-- date --%> +<td nowrap width="15%" bgcolor="ffffff"> +<font face="Arial,Helvetica"> +<%= msginfo.getDate() %> +</font></td> +<%-- subject & link --%> +<td bgcolor="ffffff"> +<font face="Arial,Helvetica"> +<a href="messagecontent?message=<%= msginfo.getNum() %>"> +<% if (msginfo.hasSubject()) { %> +<%= msginfo.getSubject() %> +<% } else { %> +<i>No Subject</i> +<% } %> +</a> +</font></td> +</tr> +</javamail:listmessages> +</table> +</body> +</html> +
diff --git a/webapp/src/main/webapp/send.jsp b/webapp/src/main/webapp/send.jsp new file mode 100644 index 0000000..6ef00a9 --- /dev/null +++ b/webapp/src/main/webapp/send.jsp
@@ -0,0 +1,36 @@ +<%-- + + Copyright (c) 2001, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Distribution License v. 1.0, which is available at + http://www.eclipse.org/org/documents/edl-v10.php. + + SPDX-License-Identifier: BSD-3-Clause + +--%> + +<%@ page language="java" %> +<%@ page errorPage="errorpage.jsp" %> +<%@ taglib uri="http://java.sun.com/products/javamail/demo/webapp" + prefix="javamail" %> + +<html> +<head> + <title>JavaMail send</title> +</head> + +<body bgcolor="white"> +<javamail:sendmail + recipients="<%= request.getParameter(\"to\") %>" + sender="<%= request.getParameter(\"from\") %>" + subject="<%= request.getParameter(\"subject\") %>" +> +<%= request.getParameter("text") %> +</javamail:sendmail> + +<h1>Message sent successfully</h1> + +</body> +</html> +
diff --git a/webapp/webapp.README.txt b/webapp/webapp.README.txt new file mode 100644 index 0000000..6a90697 --- /dev/null +++ b/webapp/webapp.README.txt
@@ -0,0 +1,189 @@ +This demo illustrates the use of JavaMail APIs in a 3-tiered web +application. It must be deployed on a web server that supports +servlet and JSP-based web applications (e.g. Tomcat) using at least +J2SE 1.3 and Servlet 2.2. + + +-----------+ +-----------+ +-----------+ + | IMAP | | | | | + | Server |<-IMAP->| JavaMail |<-HTTP->| WWW | + +-----------+ | Web app |--HTML->| Browser | + | SMTP |<-SMTP->| | | | + | Server | | | | | + +-----------+ +-----------+ +-----------+ + + +The JavaMail Web application supports the following functionality: + * login to an IMAP server + * list all the messages in the INBOX folder + * view the selected message + * compose and send a message + +It is comprised of an HTML document and several Web components +(servlets, JSP pages and custom tags) and is packaged in a Web +archive with the following contents: + + index.html + login.jsp + folders.jsp + messageheaders.jsp + messagecontent.jsp + compose.jsp + send.jsp + errorpage.jsp + errordetails.jsp + logout.jsp + WEB-INF/ + WEB-INF/classes/ + WEB-INF/classes/demo/AttachmentServlet.class + WEB-INF/classes/demo/FilterServlet.class + WEB-INF/classes/demo/MailUserBean.class + WEB-INF/lib/ + WEB-INF/lib/jtl.jar + WEB-INF/web.xml + + + + + + + + + + + +The collection of .html and .jsp files provide the client-side view +of the JavaMail Web application. + + index.html + | + login.jsp + | + folders.jsp + | + messageheaders.jsp + | + / \ + / \ + compose.jsp messagecontent.jsp + | + send.jsp + + +The WEB-INF/web.xml file contains the web applications deployment +descriptor. It is an XML document that contains configuration and +deployment information for the application. + +As per the Servlet specification, the WEB-INF/classes directory +contains servlet and utility classes used by the web application. +The FilterServlet acts as the Controller of the JavaMail Web +application. All requests are mapped to this servlet. It checks +to see that the user is logged in to the server before forwarding the +request to the appropriate JSP page. The AttachmentServlet is used +to render non-text attachments and the MailUserBean is a utility class +used to maintain information about the mail user. + +The WEB-INF/lib directory (also defined in the Servlet specification) +contains the Java archive file for the javamail custom tag library. + +The follow tags are furnished in the javamail tag library: + +<javamail:sendmail + mailuser="mailuserbean" + [host="hostname"] [port="port"] + sender="email address" recipient="email address" + [cc="email address"] [bcc="email address"] [subject="subject"]> +body of the message +</javamail:sendmail> + +Description: Used to send messages. + + +<javamail:message id="messageinfo" + folder="folder" + num="message number"/> + +Description: Used to read a single message. + + +<javamail:listmessages id="mailinfobean" + folder="folder" +</javamail:listmessages> + +Description: Used to iterate through list of messages. The body of the +tag is repeated for each message in the list. + + + + + + + + + + + +Building and Packaging the JavaMail Web application + +All source for this demo can be found in the following directories: + src/classes + src/docroot + src/taglib + +Build scripts (build.sh and build.bat) are provided for building and +packaging the demo. Before executing the build scripts, be sure that +the following are in your CLASSPATH: + mail.jar the JavaMail jar file + activation.jar the JAF jar file + servlet.jar the servlet/JSP jar file + +The following steps are performed when building and packaging the demo. + +1. Create a directory named "src/docroot/WEB-INF/classes/demo". + +2. Create a directory named "src/docroot/WEB-INF/lib". + +3. Compile the files from the "src/classes/demo" directory and add them + to "src/docroot/WEB-INF/classes/demo". + +4. Compile the files from the "src/tablib" directory. + +5. Create an archive (jtl.jar) of the taglib classes and add it + to "src/docroot/WEB-INF/lib". + +6. Create a web archive file of the contents of "src/docroot" (and all + of its sub-directories). + +(For a list of the contents of the resulting web archive, + see the beginning of this document.) + + + + + + + + + + + +A note on sending mail + +In order to send mail using the JavaMail Web App, it is necessary to identify +an SMTP host. This can be accomplished in a couple of ways. + +1. Use the TOMCAT_OPTS environment variable. + + Add the following to the TOMCAT_OPTS environment variable: + + -Dmail.smtp.host=yourSMTPmailservername + + Restart your web server. + + +2. Modify the send.jsp file and update the javamail.war file: + + Add the following parameter to the <javamail:sendmail> tag: + + host="yourSMTPmailservername" + + Repackage the javamail.war file to include the modified send.jsp file.