
// SendGmail.java
// Andrew Davison, ad@coe.psu.ac.th, June 2023

/* Send an e-mail from the Gmail account specified using
   the username and password options.

   Uses Jakarta/Angus Mail:
     * https://javaee.github.io/javamail/
     * https://eclipse-ee4j.github.io/angus-mail/

   This requires the following JARs in libs/ :
     * jakarta.mail-2.0.2.jar
     * angus-activation-2.0.1.jar
     * jakarta.activation-api-2.1.2.jar

   Requires username/password's Google Account "Less secure app access" 
   to be switched to ON. (Or you can try using OAuth2 authentication;
   see https://javaee.github.io/javamail/OAuth2 and
   https://developers.google.com/identity/protocols/oauth2 
   https://developers.google.com/gmail/imap/xoauth2-libraries )

   Jakarta Mail requires java option:
     -add-opens java.base/sun.security.util=ALL-UNNAMED
   (see run.bat)


Usage: SendGmail [-dhvV] [-a=<fnm>] [-b=<bcc>] [-c=<cc>] [-f=<from>]
                 -s=<subject> -t=<to> (-U=<usr> -P=<pwd>) [<bodyText>]
Send an email using a Gmail account.
      [<bodyText>]          Body text of the message
  -a, --attach=<fnm>        File to attach to message
  -b, --bcc=<bcc>           Bcc address of message
  -c, --cc=<cc>             Cc address of message
  -d, --debug               Turn on session debugging
  -f, --from=<from>         Sender's address; default is username
  -h, --help                Show this help message and exit.
  -P, --password=<pwd>      Google account password
  -s, --subject=<subject>   Subject line of message
  -t, --to=<to>             To address of message
  -U, --username=<usr>      Google account username
  -v, --verbose             Turn on session reports
  -V, --version             Print version information and exit.


  Picocli features:
    * named options and positional parameters
    * POSIX-style grouped short options 
    * automatic help and version options
    * exit codes
    * argument groups
    * Command-line argument files (@-files)

    * did not use an interactive password since it interferred with
      reading a message body from stdin


  Usage examples (but with your Gmail username and password):
    > compile SendGmail.java

    > run SendGmail -h

    > run SendGmail -s="test 1" -t="chunkylover53@aol.com" -U="UUUUUU" -P="XXXXX" "this is message 1"

    > run SendGmail -s="test 2" -t="chunkylover53@aol.com" @login.txt -a="SendGmail.java" "this is message 2"

    > cat Calc.java | run SendGmail -s="test 3" -t="chunkylover53@aol.com" @login.txt -a="SendGmail.java" 

    > run SendGmail -s="test 4" -a="Calc.java" -t="chunkylover53@aol.com" @login.txt 
        this is message 4
        ctrl-Z

    > run SendGmail -v @login.txt -s="test 5" -t="chunkylover53@aol.com" @longMsg.txt
*/


import java.io.*;
import java.util.*;
import java.util.concurrent.*;

import picocli.CommandLine;
import picocli.CommandLine.*;

import jakarta.mail.*;
import jakarta.mail.internet.*;
import org.eclipse.angus.mail.util.*;

@Command(name = "SendGmail", 
         mixinStandardHelpOptions = true,
         version = "SendGmail 1.0",
         description = "Send an email using a Gmail account.")
public class SendGmail implements Callable<Integer>
{
  private static final String GMAIL_HOST = "smtp.gmail.com";

  @ArgGroup(exclusive = false, multiplicity = "1")
    LoginPair loginPair;

    static class LoginPair {
      @Option(names = { "-U", "--username" }, 
          description = "Google account username",
          required = true)
      private String usr;

      @Option(names = { "-P", "--password" }, 
          description = "Google account password",
          required = true)
      private String pwd;
    }

  @Option(names = { "-f", "--from" }, 
          description = "Sender's address; default is username")
  private String from = null;

  @Option(names = { "-c", "--cc" }, 
          description = "Cc address of message")
  private String cc = null;

  @Option(names = { "-b", "--bcc" }, 
          description = "Bcc address of message")
  private String bcc = null;

  @Option(names = { "-d", "--debug" }, 
          description = "Turn on session debugging")
  private boolean isDebugging = false;

  @Option(names = { "-v", "--verbose" }, 
          description = "Turn on session reports")
  private boolean isVerbose = false;

