package javaforce.service;
import java.io.*;
import java.net.*;
import java.util.*;
import javaforce.*;
import javaforce.net.*;
import javaforce.bus.*;
/** POP3 Server.
*
* Retrieves messages from SMTP service.
*
* Supported ports : 110, 995(ssl)
*
* TODO : XTND, XLST (Thunderbird requests)
* TODO : TOP, RSET
*
* @author pquiring
*/
public class POP3Server extends ConfigServlet {
public final static String serviceBus = "javaforce.jfpop3";
public static boolean debug = false;
public String getAppName() {return "POP3";}
public String getBusName() {return serviceBus;}
public String getHelpURL() {return "https://pquiring.github.io/javaforce//projects/jfpop3/docs/help.html";}
public static String getConfigFile() {
return JF.getConfigPath() + "/jfpop3.cfg";
}
public static String getLogFile() {
return JF.getLogPath() + "/jfpop3.log";
}
public static String getMailboxFolder(String user) {
StringBuilder path = new StringBuilder();
if (JF.isWindows()) {
path.append(System.getenv("ProgramData").replaceAll("\\\\", "/"));
path.append("/jfsmtp/mail");
} else {
path.append("/var/jfsmtp/mail");
}
if (user != null) {
path.append("/");
path.append(user);
}
String mail = path.toString();
new File(mail).mkdirs();
return mail;
}
private Server server;
private ArrayList servers = new ArrayList();
private ArrayList clients = new ArrayList();
private String domain;
private String ldap_domain;
private String ldap_server;
private ArrayList users;
private Object lock = new Object();
private IP4Port bind = new IP4Port();
private ArrayList subnet_src_list;
private ArrayList ports = new ArrayList<>();
private ArrayList ssl_ports = new ArrayList<>();
private static final int FLAG_ADMIN = 1;
public POP3Server() {
}
private void addSession(ClientWorker sess) {
synchronized(lock) {
clients.add(sess);
}
}
private void removeSession(ClientWorker sess) {
synchronized(lock) {
clients.remove(sess);
}
}
private static String getKeyFile() {
return JF.getConfigPath() + "/jfpop3.key";
}
private class Server extends Thread {
private boolean active;
public void run() {
active = true;
JFLog.append(JF.getLogPath() + "/jfpop3.log", true);
JFLog.setRetention(30);
JFLog.log("POP3 : Starting service");
try {
loadConfig();
busServer = new JBusServer(serviceBus, new JBusMethods());
busServer.connect();
for(int p : ports) {
ServerWorker worker = new ServerWorker(p, false);
worker.start();
servers.add(worker);
}
for(int p : ssl_ports) {
ServerWorker worker = new ServerWorker(p, true);
worker.start();
servers.add(worker);
}
while (active) {
JF.sleep(1000);
}
} catch (Exception e) {
JFLog.log(e);
}
}
}
public void start() {
stop();
server = new Server();
server.start();
}
public void stop() {
if (server == null) return;
server.active = false;
synchronized(lock) {
ServerWorker[] sa = servers.toArray(new ServerWorker[0]);
for(ServerWorker s : sa) {
s.close();
}
servers.clear();
ClientWorker[] ca = clients.toArray(new ClientWorker[0]);
for(ClientWorker s : ca) {
s.close();
}
clients.clear();
}
if (busServer != null) {
busServer.disconnect();
busServer = null;
}
}
enum Section {None, Global};
private final static String defaultConfig
= "[global]\n"
+ "port=110\n" //default port (supports SSL)
+ "#secure=995\n" //implicit SSL port
+ "#bind=192.168.100.2\n"
+ "#domain=example.com\n"
+ "#ldap_domain=example.com\n"
+ "#ldap_server=192.168.200.2\n"
+ "#admin=user:pass\n"
+ "#account=user:pass\n"
+ "#src.ipnet=192.168.2.0/255.255.255.0\n"
+ "#src.ip=192.168.3.2\n"
;
private void loadConfig() {
JFLog.log("loadConfig");
users = new ArrayList();
Section section = Section.None;
bind.setIP("0.0.0.0"); //bind to all interfaces
bind.port = 110;
subnet_src_list = new ArrayList();
try {
BufferedReader br = new BufferedReader(new FileReader(getConfigFile()));
StringBuilder cfg = new StringBuilder();
while (true) {
String ln = br.readLine();
if (ln == null) break;
cfg.append(ln);
cfg.append("\n");
ln = ln.trim();
int cmt = ln.indexOf('#');
if (cmt != -1) ln = ln.substring(0, cmt).trim();
if (ln.length() == 0) continue;
if (ln.equals("[global]")) {
section = Section.Global;
continue;
}
int idx = ln.indexOf("=");
if (idx == -1) continue;
String key = ln.substring(0, idx);
String value = ln.substring(idx + 1);
switch (section) {
case None:
case Global:
switch (key) {
case "port":
ports.add(Integer.valueOf(ln.substring(5)));
break;
case "secure":
ssl_ports.add(Integer.valueOf(ln.substring(7)));
break;
case "bind":
if (!bind.setIP(value)) {
JFLog.log("SMTP:bind:Invalid IP:" + value);
break;
}
break;
case "admin":
case "account":
EMail user = new EMail();
int cln = value.indexOf(':');
if (cln == -1) {
JFLog.log("Invalid user:" + value);
continue;
}
user.user = value.substring(0, cln);
user.pass = value.substring(cln + 1);
user.flags = key.equals("admin") ? FLAG_ADMIN : 0;
users.add(user);
break;
case "domain":
domain = value;
break;
case "ldap_domain":
ldap_domain = value;
break;
case "ldap_server":
ldap_server = value;
break;
case "debug":
debug = value.equals("true");
break;
case "src.ipnet": {
Subnet4 subnet = new Subnet4();
idx = value.indexOf('/');
if (idx == -1) {
JFLog.log("POP3:Invalid IP Subnet:" + value);
break;
}
String ip = value.substring(0, idx);
String mask = value.substring(idx + 1);
if (!subnet.setIP(ip)) {
JFLog.log("POP3:Invalid IP:" + ip);
break;
}
if (!subnet.setMask(mask)) {
JFLog.log("POP3:Invalid netmask:" + mask);
break;
}
JFLog.log("Source Allow IP Network=" + subnet.toString());
subnet_src_list.add(subnet);
break;
}
case "src.ip": {
Subnet4 subnet = new Subnet4();
if (!subnet.setIP(value)) {
JFLog.log("POP3:Invalid IP:" + value);
break;
}
subnet.setMask("255.255.255.255");
JFLog.log("Source Allow IP Address=" + subnet.toString());
subnet_src_list.add(subnet);
break;
}
}
break;
}
}
br.close();
config = cfg.toString();
} catch (FileNotFoundException e) {
//create default config
JFLog.log("config not found, creating defaults.");
try {
FileOutputStream fos = new FileOutputStream(getConfigFile());
fos.write(defaultConfig.getBytes());
fos.close();
config = defaultConfig;
} catch (Exception e2) {
JFLog.log(e2);
}
} catch (Exception e) {
JFLog.log(e);
}
}
public class ServerWorker extends Thread {
private ServerSocket ss;
private int port;
private boolean secure;
private boolean worker_active;
public ServerWorker(int port, boolean secure) {
this.port = port;
this.secure = secure;
}
public void run() {
try {
if (secure) {
JFLog.log("CreateServerSocketSSL");
KeyMgmt keys = new KeyMgmt();
if (new File(getKeyFile()).exists()) {
FileInputStream fis = new FileInputStream(getKeyFile());
keys.open(fis, "password");
fis.close();
} else {
JFLog.log("Warning:Server SSL Keys not generated!");
}
ss = JF.createServerSocketSSL(keys);
} else {
ss = new ServerSocket();
}
synchronized(bind) {
bind.port = port;
ss.bind(bind.toInetSocketAddress());
}
worker_active = true;
while (worker_active) {
Socket s = ss.accept();
InetSocketAddress sa = (InetSocketAddress)s.getRemoteSocketAddress();
String src_ip = sa.getAddress().getHostAddress();
if (src_ip.equals("0:0:0:0:0:0:0:1")) {
//IP6 localhost
src_ip = "127.0.0.1";
}
if (!ip_src_allowed(src_ip)) {
JFLog.log("POP3:Source IP blocked:" + src_ip);
s.close();
continue;
}
ClientWorker sess = new ClientWorker(s, secure);
addSession(sess);
sess.start();
}
} catch (Exception e) {
JFLog.log(e);
}
}
public void close() {
try { ss.close(); } catch (Exception e) {}
ss = null;
}
}
private boolean ip_src_allowed(String ip4) {
if (subnet_src_list.size() == 0) return true;
IP4 target = new IP4();
if (!target.setIP(ip4)) return false;
for(Subnet4 net : subnet_src_list) {
if (net.isWithin(target)) {
return true;
}
}
return false;
}
public class ClientWorker extends Thread {
private Socket c;
private boolean secure;
private InputStream cis = null;
private OutputStream cos = null;
private byte[] req = new byte[1500];
private int reqSize = 0;
private byte[] buffer = new byte[1500];
private int bufferSize = 0;
private String mailbox;
private File[] files;
private String user;
private boolean admin;
public ClientWorker(Socket s, boolean secure) {
c = s;
this.secure = secure;
}
public void close() {
if (c != null) {
try { c.close(); } catch (Exception e) {}
}
}
public String readln() {
if (bufferSize > 0) {
System.arraycopy(buffer, 0, req, 0, bufferSize);
reqSize = bufferSize;
bufferSize = 0;
} else {
reqSize = 0;
}
try {
while (c != null && c.isConnected()) {
//check for EOL
if (reqSize >= 2) {
for(int idx=2;idx<=reqSize;idx++) {
if (req[idx - 2] == '\r' && req[idx - 1] == '\n') {
int left = reqSize - idx;
if (left > 0) {
//save extra data for next time
System.arraycopy(req, idx, buffer, 0, left);
bufferSize = left;
}
return new String(req, 0, idx - 2).trim();
}
}
}
//expand buffer if needed
if (reqSize == req.length) {
//req buffer full - expand it
if (req.length >= 8 * 1500) {
throw new Exception("data too large");
}
int newSize = req.length << 1;
byte[] new_req = new byte[newSize];
buffer = new byte[newSize];
System.arraycopy(req, 0, new_req, 0, req.length);
req = new_req;
}
//read more data
int read = cis.read(req, reqSize, req.length - reqSize);
if (read < 0) break;
reqSize += read;
}
} catch (Exception e) {
JFLog.log(e);
}
return null;
}
public void run() {
try {
JFLog.log("Session start");
cis = c.getInputStream();
cos = c.getOutputStream();
cos.write(("+OK jfPOP3 Server/" + JF.getVersion() + "\r\n").getBytes());
cos.flush();
while (c.isConnected()) {
String cmd = readln();
if (cmd == null) break;
if (cmd.equalsIgnoreCase("QUIT")) {
cos.write("+OK Goodbye\r\n".getBytes());
break;
}
doCommand(cmd);
}
} catch (Exception e) {
if (!(e instanceof SocketException)) JFLog.log(e);
}
close();
removeSession(this);
}
public boolean login(String user, String pass, boolean _md5) {
for(EMail acct : users) {
if (acct.user.equals(user)) {
admin = (acct.flags & FLAG_ADMIN) != 0;
if (_md5) {
MD5 md5 = new MD5();
md5.add(acct.pass);
return md5.toString().equals(pass);
} else {
return acct.pass.equals(pass);
}
}
}
if (ldap_server != null && ldap_domain != null) {
LDAP ldap = new LDAP();
return ldap.login(ldap_server, ldap_domain, user, pass);
}
return false;
}
private static final int bufsiz = 1500 - 20 - 20;
private void doCommand(String cmd) throws Exception {
if (debug) {
JFLog.log("Request=" + cmd);
}
String[] p = cmd.split(" ", 2);
switch (p[0].toUpperCase()) {
case "APOP": {
String[] c_u_p = cmd.split(" ", 3);
user = c_u_p[1];
String pass = c_u_p[2]; //MD5???
if (login(user, pass, true)) {
cos.write("+OK Login successful\r\n".getBytes());
} else {
JF.sleep(1000);
cos.write("-ERR Login failed\r\n".getBytes());
close();
return;
}
setupMailbox();
break;
}
case "USER": {
user = p[1];
cos.write("+OK User Accepted\r\n".getBytes());
break;
}
case "PASS": {
if (user == null) {
cos.write("-ERR Login failed\r\n".getBytes());
close();
return;
}
String pass = p[1];
if (login(user, pass, false)) {
cos.write("+OK Login successful\r\n".getBytes());
} else {
JF.sleep(1000);
cos.write("-ERR Login failed\r\n".getBytes());
close();
return;
}
setupMailbox();
break;
}
case "STARTTLS": {
if (secure) {
cos.write("-ERR Already secure\r\n".getBytes());
break;
}
cos.write("+OK Switching to TLS\r\n".getBytes());
//upgrade connection to SSL
c = JF.connectSSL(c, KeyMgmt.getDefaultClient());
cis = c.getInputStream();
cos = c.getOutputStream();
secure = true;
break;
}
case "STAT": {
//display # messages and total size
cos.write(stat().getBytes());
break;
}
case "LIST": {
//list messages
cos.write(list().getBytes());
break;
}
case "RETR": {
//get message
if (mailbox == null) {
cos.write("-ERR mailbox not ready\r\n".getBytes());
break;
}
int idx = getIndex(p[1]);
idx--;
if (idx < 0 || idx >= files.length || files[idx] == null) {
cos.write("-ERR message not found\r\n".getBytes());
break;
}
try {
File file = files[idx];
String realfile = getMailboxFolder(null) + "/" + file.getName();
FileInputStream fis = new FileInputStream(realfile);
long size = file.length();
long sent = 0;
cos.write(("+OK " + size + " octets\r\n").getBytes());
byte[] buf = new byte[bufsiz];
while (sent < size) {
int read = fis.read(buf);
if (read == -1) break;
cos.write(buf, 0, read);
sent += read;
}
fis.close();
cos.write(".\r\n".getBytes());
} catch (Exception e) {
cos.write("-ERR error\r\n".getBytes());
JFLog.log(e);
}
break;
}
case "DELE": {
//delete message
if (mailbox == null) {
cos.write("-ERR mailbox not ready\r\n".getBytes());
break;
}
int idx = getIndex(p[1]);
idx--;
if (idx < 0 || idx >= files.length || files[idx] == null) {
cos.write("-ERR message not found\r\n".getBytes());
break;
}
try {
deleteMsg(files[idx]);
files[idx] = null;
cos.write(("+OK message " + (idx+1) + " deleted\r\n").getBytes());
} catch (Exception e) {
cos.write("-ERR error\r\n".getBytes());
JFLog.log(e);
}
break;
}
case "UIDL": {
//unique id listing
String start = "1";
if (p.length > 1) {
start = p[1];
}
cos.write(uidl(start).getBytes());
break;
}
case "NOOP": {
cos.write("+OK no operation\r\n".getBytes());
break;
}
default:
cos.write("-ERR Unknown command\r\n".getBytes());
break;
}
}
private int getIndex(String arg) {
int idx;
if (arg.startsWith("T-")) {
String find = arg.substring(2) + ".msg";
idx = 0;
int offset = 1;
for(File file : files) {
if (file.getName().equals(find)) {
idx = offset;
break;
}
offset++;
}
} else {
idx = Integer.valueOf(arg);
}
return idx;
}
private void reset() {
mailbox = null;
files = null;
}
private void deleteMsg(File file) {
//deletes user .msg file
//deletes real .msg file if all other recipients have deleted msg
synchronized(lock) {
try {
if (admin) {
//TODO : check all users do not have .msg file linked (what about ldap users?)
file.delete();
return;
}
String filename = file.getName();
FileInputStream fis = new FileInputStream(file);
byte[] data = fis.readAllBytes();
fis.close();
file.delete(); //delete user .msg file
String[] lns = new String(data).split("\r\n");
for(String ln : lns) {
if (!ln.startsWith("to:")) continue;
String to_user = ln.substring(3);
String usermailbox = getMailboxFolder(to_user);
if (new File(usermailbox + "/" + filename).exists()) {
//msg still linked to another recipient
return;
}
}
//real msg no longer needed
String mailbox = getMailboxFolder(null);
String msgfile = mailbox + "/" + filename;
new File(msgfile).delete(); //delete real .msg file
} catch (Exception e) {
JFLog.log(e);
}
}
}
private void setupMailbox() {
if (admin) {
mailbox = getMailboxFolder(null);
} else {
mailbox = getMailboxFolder(user);
}
listFiles();
}
private void listFiles() {
files = new File(mailbox).listFiles(new FilenameFilter() {
public boolean accept(File dir, String name) {
return name.endsWith(".msg");
}
});
}
private String stat() {
if (mailbox == null) {
return "-ERR Mailbox not ready\r\n";
}
int cnt = 0;
int size = 0;
listFiles();
for(File file : files) {
cnt++;
size += file.length();
}
StringBuilder reply = new StringBuilder();
reply.append("+OK ");
reply.append(cnt);
reply.append(" ");
reply.append(size);
reply.append("\r\n");
return reply.toString();
}
private String list() {
if (mailbox == null) {
return "-ERR Mailbox not ready\r\n";
}
int cnt = 0;
int size = 0;
StringBuilder list = new StringBuilder();
int idx = 1;
listFiles();
for(File file : files) {
cnt++;
long msgsize = file.length();
size += msgsize;
list.append(idx++);
list.append(" ");
list.append(msgsize);
list.append("\r\n");
}
StringBuilder reply = new StringBuilder();
reply.append("+OK ");
reply.append(cnt);
reply.append(" ");
reply.append(size);
reply.append("\r\n");
reply.append(list);
reply.append(".\r\n");
return reply.toString();
}
private String uidl(String start) {
if (mailbox == null) {
return "-ERR Mailbox not ready\r\n";
}
int sidx = 1;
if (start.length() > 0) {
sidx = JF.atoi(start);
}
StringBuilder list = new StringBuilder();
int idx = 0;
listFiles();
for(File file : files) {
String filename = file.getName();
int ext = filename.indexOf('.');
filename = filename.substring(0, ext);
idx++;
if (idx < sidx) continue;
list.append(idx); //index
list.append(" T-");
list.append(filename); //unique ID (timestamp when file was created)
list.append("\r\n");
}
StringBuilder reply = new StringBuilder();
reply.append("+OK unique ID list follows...\r\n");
reply.append(list);
reply.append(".\r\n");
return reply.toString();
}
}
private static POP3Server pop3;
public static void serviceStart(String[] args) {
pop3 = new POP3Server();
pop3.start();
}
public static void serviceStop() {
JFLog.log("POP3 : Stopping service");
pop3.stop();
}
private static JBusServer busServer;
private String config;
public static boolean createKeys() {
return KeyMgmt.keytool(new String[] {
"-genkey", "-debug", "-alias", "jfpop3", "-keypass", "password", "-storepass", "password",
"-keystore", getKeyFile(), "-validity", "3650", "-dname", "CN=jfpop3.sourceforge.net, OU=user, O=server, C=CA",
"-keyalg" , "RSA", "-keysize", "2048"
});
}
public static class JBusMethods {
public String getConfig() {
return pop3.config;
}
public boolean setConfig(String cfg) {
//write new file
JFLog.log("setConfig");
try {
FileOutputStream fos = new FileOutputStream(getConfigFile());
fos.write(cfg.getBytes());
fos.close();
return true;
} catch (Exception e) {
JFLog.log(e);
return false;
}
}
public boolean restart() {
JFLog.log("restart");
pop3.stop();
pop3 = new POP3Server();
pop3.start();
return true;
}
public boolean genKeys() {
if (createKeys()) {
JFLog.log("Generated Keys");
return true;
} else {
return false;
}
}
public String getLogFile() {
return POP3Server.getLogFile();
}
}
}