Sending: Session, MimeMessage, Transport
◈ 5 cardsInstall the API, build a Session from Properties plus an Authenticator, fill a MimeMessage with TO, CC and BCC parsed from comma lists, and hand the result to Transport.send.
It is a dependency, not part of the JDK
java.net ships inside the JDK. Mail does not. You download it, put it on the classpath, and write down exactly how you did it — that write-up is a large share of the assignment's research marks.
There are two generations of one API, and they are not interchangeable:
| generation | package | the two jars |
|---|---|---|
| JavaMail 1.x | javax.mail | javax.mail-api + a com.sun.mail implementation |
| Jakarta Mail 2.x | jakarta.mail | jakarta.mail-api + an Angus Mail implementation |
The rename happened when the Java EE specifications moved to the Eclipse Foundation, which could not keep the javax namespace. Class and method names are otherwise unchanged, so a tutorial written for one translates to the other by editing its imports — but both jars must come from the same generation, for the reason in the warning below.
You also need Jakarta Activation, which supplies DataHandler and FileDataSource. The implementation jar normally pulls it in; you find out in the next lesson, when attachments need it.
Session: configuration, not a connection
Session sounds like a connection. It is not. It is a Properties bundle plus an optional Authenticator, and building one touches the network zero times:
Properties props = new Properties();
props.put("mail.smtp.host", "smtp.example.net");
props.put("mail.smtp.port", "587");
props.put("mail.smtp.auth", "true");
props.put("mail.smtp.starttls.enable", "true");
mail.smtp.auth tells the transport to log in once it sees the server advertise AUTH. mail.smtp.starttls.enable tells it to upgrade the plaintext connection to TLS after EHLO, which is what the submission port 587 expects. Port 465 wants TLS from the very first byte instead — that is mail.smtp.ssl.enable, and you set one or the other, never both.
Credentials arrive through an Authenticator subclass whose getPasswordAuthentication() the transport calls back when it needs them, and you pass it to Session.getInstance(props, auth). Use a provider-issued app password, never your account password: Gmail and Outlook refuse plain account passwords for SMTP outright, and an app password is a per-program credential you can revoke without changing your login. Read it from the input file or the environment; never a string literal in source you submit.
MimeMessage: the message you are building
new MimeMessage(session) gives you an empty RFC 5322 message. Filling it is setFrom, setRecipients, setSubject, setText, and optionally setSentDate.
The recipient setter is where the assignment trips people. There are two of them and they differ by one letter. setRecipient (singular) takes exactly one Address. setRecipients (plural) takes an Address[]. The input file hands you CC: lee@example.net, ada@example.net — one comma-separated string — and the tool for that is InternetAddress.parse(String), which splits it, validates each address and returns the array the plural setter wants, throwing AddressException on anything malformed.
Message.RecipientType.BCC is a real recipient type alongside TO and CC — and notice what it is not. It is not a header. Transport.send asks the message for getAllRecipients(), which is TO plus CC plus BCC, and issues one RCPT TO per address; the writer then omits the Bcc: header from the bytes that go into DATA. That is the previous lesson's envelope/header split, implemented for you.
Worked example — reading the whole message from a file
The assignment specifies the entire message, credentials included, in one text file named on the command line:
Server: smtp.example.net
User: dana@example.org
Password: abcd-efgh-ijkl-mnop
To: sam@example.net
CC: lee@example.net, ada@example.net
BCC: quiet@example.net
Subject: lab notes
Body: Numbers attached tomorrow.
Second line of the body.
Nine lines, and one of them changes the shape of your parser. Everything above Body: is a key: value pair. Body: is the last key, and everything after it is body text — including later lines that happen to contain a colon, and including blank lines. The body ends at EOF, not at a terminator, so the loop must stop splitting and start appending the moment it sees that key:
String line;
StringBuilder body = new StringBuilder();
boolean inBody = false;
while ((line = in.readLine()) != null) {
if (inBody) { body.append(line).append("\n"); continue; }
int colon = line.indexOf(':');
if (colon < 0) continue;
String key = line.substring(0, colon).trim().toUpperCase();
String value = line.substring(colon + 1).trim();
if (key.equals("BODY")) { inBody = true; body.append(value).append("\n"); }
else fields.put(key, value);
}
A parser that splits every line on the first colon instead will silently truncate any body line containing one, and a parser that reads the body eagerly will lose the blank lines that make it readable. After that the send is mechanical: properties, session, message, three recipient sets, subject, text, Transport.send.