package com.planet_ink.coffee_mud.Libraries; import com.planet_ink.coffee_mud.Libraries.interfaces.*; import com.planet_ink.coffee_mud.core.interfaces.*; import com.planet_ink.coffee_mud.core.*; import com.planet_ink.coffee_mud.core.CMProps.Str; import com.planet_ink.coffee_mud.core.collections.*; import java.net.*; import java.io.*; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.*; import javax.naming.*; import javax.naming.directory.*; import com.planet_ink.coffee_mud.core.exceptions.*; /* Copyright 2004-2019 Bo Zimmerman Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ public class SMTPclient extends StdLibrary implements SMTPLibrary, SMTPLibrary.SMTPClient { @Override public String ID() { return "SMTPclient"; } /** Reply buffer */ public BufferedReader reply = null; /** Send writer */ public PrintWriter send = null; /** Socket to use */ public Socket sock = null; private SMTPHostAuth auth = null; Attribute doMXLookup( final String hostName ) { try { final Hashtable<String,String> env = new Hashtable<String,String>(); env.put("java.naming.factory.initial", "com.sun.jndi.dns.DnsContextFactory"); final DirContext ictx = new InitialDirContext( env ); final Attributes attrs = ictx.getAttributes( hostName, new String[] { "MX" }); final Attribute attr = attrs.get( "MX" ); if( attr == null ) return( null ); return( attr ); } catch(final javax.naming.NamingException x) { } return null; } @Override public SMTPClient getClient(final String SMTPServerInfo, final int port) throws UnknownHostException,IOException { return new SMTPclient(SMTPServerInfo,port); } @Override public SMTPClient getClient(final String emailAddress) throws IOException, BadEmailAddressException { return new SMTPclient(emailAddress); } public SMTPclient() { super(); } /** * Create a SMTP object pointing to the specified host * @param SMTPServerInfo the host to connect to. * @param port the port to connect to. * @throws UnknownHostException the host was unknown * @throws IOException a socket error */ public SMTPclient( final String SMTPServerInfo, int port) throws UnknownHostException,IOException { auth = new SMTPHostAuth(SMTPServerInfo); final int portIndex=auth.host.lastIndexOf(':'); if(portIndex>0) { port=CMath.s_int(auth.host.substring(portIndex+1)); auth.host=auth.host.substring(0,portIndex); } sock = new Socket( auth.getHost(), port ); reply = new BufferedReader(new InputStreamReader(sock.getInputStream())); sock.setSoTimeout(DEFAULT_TIMEOUT); send = new PrintWriter( sock.getOutputStream() ); final boolean debug = CMSecurity.isDebugging(CMSecurity.DbgFlag.SMTPCLIENT); String rstr = reply.readLine(); if(debug) Log.debugOut("SMTPclient",rstr); if ((rstr==null)||(!rstr.startsWith("220"))) throw new ProtocolException(rstr); while (rstr.indexOf('-') == 3) { rstr = reply.readLine(); if(debug) Log.debugOut("SMTPclient",rstr); if (!rstr.startsWith("220")) throw new ProtocolException(rstr); } } public SMTPclient (final String emailAddress) throws IOException, BadEmailAddressException { int x=this.getEmailAddressError(emailAddress); if(x>=0) throw new BadEmailAddressException("Malformed email address"); x=emailAddress.indexOf('@'); final String domain=emailAddress.substring(x+1).trim(); final Vector<String> addys=new Vector<String>(); final Attribute mx=doMXLookup(domain); boolean connected=false; try { if((mx!=null)&&(mx.size()>0)) for(final NamingEnumeration<?> e=mx.getAll();e.hasMore();) addys.addElement((String)e.next()); } catch(final javax.naming.NamingException ne) { } if(addys.size()==0) addys.addElement(domain); for (String hostid : addys) { final int y=hostid.lastIndexOf(' '); if(y>=0) hostid=hostid.substring(y+1).trim(); try { sock = new Socket( hostid, DEFAULT_PORT ); reply = new BufferedReader(new InputStreamReader(sock.getInputStream())); sock.setSoTimeout(DEFAULT_TIMEOUT); send = new PrintWriter( sock.getOutputStream() ); final boolean debug = CMSecurity.isDebugging(CMSecurity.DbgFlag.SMTPCLIENT); String rstr = reply.readLine(); if(debug) Log.debugOut("SMTPclient",rstr); if ((rstr==null)||(!rstr.startsWith("220"))) throw new ProtocolException(rstr); while (rstr.indexOf('-') == 3) { rstr = reply.readLine(); if(debug) Log.debugOut("SMTPclient",rstr); if (!rstr.startsWith("220")) throw new ProtocolException(rstr); } connected=true; break; } catch(final Exception ex) { // just try the next one. } } if(!connected) throw new IOException("Unable to connect to '"+domain+"'."); } @Override public boolean emailIfPossible(final String fromName, final String toName, final String subj, final String msg) { try { SMTPLibrary.SMTPClient SC=null; final String toEmail=makeValidEmailAddress(toName); if(!isValidEmailAddress(toEmail)) { Log.errOut("SMTPClient","User/Account not found: "+toName); return false; } if(CMProps.getVar(CMProps.Str.SMTPSERVERNAME).length()>0) SC=CMLib.smtp().getClient(CMProps.getVar(CMProps.Str.SMTPSERVERNAME),SMTPLibrary.DEFAULT_PORT); else SC=CMLib.smtp().getClient(toEmail); final String domain=CMProps.getVar(CMProps.Str.MUDDOMAIN).toLowerCase(); SC.sendMessage(fromName+"@"+domain, fromName+"@"+domain, toEmail, toEmail, subj, CMLib.coffeeFilter().simpleOutFilter(msg)); return true; } catch(final Exception e) { Log.errOut("SMTPClient",e.getMessage()); return false; } } @Override public boolean emailIfPossible(final String SMTPServerInfo, final String from, final String replyTo, final String to, final String subject, final String message) { if(CMSecurity.isDisabled(CMSecurity.DisFlag.SMTPCLIENT)) { Log.debugOut("SMTPclient", "Message not sent: "+from+"/"+replyTo+"/"+to+"/"+to+"/"+subject+"/"+message); return false; } try { SMTPclient SC=null; if(SMTPServerInfo.length()>0) SC=new SMTPclient(SMTPServerInfo,DEFAULT_PORT); else SC=new SMTPclient(to); SC.sendMessage(from, replyTo, to, to, subject, message); return true; } catch(final Exception ioe) { } return false; } @Override public void emailOrJournal(final String from, final String replyTo, final String to, final String subject, String message) { final String smtpServerInfo = CMProps.getVar(CMProps.Str.SMTPSERVERNAME); final String fromEmail=(from.indexOf('@')>0)?makeValidEmailAddress(from):(from+'@'+CMProps.getVar(Str.MUDDOMAIN).toLowerCase()); final String toEmail=makeValidEmailAddress(to); final String replyToEmail=(replyTo.indexOf('@')>0)?makeValidEmailAddress(replyTo):(replyTo+'@'+CMProps.getVar(Str.MUDDOMAIN).toLowerCase()); final String unSubUrl; if(!isValidEmailAddress(to)) unSubUrl = CMLib.utensils().getUnsubscribeURL(to); else unSubUrl = null; message+=L("\n\r\n\rThis message was sent to "+to+" through the @x1 mail server at @x2, port @x3. ", CMProps.getVar(CMProps.Str.MUDNAME), CMProps.getVar(CMProps.Str.MUDDOMAIN), CMProps.getVar(CMProps.Str.MUDPORTS))+ L("Please contact the administrators regarding any abuse of this system.\n\r")+ ((unSubUrl == null) ? "" : L("To unsubscribe, visit: @x1 \n\r",unSubUrl)); if(!emailIfPossible(smtpServerInfo, fromEmail, replyToEmail, toEmail, subject, message)) { if(CMProps.getIntVar(CMProps.Int.MAXMAILBOX)>0) { final int count=CMLib.database().DBCountJournal(CMProps.getVar(CMProps.Str.MAILBOX),null,to); if(count>=CMProps.getIntVar(CMProps.Int.MAXMAILBOX)) { Log.warnOut("Unable to email "+to+" because max mailbox has been reached."); return; } } CMLib.database().DBWriteJournal(CMProps.getVar(CMProps.Str.MAILBOX), from, to, subject, message); } } /** private constants for chars that are valid in email addy names */ private final static String EMAIL_VALID_LOCAL_CHARS="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#$%&'*+-/=?^_`{|}~."; /** private constants for chars that are valid in email addy domain names */ private final static String EMAIL_VALID_DOMAIN_CHARS="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-."; /* * Checks to see if the given string is a valid email address. * @param addy the email address * @return true if all is good, false otherwise. */ @Override public boolean isValidEmailAddress(final String addy) { return getEmailAddressError(addy)<0; } /* * Checks to see if the given string is a valid email address. * If it is not, it returns the location from 0-length+1 where * the problem occurs. * @param addy the email address * @return -1, or the location of the error from 0-length+1 */ private int getEmailAddressError(final String addy) { if(addy==null) return 0; final int x=addy.indexOf('@'); if(x<0) return addy.length(); final String localPart=addy.substring(0,x).trim(); final String network=addy.substring(x+1); if(localPart.length()==0) return x; if(network.length()==0) return addy.length(); if((localPart.startsWith("\"")&&localPart.endsWith("\""))) { final int z=localPart.substring(1,localPart.length()-1).indexOf("\""); if(z>=0) return 1+z; } else for(int l=0;l<localPart.length();l++) { if(EMAIL_VALID_LOCAL_CHARS.indexOf(localPart.charAt(l))<0) return l; } if(localPart.startsWith(".")) return 0; if(localPart.endsWith(".")) return x-1; if(localPart.length()>64) return x; for(int l=0;l<network.length();l++) { if(EMAIL_VALID_DOMAIN_CHARS.indexOf(network.charAt(l))<0) return x+1+l; } if(network.startsWith("-")) return x+1; if(network.endsWith("-")) return addy.length(); if(network.startsWith(".")) return x+1; if(network.length()>255) return addy.length(); return -1; } public String makeValidEmailAddress(final String name) { if(!isValidEmailAddress(name)) { if(CMLib.players().playerExists(name)) return CMLib.players().getLoadPlayer(name).playerStats().getEmail(); else if(CMLib.players().accountExists(name)) return CMLib.players().getLoadAccount(name).getEmail(); else if(CMLib.players().playerExistsAllHosts(name)) return CMLib.players().getPlayerAllHosts(name).playerStats().getEmail(); else if(CMLib.players().accountExistsAllHosts(name)) return CMLib.players().getAccountAllHosts(name).getEmail(); } return name; } public void sendLine(final boolean debug, final String sstr) { if(debug) Log.debugOut("SMTPclient",sstr); send.print(sstr); send.print(EOL); send.flush(); } /** * Send a message * * Usage: Mailer.sendmsg(S, From, To, Subject, Message); * @param froaddress Address sending from * @param reply_address Address reply to * @param to_address Address sending to * @param mockto_address Address sending to * @param subject Subject line * @param message Message content */ @Override public synchronized void sendMessage(String froaddress, String reply_address, String to_address, final String mockto_address, final String subject, String message) throws IOException { froaddress=froaddress.replace(' ','_'); reply_address=reply_address.replace(' ','_'); to_address=to_address.replace(' ','_'); if(CMSecurity.isDisabled(CMSecurity.DisFlag.SMTPCLIENT)) { Log.debugOut("SMTPclient", "Message not sent: "+froaddress+"/"+reply_address+"/"+to_address+"/"+mockto_address+"/"+subject+"/"+message); return; } String rstr; final boolean debug = CMSecurity.isDebugging(CMSecurity.DbgFlag.SMTPCLIENT); final StringBuffer fixMsg=new StringBuffer(message); for(int f=0;f<fixMsg.length();f++) { if((fixMsg.charAt(f)=='\n') &&(f>0) &&(fixMsg.charAt(f-1)!='\r')) { if((f<fixMsg.length()-1)&&(fixMsg.charAt(f+1)=='\r')) { fixMsg.setCharAt(f,'\r'); fixMsg.setCharAt(f+1,'\n'); } else { fixMsg.insert(f,'\r'); f++; } } else if((fixMsg.charAt(f)=='\r') &&(f<fixMsg.length()-1) &&(fixMsg.charAt(f+1)!='\n')) { if((f>0)&&(fixMsg.charAt(f-1)=='\n')) { fixMsg.setCharAt(f-1,'\r'); fixMsg.setCharAt(f,'\n'); } else { fixMsg.insert(f+1,'\n'); f++; } } } message=fixMsg.toString(); final String host = CMProps.getVar(CMProps.Str.MUDDOMAIN); if((auth != null) && (auth.getAuthType().length()>0)) sendLine(debug,"EHLO " + host); else sendLine(debug,"HELO " + host); rstr = reply.readLine(); if(debug) Log.debugOut("SMTPclient",rstr); if ((rstr==null)||(!rstr.startsWith("250"))) throw new ProtocolException(""+rstr); if((auth != null) && (auth.getAuthType().length()>0)) { if(auth.getAuthType().equalsIgnoreCase("login")) sendLine(debug,"AUTH " + auth.getAuthType()+" "+auth.getLogin()); else sendLine(debug,"AUTH " + auth.getAuthType()); while(rstr.startsWith("250")) rstr = reply.readLine(); if(debug) Log.debugOut("SMTPclient",rstr); if (!rstr.startsWith("334")) throw new ProtocolException(""+rstr); if(auth.getAuthType().equalsIgnoreCase("plain")) sendLine(debug,auth.getPlainLogin()); else if(auth.getAuthType().equalsIgnoreCase("login")) sendLine(debug,auth.getPassword()); rstr = reply.readLine(); if(debug) Log.debugOut("SMTPclient",rstr); if ((rstr==null)||(!rstr.startsWith("235"))) throw new ProtocolException(""+rstr); } sendLine(debug,"MAIL FROM:<" + froaddress+">"); rstr = reply.readLine(); if(debug) Log.debugOut("SMTPclient",rstr); if ((rstr==null)||(!rstr.startsWith("250"))) throw new ProtocolException(""+rstr); sendLine(debug,"RCPT TO:<" + to_address+">"); rstr = reply.readLine(); if(debug) Log.debugOut("SMTPclient",rstr); if ((rstr==null)||(!rstr.startsWith("250"))) throw new ProtocolException(""+rstr); sendLine(debug,"DATA"); rstr = reply.readLine(); if(debug) Log.debugOut("SMTPclient",rstr); if ((rstr==null)||(!rstr.startsWith("354"))) throw new ProtocolException(""+rstr); sendLine(debug,"MIME-Version: 1.0"); if((message.indexOf("<HTML>")>=0)&&(message.indexOf("</HTML>")>=0)) sendLine(debug,"Content-Type: text/html"); else sendLine(debug,"Content-Type: text/plain; charset=iso-8859-1"); sendLine(debug,"Content-Transfer-Encoding: 7bit"); sendLine(debug,"Date: " + CMLib.time().smtpDateFormat(System.currentTimeMillis())); sendLine(debug,"From: " + froaddress); sendLine(debug,"Subject: " + subject); sendLine(debug,"Sender: " + froaddress); sendLine(debug,"Reply-To: " + reply_address); sendLine(debug,"To: " + mockto_address); // Sending a blank line ends the header part. send.print(EOL); // Now send the message proper sendLine(debug,message); sendLine(debug,"."); rstr = reply.readLine(); if(debug) Log.debugOut("SMTPclient",rstr); if (!rstr.startsWith("250")) throw new ProtocolException(rstr); } /** * return members of a list on an email server. * 250-First Last [emailaddress]\r * * Usage: List=Mailer.getListMembers(List); * @param list member list * @return String List of members * @throws IOException a socket error * @throws ProtocolException an internal error */ public synchronized String getListMembers( final String list) throws IOException, ProtocolException { String sendString; InetAddress local; try { local = InetAddress.getLocalHost(); } catch (final UnknownHostException ioe) { System.err.println("No local IP address found - is your network up?"); throw ioe; } final String host = local.getHostName(); send.print("HELO " + host); send.print(EOL); send.flush(); String rstr = reply.readLine(); if (!rstr.startsWith("250")) throw new ProtocolException(rstr); sendString = "EXPN " + list ; send.print(sendString); send.print(EOL); send.flush(); rstr=""; try { while(true) { rstr+=reply.readLine(); sock.setSoTimeout(1000); } } catch(final java.io.InterruptedIOException x) { // not really an error, just a control break } sock.setSoTimeout(DEFAULT_TIMEOUT); if (!rstr.startsWith("250")) throw new ProtocolException(rstr); return rstr; } /** * close this socket * * Usage: this.close(); */ public void close() { try { send.print("QUIT"); send.print(EOL); send.flush(); sock.close(); } catch (final IOException ioe) { // As though there's anything I can dof about it now... } } /** * close this socket * * Usage: finalize(); */ @Override protected void finalize() throws Throwable { this.close(); super.finalize(); } private class SMTPHostAuth { public SMTPHostAuth(final String unparsedServerInfo) { final List<String> info=CMParms.parseCommas(unparsedServerInfo,false); if(info.size()==0) return; host = info.remove(0); if((info.size()==0)||(host.length()==0)) return; final String s=info.get(0); if(s.equalsIgnoreCase("plain")||s.equalsIgnoreCase("login")) authType=info.remove(0).toUpperCase().trim(); else authType="PLAIN"; if(info.size()==0) { authType=""; return; } login=info.remove(0); if(info.size()==0) return; password=info.remove(0); } private String host = ""; private String authType = ""; private String login = ""; private String password = ""; public String getHost() { return host; } public String getAuthType() { return authType; } public String getPlainLogin() { final byte[] buffer = new byte[2 + login.length() + password.length()]; int bufDex=0; buffer[bufDex++]=0; for(int i=0;i<login.length();i++) buffer[bufDex++]=(byte)login.charAt(i); buffer[bufDex++]=0; for(int i=0;i<password.length();i++) buffer[bufDex++]=(byte)password.charAt(i); return B64Encoder.B64encodeBytes(buffer); } public String getLogin() { final byte[] buffer = new byte[login.length()]; int bufDex=0; for(int i=0;i<login.length();i++) buffer[bufDex++]=(byte)login.charAt(i); return B64Encoder.B64encodeBytes(buffer); } public String getPassword() { final byte[] buffer = new byte[password.length()]; int bufDex=0; for(int i=0;i<password.length();i++) buffer[bufDex++]=(byte)password.charAt(i); return B64Encoder.B64encodeBytes(buffer); } } }