  @Option(names = { "-a", "--attach" }, 
          description = "File to attach to message")
  private String fnm = null;

  @Option(names = { "-t", "--to" }, 
          description = "@|bold To|@ address of message",
          required = true)
  private String to;

  @Option(names = { "-s", "--subject" }, 
       description = "@|bold Subject line|@ of message",
       required = true)
  private String subject;


  @Parameters(index = "0", arity="0..1",
        description = "@|bold Body text|@ of the message")
  private String bodyText;


  public Integer call()
  { 
    Properties props = new Properties();
    props.put("mail.smtp.host", GMAIL_HOST);
    props.put("mail.smtp.ssl.enable", "true");   // required for gmail

    Session session = Session.getInstance(props);
    session.setDebug(isDebugging);

    try {
      if (isVerbose)
        printlnAnsi("@|green \nPreparing mail...|@");

      MimeMessage msg = new MimeMessage(session);
      if (from == null)
        from = loginPair.usr;
      msg.setFrom( new InternetAddress(from));
      InternetAddress[] addr = { new InternetAddress(to) };
      msg.setRecipients(Message.RecipientType.TO, addr);

      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);
      msg.setSentDate(new Date());

      if (bodyText == null)
        bodyText = readMsg();

      if (fnm != null) {
        // use a multipart message to hold the attachment
        MimeBodyPart mbp1 = new MimeBodyPart();
        mbp1.setText(bodyText);

        MimeBodyPart mbp2 = new MimeBodyPart();
        try {
          mbp2.attachFile(fnm);
        }
        catch (IOException e)
        {  System.err.println("Unable to attach file: " + fnm);  }

        MimeMultipart mp = new MimeMultipart();
        mp.addBodyPart(mbp1);
        mp.addBodyPart(mbp2);
        msg.setContent(mp);
      } 
      else
        // setText(bodyText, charset)
        msg.setText(bodyText);

      Transport.send(msg, loginPair.usr, 
                          loginPair.pwd); 
      if (isVerbose)
        printlnAnsi("@|green Mail was sent successfully.|@");
      return 0;
    }
    catch (MessagingException mex) {
      printlnErrAnsi("@|red Message send error:|@");
      System.err.println(" " + mex.getMessage());
      // mex.printStackTrace();
      showErrors(mex);
      return 1;
    }
  } /* call */


  private void showErrors(Exception ex)
  {
    do {
      if (ex instanceof SendFailedException) {
        System.err.println();
        SendFailedException sfex = (SendFailedException)ex;

        Address[] invalid = sfex.getInvalidAddresses();
        if (invalid != null) {
          System.err.println("Invalid Addresses");
          for (int i = 0; i < invalid.length; i++) 
            System.err.println(" " + invalid[i]);
        }

        Address[] validUnsent = sfex.getValidUnsentAddresses();
        if (validUnsent != null) {
          System.err.println("Valid Unsent Addresses");
          for (int i = 0; i < validUnsent.length; i++) 
            System.err.println(" " + validUnsent[i]);
        }

        Address[] validSent = sfex.getValidSentAddresses();
        if (validSent != null) {
          System.err.println("Valid Sent Addresses");
          for (int i = 0; i < validSent.length; i++) 
            System.err.println(" " + validSent[i]);
        }
      }
      if (ex instanceof MessagingException)
        ex = ((MessagingException)ex).getNextException();
      else
        ex = null;
    } while (ex != null);

  }  // end of showErrors()


  private void printlnAnsi(String msg)
  {  System.out.println(Help.Ansi.AUTO.string(msg));  }

  private void printlnErrAnsi(String msg)
  {  System.err.println(Help.Ansi.AUTO.string(msg));  }


  private String readMsg() 
  {
    BufferedReader in =
       new BufferedReader(new InputStreamReader(System.in));
    try {
      String line;
      StringBuffer sb = new StringBuffer();
      while ((line = in.readLine()) != null)
        sb.append(line + "\n");
      return sb.toString();
    }
    catch(IOException e) 
    { System.err.println("Unable to read message");
      return "No message text";
    }
  }  // end of readMsg()



  // ---------------------------------------------

  public static void main(String[] args)
  {
    int exitCode = new CommandLine(new SendGmail()).execute(args);
    System.exit(exitCode);     // success or failure
  }


}  // end of SendGmail class