diff --git a/.classpath b/.classpath new file mode 100644 index 0000000..1b11f8b --- /dev/null +++ b/.classpath @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a197d90 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +target/ +sql/ diff --git a/.project b/.project new file mode 100644 index 0000000..2834f44 --- /dev/null +++ b/.project @@ -0,0 +1,37 @@ + + + com.stephenschafer.email + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.wst.common.project.facet.core.builder + + + + + org.eclipse.wst.validation.validationbuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.m2e.core.maven2Nature + org.eclipse.jem.workbench.JavaEMFNature + org.eclipse.wst.common.modulecore.ModuleCoreNature + org.eclipse.wst.common.project.facet.core.nature + org.eclipse.jdt.core.javanature + org.eclipse.wst.jsdt.core.jsNature + + diff --git a/.settings/.jsdtscope b/.settings/.jsdtscope new file mode 100644 index 0000000..92e666d --- /dev/null +++ b/.settings/.jsdtscope @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..cac0df4 --- /dev/null +++ b/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,11 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore +org.eclipse.jdt.core.compiler.release=disabled +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/.settings/org.eclipse.m2e.core.prefs b/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 0000000..f897a7f --- /dev/null +++ b/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/.settings/org.eclipse.wst.common.component b/.settings/org.eclipse.wst.common.component new file mode 100644 index 0000000..c5a01d5 --- /dev/null +++ b/.settings/org.eclipse.wst.common.component @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/.settings/org.eclipse.wst.common.project.facet.core.xml b/.settings/org.eclipse.wst.common.project.facet.core.xml new file mode 100644 index 0000000..5a6bcc7 --- /dev/null +++ b/.settings/org.eclipse.wst.common.project.facet.core.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/.settings/org.eclipse.wst.jsdt.ui.superType.container b/.settings/org.eclipse.wst.jsdt.ui.superType.container new file mode 100644 index 0000000..3bd5d0a --- /dev/null +++ b/.settings/org.eclipse.wst.jsdt.ui.superType.container @@ -0,0 +1 @@ +org.eclipse.wst.jsdt.launching.baseBrowserLibrary \ No newline at end of file diff --git a/.settings/org.eclipse.wst.jsdt.ui.superType.name b/.settings/org.eclipse.wst.jsdt.ui.superType.name new file mode 100644 index 0000000..05bd71b --- /dev/null +++ b/.settings/org.eclipse.wst.jsdt.ui.superType.name @@ -0,0 +1 @@ +Window \ No newline at end of file diff --git a/.settings/org.eclipse.wst.validation.prefs b/.settings/org.eclipse.wst.validation.prefs new file mode 100644 index 0000000..04cad8c --- /dev/null +++ b/.settings/org.eclipse.wst.validation.prefs @@ -0,0 +1,2 @@ +disabled=06target +eclipse.preferences.version=1 diff --git a/WebContent/META-INF/MANIFEST.MF b/WebContent/META-INF/MANIFEST.MF new file mode 100644 index 0000000..5e94951 --- /dev/null +++ b/WebContent/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Class-Path: + diff --git a/WebContent/WEB-INF/web.xml b/WebContent/WEB-INF/web.xml new file mode 100644 index 0000000..f358b33 --- /dev/null +++ b/WebContent/WEB-INF/web.xml @@ -0,0 +1,52 @@ + + + + + + 1440 + + + + PostMapping + com.stephenschafer.email.PostMapping + 1 + + + + PostMapping + /post-mapping + + + + NewAlias + com.stephenschafer.email.NewAlias + + + + NewAlias + /new-alias + + + + reset + com.stephenschafer.email.ResetSession + + + + reset + /reset + + + + logout + com.stephenschafer.email.Logout + + + + logout + /logout + + diff --git a/WebContent/edit.jsp b/WebContent/edit.jsp new file mode 100644 index 0000000..ac8f15d --- /dev/null +++ b/WebContent/edit.jsp @@ -0,0 +1,202 @@ +<%@ page language="java" contentType="text/html; charset=UTF-8" + pageEncoding="UTF-8"%> +<%@ page import="java.util.Map"%> +<%@ page import="java.util.HashMap"%> +<%@ page import="java.util.List"%> +<%@ page import="java.util.ArrayList"%> +<%@ page import="java.util.Set"%> +<%@ page import="java.util.HashSet"%> +<%@ page import="java.util.Collections"%> +<%@ page import="java.util.Comparator"%> +<%@ page import="com.stephenschafer.email.Mapping"%> +<%@ page import="com.stephenschafer.email.Util"%> +<%@ page import="com.stephenschafer.email.User"%> +<% +final String address = request.getParameter("address"); +final boolean mobile = "true".equalsIgnoreCase(request.getParameter("mobile")); +final User user = Util.identify(session); +if (user == null) { + session.setAttribute("return-servlet-path", request.getServletPath()); + session.setAttribute("return-query-string", request.getQueryString()); + response.sendRedirect("login.jsp"); + return; +} +Util.updateSession(session); +@SuppressWarnings("unchecked") +final List targetList = (List) session.getAttribute("target-list"); +final Mapping mapping = address == null ? null : Util.getMapping(address); +final String username = user.getName(); +final String selectedTarget = "sandy".equals(username) ? "sandy" : "elephant".equals(username) ? "steve" : ""; +final String header = mobile ? selectedTarget : "Mapping"; +final String targetDisplay = mobile ? "display: none" : ""; +final String disabledAttr = mapping == null ? "" : mapping.isDisabled() ? " checked" : ""; +final String disableDisplay = mobile ? "display: none" : ""; +final String descriptionValue = mapping == null ? "" : mapping.getDisplayDescription(); +final String dateValue = mapping == null ? "" : mapping.getFormattedDate(); +%> + + + + +Edit Mapping + + + + +

<%=header %>

+
+"/> +"/> + + + + + + + + + + + + + + + + + +<% +if(mapping != null) { +%> + + + + +<% +} +%> + + + + +
Target + +
Email Address +<% +if(mapping == null) { +%> +<% +} +else { +%> + <%=mapping.getAddress()%> +<% +} +%>
Description + +
Date + +
Disabled + /> +
+
+ +<% +if(!mobile) { +%> + +<% +} +%>
+
+ + \ No newline at end of file diff --git a/WebContent/index.jsp b/WebContent/index.jsp new file mode 100644 index 0000000..60b995a --- /dev/null +++ b/WebContent/index.jsp @@ -0,0 +1,361 @@ +<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> +<%@ page import="java.util.Map"%> +<%@ page import="java.util.HashMap"%> +<%@ page import="java.util.List"%> +<%@ page import="java.util.ArrayList"%> +<%@ page import="java.util.Set"%> +<%@ page import="java.util.HashSet"%> +<%@ page import="java.util.Collections"%> +<%@ page import="java.util.Comparator"%> +<%@ page import="com.stephenschafer.email.Logger"%> +<%@ page import="com.stephenschafer.email.Mapping"%> +<%@ page import="com.stephenschafer.email.Util"%> +<%@ page import="com.stephenschafer.email.User"%> +<% +final User user = Util.identify(session); +if (user == null) { + session.setAttribute("return-servlet-path", request.getServletPath()); + session.setAttribute("return-query-string", request.getQueryString()); + response.sendRedirect("login.jsp"); + return; +} +String domainString = request.getParameter("domain"); +if(domainString == null) { + domainString = (String)session.getAttribute("domain-filter"); +} +else { + session.setAttribute("domain-filter", domainString); +} +String targetString = request.getParameter("target"); +if(targetString == null) { + targetString = (String)session.getAttribute("target-filter"); +} +else { + session.setAttribute("target-filter", targetString); +} +String hideDisabledString = request.getParameter("hide-disabled"); +/* +if(hideDisabledString == null) { + hideDisabledString = (String) session.getAttribute("hide-disabled-filter"); +} +else { + session.setAttribute("hide-disabled-filter", hideDisabledString); +} +*/ +final boolean hideDisabled = "hide".equals(hideDisabledString); +Util.updateSession(session); +@SuppressWarnings("unchecked") +final List targetList = (List) session.getAttribute("target-list"); +@SuppressWarnings("unchecked") +final List domainList = (List) session.getAttribute("domain-list"); +final List mappings = Util.getMappings(); +Logger.log(String.format("got %d mappings", Integer.valueOf(mappings.size()))); +final List domainOptions = new ArrayList<>(); +boolean domainFound = false; +for(final String domain : domainList) { + final String selected; + if(domain.equals(domainString)) { + domainFound = true; + selected = " selected"; + } + else { + selected = ""; + } + domainOptions.add(String.format("", domain, selected, domain)); +} +if(!domainFound) { + domainString = null; + session.removeAttribute("domain-filter"); +} +final List targetOptions = new ArrayList<>(); +boolean targetFound = false; +for(final String target : targetList) { + final String selected; + if(target.equals(targetString)) { + targetFound = true; + selected = " selected"; + } + else { + selected = ""; + } + targetOptions.add(String.format("", target, selected, target)); +} +if(!targetFound) { + targetString = null; + session.removeAttribute("target-filter"); +} +final Map> names = new HashMap<>(); +%> + + + + + +Virtual Addresses + + + + +

Virtual Addresses

+
+ + + + + +<% +final String hideDisabledChecked = hideDisabled ? " checked" : ""; +%> +
+ +<% +for(final Mapping mapping : mappings) { + if(!mapping.qualifies(user)) { + continue; + } + if(domainString != null && domainString.trim().length() > 0 && !domainString.equals(mapping.getDomain())) { + continue; + } + if(targetString != null && targetString.trim().length() > 0 && !targetString.equals(mapping.getTarget())) { + continue; + } + if(hideDisabled && mapping.isDisabled()) { + continue; + } + final String disabledClass = mapping.isDisabled() ? " disabled" : ""; +%> + + + + + + + + + + +<% + if(!mapping.isDisabled()) { + String mappingDomain = mapping.getDomain(); + if(mappingDomain == null) { + mappingDomain = ""; + } + Set namesForDomain = names.get(mappingDomain); + if(namesForDomain == null) { + namesForDomain = new HashSet<>(); + names.put(mappingDomain, namesForDomain); + } + namesForDomain.add(mapping.getName()); + } +} + +%>
<%=mapping.getAddress()%><%=mapping.getTarget()%><%=mapping.getDisplayDescription()%><%=mapping.isDisabled() ? "disabled" : "" %><%=mapping.getFormattedDate() %>
+<% +final List domains = new ArrayList<>(names.keySet()); +Collections.sort(domains); +for(String mappingDomain : domains) { + List namesForDomain = new ArrayList<>(names.get(mappingDomain)); + Collections.sort(namesForDomain); +%>
+

Names for <%=mappingDomain %>

+<% + String sep = ""; + for(final String name : namesForDomain) { + out.print(sep); + sep = ", "; + out.println(name); + } +%>
+<% +} +%> + \ No newline at end of file diff --git a/WebContent/login.jsp b/WebContent/login.jsp new file mode 100644 index 0000000..db32369 --- /dev/null +++ b/WebContent/login.jsp @@ -0,0 +1,121 @@ +<%@ page language="java" contentType="text/html; charset=UTF-8" + pageEncoding="UTF-8"%> +<%@ page import="com.stephenschafer.email.Util"%> +<%@ page import="com.stephenschafer.email.Configuration"%> +<%@ page import="com.stephenschafer.email.Session"%> +<%@ page import="com.stephenschafer.email.User"%> +<%@ page import="com.stephenschafer.email.Logger"%> +<% + final boolean failed; + final String username = request.getParameter("username"); + User user = null; + if ("post".equalsIgnoreCase(request.getMethod())) { + final String password = request.getParameter("password"); + user = Util.login(username, password); + failed = true; + } + else { + final String privilegedHost = Configuration.INSTANCE.getPrivilegedHost(); + if (username != null && privilegedHost != null ) { + String remoteHost = request.getRemoteHost(); + if (privilegedHost.equals(remoteHost)) { + user = Util.login(username); + } + } + failed = false; + session.removeAttribute("email-session"); + } + if (user != null) { + Session timesheetSession = new Session(); + timesheetSession.setUser(user); + session.setAttribute("email-session", timesheetSession); + String returnServletPath = (String) session.getAttribute("return-servlet-path"); + if (returnServletPath == null) { + returnServletPath = "/index.jsp"; + } + String returnQueryString = (String) session.getAttribute("return-query-string"); + if (returnQueryString == null) { + returnQueryString = ""; + } + final String returnUrl = request.getContextPath() + returnServletPath + "?" + + returnQueryString; + response.sendRedirect(returnUrl); + return; + } +%> + + + +Login + + + + <% + if (failed) { + %>
That didn't work.
+ <% + } + %>
+ + + + + + + + + +
Username" />
Password
+
+ +
+
+ + diff --git a/WebContent/mapping.jsp b/WebContent/mapping.jsp new file mode 100644 index 0000000..257df4f --- /dev/null +++ b/WebContent/mapping.jsp @@ -0,0 +1,133 @@ +<%@ page language="java" contentType="text/html; charset=UTF-8" + pageEncoding="UTF-8"%> +<%@ page import="java.util.Map"%> +<%@ page import="java.util.HashMap"%> +<%@ page import="java.util.List"%> +<%@ page import="java.util.ArrayList"%> +<%@ page import="java.util.Set"%> +<%@ page import="java.util.HashSet"%> +<%@ page import="java.util.Collections"%> +<%@ page import="java.util.Comparator"%> +<%@ page import="com.stephenschafer.email.Mapping"%> +<%@ page import="com.stephenschafer.email.Util"%> +<%@ page import="com.stephenschafer.email.User"%> +<% +final String address = request.getParameter("address"); +final User user = Util.identify(session); +if (user == null) { + session.setAttribute("return-servlet-path", request.getServletPath()); + session.setAttribute("return-query-string", request.getQueryString()); + response.sendRedirect("login.jsp"); + return; +} +Util.updateSession(session); +@SuppressWarnings("unchecked") +final List targetList = (List) session.getAttribute("target-list"); +final Mapping mapping = address == null ? null : Util.getMapping(address); +final String username = user.getName(); +final String header = "Mapping"; +final String descriptionValue = mapping == null ? "" : mapping.getDisplayDescription(); +final String dateValue = mapping == null ? "" : mapping.getFormattedDate(); +%> + + + + +Mapping + + + +

<%=header %>

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Target + <%=mapping.getTarget()%> +
Email Address + <%=mapping.getAddress()%> +
Description + <%=mapping.getDisplayDescription()%> +
Date + <%=mapping.getFormattedDate()%> +
Disabled + <%=mapping.isDisabled() ? "true" : "false" %> +
+
+ +
+ + \ No newline at end of file diff --git a/WebContent/password.jsp b/WebContent/password.jsp new file mode 100644 index 0000000..97af3e5 --- /dev/null +++ b/WebContent/password.jsp @@ -0,0 +1,87 @@ +<%@ page language="java" contentType="text/html; charset=UTF-8" + pageEncoding="UTF-8"%> +<%@ page import="com.stephenschafer.email.Util"%> +<%@ page import="com.stephenschafer.email.Session"%> +<%@ page import="com.stephenschafer.email.User"%> +<% + final User user = Util.identify(session); + if (user == null) { + session.setAttribute("return-servlet-path", request.getServletPath()); + session.setAttribute("return-query-string", request.getQueryString()); + response.sendRedirect("login.jsp"); + return; + } + String message = null; + if ("post".equalsIgnoreCase(request.getMethod())) { + final String password1 = request.getParameter("password1"); + final String password2 = request.getParameter("password2"); + if (!password1.equals(password2)) { + message = "Passwords don't match."; + } + else { + Util.changePassword(user.getId(), password1); + response.sendRedirect("index.jsp"); + return; + } + } + else { + message = null; + } +%> + + + +Password + + + + <% + if (message != null) { + %>
+ <%=message%> +
+ <% + } + %>
+ + + + + + + + + + + + + +
Password
Confirm password
Display name
+
+ +
+
+ + diff --git a/WebContent/register.jsp b/WebContent/register.jsp new file mode 100644 index 0000000..6b57e2f --- /dev/null +++ b/WebContent/register.jsp @@ -0,0 +1,100 @@ +<%@ page language="java" contentType="text/html; charset=UTF-8" + pageEncoding="UTF-8"%> +<%@ page import="com.stephenschafer.email.Util"%> +<%@ page import="com.stephenschafer.email.Session"%> +<%@ page import="com.stephenschafer.email.User"%> +<% +final String message; +if("post".equalsIgnoreCase(request.getMethod())) { + final String username = request.getParameter("username"); + final String password1 = request.getParameter("password1"); + final String password2 = request.getParameter("password2"); + final String displayName = request.getParameter("displayName"); + final boolean canWrite = "true".equals(request.getParameter("canWrite")); + if(!password1.equals(password2)) { + message = "Passwords don't match."; + } + else { + final User user = Util.register(username, password1, displayName, canWrite); + if(user != null) { + Session emailSession = new Session(); + emailSession.setUser(user); + session.setAttribute("email-session", emailSession); + response.sendRedirect("index.jsp"); + return; + } + message = "That didn't work."; + } +} +else { + message = null; +} + +%> + + + +Register + + + +<% +if(message != null) { + +%>
+<%=message %> +
+<% +} + +%>
+ + + + + + + + + + + + + + + + + + + + + +
Username
Password
Confirm password
Display name
Can write
+
+ +
+
+ + diff --git a/build b/build new file mode 100755 index 0000000..7360952 --- /dev/null +++ b/build @@ -0,0 +1,9 @@ +#!/bin/sh +cd "$(dirname "${BASH_SOURCE[0]}")" +ROOT=$(pwd) +mkdir -p logs +if ! mvn clean package > logs/build.log 2> logs/build.err.log; then + echo "build failed" + exit 1 +fi +echo "success" \ No newline at end of file diff --git a/deploy b/deploy new file mode 100755 index 0000000..3ee59f4 --- /dev/null +++ b/deploy @@ -0,0 +1,5 @@ +#!/bin/sh +cd "$(dirname "${BASH_SOURCE[0]}")" +TIMESTAMP=$(date +%Y%m%d-%H%M%S) +ssh pi@raspi "mv ~/tomcat/webapps/email.war ~/tomcat/webapps/email.war_$TIMESTAMP" +scp target/com.stephenschafer.email-manager-0.0.2-SNAPSHOT.war pi@raspi:~/tomcat/webapps/email.war \ No newline at end of file diff --git a/logs/build.err.log b/logs/build.err.log new file mode 100644 index 0000000..e69de29 diff --git a/logs/build.log b/logs/build.log new file mode 100644 index 0000000..e61a20b --- /dev/null +++ b/logs/build.log @@ -0,0 +1,42 @@ +[INFO] Scanning for projects... +[INFO] +[INFO] --< com.stephenschafer.email-manager:com.stephenschafer.email-manager >-- +[INFO] Building com.stephenschafer.email-manager 0.0.2-SNAPSHOT +[INFO] from pom.xml +[INFO] --------------------------------[ war ]--------------------------------- +[INFO] +[INFO] --- clean:3.2.0:clean (default-clean) @ com.stephenschafer.email-manager --- +[INFO] Deleting /disk1/home/sschafer/projects/com.stephenschafer.email/target +[INFO] +[INFO] --- resources:3.3.1:resources (default-resources) @ com.stephenschafer.email-manager --- +[WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent! +[INFO] skip non existing resourceDirectory /disk1/home/sschafer/projects/com.stephenschafer.email/src/main/resources +[INFO] +[INFO] --- compiler:3.13.0:compile (default-compile) @ com.stephenschafer.email-manager --- +[INFO] Recompiling the module because of changed source code. +[WARNING] File encoding has not been set, using platform encoding UTF-8, i.e. build is platform dependent! +[INFO] Compiling 14 source files with javac [debug target 1.8] to target/classes +[WARNING] bootstrap class path not set in conjunction with -source 8 +[INFO] +[INFO] --- resources:3.3.1:testResources (default-testResources) @ com.stephenschafer.email-manager --- +[WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent! +[INFO] skip non existing resourceDirectory /disk1/home/sschafer/projects/com.stephenschafer.email/src/test/resources +[INFO] +[INFO] --- compiler:3.13.0:testCompile (default-testCompile) @ com.stephenschafer.email-manager --- +[INFO] No sources to compile +[INFO] +[INFO] --- surefire:3.2.2:test (default-test) @ com.stephenschafer.email-manager --- +[INFO] No tests to run. +[INFO] +[INFO] --- war:3.4.0:war (default-war) @ com.stephenschafer.email-manager --- +[INFO] Packaging webapp +[INFO] Assembling webapp [com.stephenschafer.email-manager] in [/disk1/home/sschafer/projects/com.stephenschafer.email/target/com.stephenschafer.email-manager-0.0.2-SNAPSHOT] +[INFO] Processing war project +[INFO] Copying webapp resources [/disk1/home/sschafer/projects/com.stephenschafer.email/WebContent] +[INFO] Building war: /disk1/home/sschafer/projects/com.stephenschafer.email/target/com.stephenschafer.email-manager-0.0.2-SNAPSHOT.war +[INFO] ------------------------------------------------------------------------ +[INFO] BUILD SUCCESS +[INFO] ------------------------------------------------------------------------ +[INFO] Total time: 2.078 s +[INFO] Finished at: 2024-11-28T08:07:33-07:00 +[INFO] ------------------------------------------------------------------------ diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..2c2b0fd --- /dev/null +++ b/pom.xml @@ -0,0 +1,51 @@ + + 4.0.0 + com.stephenschafer.email-manager + com.stephenschafer.email-manager + 0.0.2-SNAPSHOT + war + + src + + + maven-compiler-plugin + 3.13.0 + + 1.8 + 1.8 + + + + maven-war-plugin + 3.4.0 + + WebContent + + + + + + + javax.servlet + javax.servlet-api + 4.0.1 + + + mysql + mysql-connector-java + 5.1.38 + + + com.fasterxml.jackson.core + jackson-core + 2.18.1 + + + com.fasterxml.jackson.core + jackson-databind + 2.18.1 + + + \ No newline at end of file diff --git a/src/com/stephenschafer/email/Configuration.java b/src/com/stephenschafer/email/Configuration.java new file mode 100644 index 0000000..128ec0b --- /dev/null +++ b/src/com/stephenschafer/email/Configuration.java @@ -0,0 +1,120 @@ +package com.stephenschafer.email; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Properties; + +import javax.naming.InitialContext; +import javax.naming.NamingException; +import javax.sql.DataSource; + +public class Configuration { + public static final Configuration INSTANCE; + private String jndiName; + private DbConnectionPool pool; + private String privilegedHost; + private String defaultDomain; + private boolean loaded; + + private Configuration() { + jndiName = null; + pool = null; + loaded = false; + } + + public void load() throws IOException, ClassNotFoundException, SQLException { + if (loaded) { + return; + } + final Properties systemProps = System.getProperties(); + final String catalinaHome = systemProps.getProperty("catalina.home"); + String propertiesFileName = System.getenv("EMAIL_PROPERTIES"); + if (propertiesFileName == null) { + propertiesFileName = catalinaHome + "/conf/Catalina/localhost/email.properties"; + } + final File propertiesFile = new File(propertiesFileName); + final Properties properties = new Properties(); + try (FileInputStream fis = new FileInputStream(propertiesFile)) { + properties.load(fis); + } + Logger.logFilename = properties.getProperty("log.filename"); + Logger.log("********************************* Starting"); + jndiName = properties.getProperty("db.jndi"); + final String url = properties.getProperty("db.url"); + final String username = properties.getProperty("db.username"); + final String password = properties.getProperty("db.password"); + if (jndiName == null) { + pool = new DbConnectionPool("com.mysql.jdbc.Driver", url, username, password); + } + privilegedHost = properties.getProperty("priv-host"); + final String defaultDomain = properties.getProperty("default-domain"); + this.defaultDomain = defaultDomain == null ? "schafer.cc" : defaultDomain; + Logger.log("initialized, jndiName = " + jndiName); + loaded = true; + } + + public Connection getConnection() throws SQLException, NamingException { + final String jndiName = getJndiName(); + if (jndiName != null) { + final InitialContext initialContext = new InitialContext(); + final DataSource datasource = (DataSource) initialContext.lookup(jndiName); + SQLException lastException = null; + int i = 0; + while (i < 4) { + try { + return datasource.getConnection(); + } + catch (final SQLException e) { + Logger.log("Failed to get connection, " + (3 - i) + " retrys left", e); + lastException = e; + try { + Thread.sleep(5000L); + } + catch (final InterruptedException ex) { + } + ++i; + continue; + } + } + if (lastException != null) { + throw lastException; + } + } + return getPool().getConnection(8, false, true); + } + + public String getJndiName() { + return jndiName; + } + + public void setJndiName(final String jndiName) { + this.jndiName = jndiName; + } + + public DbConnectionPool getPool() { + return pool; + } + + public void setPool(final DbConnectionPool pool) { + this.pool = pool; + } + + static { + INSTANCE = new Configuration(); + } + + public String getPrivilegedHost() { + return privilegedHost; + } + + public String getDefaultDomain() { + return defaultDomain; + } + + public void setDefaultDomain(final String defaultDomain) { + this.defaultDomain = defaultDomain; + } +} diff --git a/src/com/stephenschafer/email/DbConnectionPool.java b/src/com/stephenschafer/email/DbConnectionPool.java new file mode 100644 index 0000000..b441eb9 --- /dev/null +++ b/src/com/stephenschafer/email/DbConnectionPool.java @@ -0,0 +1,693 @@ +package com.stephenschafer.email; + +import java.io.Serializable; +import java.net.SocketException; +import java.sql.Array; +import java.sql.Blob; +import java.sql.CallableStatement; +import java.sql.Clob; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.DriverManager; +import java.sql.NClob; +import java.sql.PreparedStatement; +import java.sql.SQLClientInfoException; +import java.sql.SQLException; +import java.sql.SQLWarning; +import java.sql.SQLXML; +import java.sql.Savepoint; +import java.sql.Statement; +import java.sql.Struct; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Queue; +import java.util.concurrent.Executor; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class DbConnectionPool implements Serializable { + private static final long serialVersionUID = 1L; + private static final Logger LOGGER; + private final String uRL; + private final String username; + private final String password; + private final String driver; + private final Queue connections; + private long openConnectionIndex; + private final Map openConnections; + private long timeout; + + public DbConnectionPool(final String dbDriver, final String dbName, final String dbUsername, + final String dbPassword) throws ClassNotFoundException, SQLException { + this.connections = new LinkedList<>(); + this.openConnectionIndex = 0L; + this.openConnections = new HashMap<>(); + this.timeout = 900000L; + this.driver = dbDriver; + this.uRL = dbName; + this.username = dbUsername; + this.password = dbPassword; + this.validateConnection(); + } + + public final void validateConnection() throws ClassNotFoundException, SQLException { + DbConnectionPool.LOGGER.log(Level.FINEST, "Testing connection pool"); + DbConnectionPool.LOGGER.log(Level.FINEST, "Instantiating " + this.driver + "\n"); + Class.forName(this.driver); + DbConnectionPool.LOGGER.log(Level.FINEST, "Connecting"); + final Connection connection = DriverManager.getConnection(this.uRL, this.username, + this.password); + try { + final DatabaseMetaData dbmd = connection.getMetaData(); + DbConnectionPool.LOGGER.log(Level.FINEST, + "Connection to " + dbmd.getDatabaseProductName() + " " + + dbmd.getDatabaseProductVersion() + " successful."); + } + finally { + connection.close(); + } + } + + public final void clear() throws SQLException { + synchronized (this) { + while (!this.connections.isEmpty()) { + final PooledConnection connection = this.connections.remove(); + connection.reallyClose(); + } + } + } + + public final PooledConnection getConnection(final int transactionIsolation, + final boolean readOnly, final boolean autoCommit) throws SQLException { + while (true) { + PooledConnection oldConnection; + final long timeout; + synchronized (this) { + if (this.connections.isEmpty()) { + oldConnection = null; + } + else { + oldConnection = this.connections.remove(); + } + timeout = this.timeout; + } + if (oldConnection == null) { + DbConnectionPool.LOGGER.log(Level.FINEST, "Connecting"); + SQLException exception = null; + int retryCount = 0; + while (retryCount < 10) { + Connection newConnection; + try { + newConnection = DriverManager.getConnection(this.uRL, this.username, + this.password); + } + catch (final SQLException e) { + if (!(e.getCause() instanceof SocketException)) { + throw e; + } + exception = e; + DbConnectionPool.LOGGER.log(Level.FINEST, "Retrying", e); + try { + Thread.sleep(1000L); + } + catch (final InterruptedException ex) { + } + ++retryCount; + newConnection = null; + } + if (newConnection != null) { + newConnection.setAutoCommit(true); + newConnection.setTransactionIsolation(transactionIsolation); + newConnection.setReadOnly(readOnly); + newConnection.setAutoCommit(autoCommit); + final long index = this.incrementOpenConnectionCount(); + return new PooledConnection(newConnection, index); + } + } + if (exception != null) { + throw exception; + } + } + if (oldConnection != null) { + if (oldConnection.isClosed()) { + DbConnectionPool.LOGGER.log(Level.WARNING, + "Pooled connection was already closed"); + } + else { + Label_0439: { + Label_0428: { + if (timeout != 0L) { + if (System.currentTimeMillis() + - oldConnection.getLastAccess() >= timeout) { + break Label_0428; + } + } + try { + oldConnection.setAutoCommit(true); + oldConnection.setTransactionIsolation(transactionIsolation); + oldConnection.setReadOnly(readOnly); + oldConnection.setAutoCommit(autoCommit); + synchronized (this) { + final Long key = oldConnection.getIndex(); + final OpenConnectionInfo info = this.openConnections.get(key); + if (info != null) { + DbConnectionPool.LOGGER.log(Level.WARNING, + "Overwriting open connection info: " + key + " " + + info); + } + this.openConnections.put(key, new OpenConnectionInfo()); + } + return oldConnection; + } + catch (final Exception e2) { + DbConnectionPool.LOGGER.log(Level.SEVERE, + "Unable to reuse DB connection", e2); + break Label_0439; + } + } + DbConnectionPool.LOGGER.log(Level.FINEST, "DB connection timed out"); + try { + oldConnection.reallyClose(); + } + catch (final Exception e2) { + DbConnectionPool.LOGGER.log(Level.SEVERE, + "Unable to really close DB connection", e2); + } + } + } + } + } + } + + private long incrementOpenConnectionCount() { + synchronized (this) { + final long index = this.openConnectionIndex++; + final Long key = index; + final OpenConnectionInfo info = this.openConnections.get(key); + if (info != null) { + DbConnectionPool.LOGGER.log(Level.WARNING, + "Overwriting open connection info: " + key + " " + info); + } + this.openConnections.put(key, new OpenConnectionInfo()); + return index; + } + } + + protected final void add(final PooledConnection connection) { + synchronized (this) { + final OpenConnectionInfo info = this.openConnections.remove(connection.getIndex()); + if (info == null) { + DbConnectionPool.LOGGER.log(Level.WARNING, + "adding orphaned connection: " + connection.getIndex()); + } + else { + this.connections.offer(connection); + } + } + } + + public final long getOpenConnectionIndex() { + synchronized (this) { + return this.openConnectionIndex; + } + } + + public final OpenConnectionInfo getOpenConnectionInfo(final long index) { + synchronized (this) { + return this.openConnections.get(index); + } + } + + public final int getOpenConnectionCount() { + synchronized (this) { + return this.openConnections.size(); + } + } + + public final int getPooledConnectionCount() { + synchronized (this) { + return this.connections.size(); + } + } + + @Override + public final String toString() { + final StringBuilder buf = new StringBuilder(); + buf.append("DB "); + buf.append(this.uRL); + buf.append(" "); + synchronized (this) { + buf.append("open: "); + buf.append(this.openConnections.size()); + buf.append(", pooled: "); + buf.append(this.connections.size()); + buf.append(", next: "); + buf.append(this.openConnectionIndex); + } + return buf.toString(); + } + + public final long getTimeout() { + synchronized (this) { + return this.timeout; + } + } + + public final void setTimeout(final long timeout) { + synchronized (this) { + this.timeout = timeout; + } + } + + public final String getDriver() { + return this.driver; + } + + public final String getURL() { + return this.uRL; + } + + public final String getPassword() { + return this.password; + } + + public final String getUsername() { + return this.username; + } + + public synchronized Map getOpenConnections() { + final Map map = new HashMap<>(); + for (final Long index : this.openConnections.keySet()) { + final OpenConnectionInfo info = this.openConnections.get(index); + map.put(index, info.timestamp); + } + return map; + } + + static { + LOGGER = Logger.getLogger(DbConnectionPool.class.getName()); + } + + public static final class OpenConnectionInfo { + public final long timestamp; + private final List stackTrace; + + public OpenConnectionInfo() { + this.timestamp = System.currentTimeMillis(); + final StackTraceElement[] steArray = Thread.currentThread().getStackTrace(); + final List stackTrace = new ArrayList<>( + steArray.length); + for (final StackTraceElement element : steArray) { + stackTrace.add(element); + } + this.stackTrace = Collections.unmodifiableList( + (List) stackTrace); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + sb.append("Timestamp: "); + sb.append(new Date(this.timestamp)); + sb.append("\n"); + for (final StackTraceElement element : this.stackTrace) { + sb.append(element); + sb.append("\n"); + } + return sb.toString(); + } + } + + public class PooledConnection implements Connection { + private final Connection connection; + private long lastAccess; + private boolean autoCommit; + private boolean readOnly; + private final long index; + + public PooledConnection(final Connection connection, final long index) { + this.lastAccess = System.currentTimeMillis(); + this.autoCommit = false; + this.readOnly = false; + this.index = index; + this.connection = connection; + try { + this.autoCommit = connection.getAutoCommit(); + } + catch (final SQLException e) { + DbConnectionPool.LOGGER.log(Level.WARNING, "Unable to get auto commit", e); + } + try { + this.readOnly = connection.isReadOnly(); + } + catch (final SQLException e) { + DbConnectionPool.LOGGER.log(Level.WARNING, "Unable to get read only", e); + } + } + + public void clearPool() throws SQLException { + DbConnectionPool.this.clear(); + } + + @Override + public String nativeSQL(final String sql) throws SQLException { + return this.connection.nativeSQL(sql); + } + + @Override + public int hashCode() { + return this.connection.hashCode(); + } + + @Override + public Map> getTypeMap() throws SQLException { + return this.connection.getTypeMap(); + } + + @Override + public PreparedStatement prepareStatement(final String sql) throws SQLException { + final PreparedStatement stmt = this.connection.prepareStatement(sql); + if (this.connection.getAutoCommit() != this.autoCommit + || this.connection.isReadOnly() != this.readOnly) { + final StringBuilder sb = new StringBuilder(); + String sep = ""; + if (this.connection.getAutoCommit() != this.autoCommit) { + sb.append(sep); + sep = " and "; + sb.append("autoCommit has changed from " + this.autoCommit); + } + if (this.connection.isReadOnly() != this.readOnly) { + sb.append(sep); + sep = " and "; + sb.append("readOnly has changed from " + this.readOnly); + } + sb.append(" in "); + sb.append(stmt); + DbConnectionPool.LOGGER.log(Level.WARNING, sb.toString()); + } + return stmt; + } + + @Override + public void setTransactionIsolation(final int level) throws SQLException { + this.connection.setTransactionIsolation(level); + } + + @Override + public String getCatalog() throws SQLException { + return this.connection.getCatalog(); + } + + @Override + public int getTransactionIsolation() throws SQLException { + return this.connection.getTransactionIsolation(); + } + + @Override + public void releaseSavepoint(final Savepoint savepoint) throws SQLException { + this.connection.releaseSavepoint(savepoint); + } + + @Override + public int getHoldability() throws SQLException { + return this.connection.getHoldability(); + } + + @Override + public CallableStatement prepareCall(final String sql, final int resultSetType, + final int resultSetConcurrency, final int resultSetHoldability) + throws SQLException { + return this.connection.prepareCall(sql, resultSetType, resultSetConcurrency, + resultSetHoldability); + } + + @Override + public boolean getAutoCommit() throws SQLException { + return this.connection.getAutoCommit(); + } + + @Override + public Statement createStatement() throws SQLException { + return this.connection.createStatement(); + } + + @Override + public CallableStatement prepareCall(final String sql) throws SQLException { + return this.connection.prepareCall(sql); + } + + @Override + public void setAutoCommit(final boolean autoCommit) throws SQLException { + this.autoCommit = autoCommit; + this.connection.setAutoCommit(autoCommit); + } + + @Override + public PreparedStatement prepareStatement(final String sql, final int autoGeneratedKeys) + throws SQLException { + return this.connection.prepareStatement(sql, autoGeneratedKeys); + } + + @Override + public void setReadOnly(final boolean readOnly) throws SQLException { + this.readOnly = readOnly; + this.connection.setReadOnly(readOnly); + } + + @Override + public CallableStatement prepareCall(final String sql, final int resultSetType, + final int resultSetConcurrency) throws SQLException { + return this.connection.prepareCall(sql, resultSetType, resultSetConcurrency); + } + + @Override + public SQLWarning getWarnings() throws SQLException { + return this.connection.getWarnings(); + } + + @Override + public PreparedStatement prepareStatement(final String sql, final int resultSetType, + final int resultSetConcurrency) throws SQLException { + return this.connection.prepareStatement(sql, resultSetType, resultSetConcurrency); + } + + @Override + public boolean equals(final Object obj) { + return this.connection.equals(obj); + } + + @Override + public PreparedStatement prepareStatement(final String sql, final int[] columnIndexes) + throws SQLException { + return this.connection.prepareStatement(sql, columnIndexes); + } + + @Override + public boolean isClosed() throws SQLException { + return this.connection.isClosed(); + } + + @Override + public PreparedStatement prepareStatement(final String sql, final int resultSetType, + final int resultSetConcurrency, final int resultSetHoldability) + throws SQLException { + return this.connection.prepareStatement(sql, resultSetType, resultSetConcurrency, + resultSetHoldability); + } + + @Override + public void commit() throws SQLException { + this.connection.commit(); + } + + @Override + public void clearWarnings() throws SQLException { + this.connection.clearWarnings(); + } + + @Override + public void setCatalog(final String catalog) throws SQLException { + this.connection.setCatalog(catalog); + } + + @Override + public void close() { + DbConnectionPool.this.add(this); + this.lastAccess = System.currentTimeMillis(); + } + + public void reallyClose() throws SQLException { + this.connection.close(); + } + + @Override + public String toString() { + return this.connection.toString(); + } + + @Override + public DatabaseMetaData getMetaData() throws SQLException { + return this.connection.getMetaData(); + } + + @Override + public void rollback() throws SQLException { + this.connection.rollback(); + } + + @Override + public Savepoint setSavepoint(final String name) throws SQLException { + return this.connection.setSavepoint(name); + } + + @Override + public boolean isReadOnly() throws SQLException { + return this.connection.isReadOnly(); + } + + @Override + public Statement createStatement(final int resultSetType, final int resultSetConcurrency) + throws SQLException { + return this.connection.createStatement(resultSetType, resultSetConcurrency); + } + + @Override + public void rollback(final Savepoint savepoint) throws SQLException { + this.connection.rollback(savepoint); + } + + @Override + public PreparedStatement prepareStatement(final String sql, final String[] columnNames) + throws SQLException { + return this.connection.prepareStatement(sql, columnNames); + } + + @Override + public Savepoint setSavepoint() throws SQLException { + return this.connection.setSavepoint(); + } + + @Override + public Statement createStatement(final int resultSetType, final int resultSetConcurrency, + final int resultSetHoldability) throws SQLException { + return this.connection.createStatement(resultSetType, resultSetConcurrency, + resultSetHoldability); + } + + @Override + public void setTypeMap(final Map> map) throws SQLException { + this.connection.setTypeMap(map); + } + + @Override + public void setHoldability(final int holdability) throws SQLException { + this.connection.setHoldability(holdability); + } + + public long getLastAccess() { + return this.lastAccess; + } + + @Override + public Array createArrayOf(final String arg0, final Object[] arg1) throws SQLException { + return this.connection.createArrayOf(arg0, arg1); + } + + @Override + public Blob createBlob() throws SQLException { + return this.connection.createBlob(); + } + + @Override + public Clob createClob() throws SQLException { + return this.connection.createClob(); + } + + @Override + public NClob createNClob() throws SQLException { + return this.connection.createNClob(); + } + + @Override + public SQLXML createSQLXML() throws SQLException { + return this.connection.createSQLXML(); + } + + @Override + public Struct createStruct(final String arg0, final Object[] arg1) throws SQLException { + return this.connection.createStruct(arg0, arg1); + } + + @Override + public Properties getClientInfo() throws SQLException { + return this.connection.getClientInfo(); + } + + @Override + public String getClientInfo(final String arg0) throws SQLException { + return this.connection.getClientInfo(arg0); + } + + @Override + public boolean isValid(final int arg0) throws SQLException { + return this.connection.isValid(arg0); + } + + @Override + public void setClientInfo(final Properties arg0) throws SQLClientInfoException { + this.connection.setClientInfo(arg0); + } + + @Override + public void setClientInfo(final String arg0, final String arg1) + throws SQLClientInfoException { + this.connection.setClientInfo(arg0, arg1); + } + + @Override + public boolean isWrapperFor(final Class arg0) throws SQLException { + return this.connection.isWrapperFor(arg0); + } + + @Override + public T unwrap(final Class arg0) throws SQLException { + return this.connection.unwrap(arg0); + } + + public long getIndex() { + return this.index; + } + + @Override + public void setSchema(final String schema) throws SQLException { + this.connection.setSchema(schema); + } + + @Override + public String getSchema() throws SQLException { + return this.connection.getSchema(); + } + + @Override + public void abort(final Executor executor) throws SQLException { + this.connection.abort(executor); + } + + @Override + public void setNetworkTimeout(final Executor executor, final int milliseconds) + throws SQLException { + this.connection.setNetworkTimeout(executor, milliseconds); + } + + @Override + public int getNetworkTimeout() throws SQLException { + return this.connection.getNetworkTimeout(); + } + } +} diff --git a/src/com/stephenschafer/email/GlobalCache.java b/src/com/stephenschafer/email/GlobalCache.java new file mode 100644 index 0000000..4e7713c --- /dev/null +++ b/src/com/stephenschafer/email/GlobalCache.java @@ -0,0 +1,190 @@ +package com.stephenschafer.email; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.sql.SQLException; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.naming.NamingException; + +import com.stephenschafer.email.Util.SaveMappingsResult; + +public class GlobalCache { + public static final GlobalCache INSTANCE = new GlobalCache(); + private static final Pattern COMMENT_PATTERN = Pattern.compile("^ *# *(.*)$"); + private static final Pattern DATE_PATTERN = Pattern.compile( + "^ *# *@([0-9]{4}-[0-9]{2}-[0-9]{2})$"); + private static final Pattern MAPPING_PATTERN = Pattern.compile("^ *([^ ]+) +(.*)$"); + private static final Pattern DISABLED_PATTERN = Pattern.compile("^ *# *- *([^ ]+) +(.*)$"); + private boolean initialized = false; + + public synchronized void initialize() throws IOException, SQLException, NamingException { + if (!initialized) { + if (Util.getMappingsCountFromDb() == 0) { + parseVirtualFile(); + } + initialized = true; + } + } + + private synchronized static void parseVirtualFile() + throws IOException, SQLException, NamingException { + Logger.log("parsing virtual file"); + final DateFormat df = new SimpleDateFormat("yyyy-MM-dd"); + final File file = new File("/etc/postfix/virtual"); + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + String line = reader.readLine(); + String previousComment = null; + Date previousDate = null; + while (line != null) { + final Matcher disabledMatcher = DISABLED_PATTERN.matcher(line); + if (disabledMatcher.matches()) { + final String address = disabledMatcher.group(1); + final String target = disabledMatcher.group(2); + final Mapping mapping = new Mapping(address, target, previousComment, + previousDate, true); + Util.addMappingToDb(mapping); + previousComment = null; + previousDate = null; + } + else { + final Matcher datedCommentMatcher = DATE_PATTERN.matcher(line); + if (datedCommentMatcher.matches()) { + try { + final String dateString = datedCommentMatcher.group(1); + Logger.log(String.format("found date: %s", dateString)); + previousDate = df.parse(dateString); + } + catch (final ParseException e) { + Logger.log("Failed to parse date from virtual", e); + previousDate = null; + } + } + else { + final Matcher commentMatcher = COMMENT_PATTERN.matcher(line); + if (commentMatcher.matches()) { + previousComment = commentMatcher.group(1); + } + else { + final Matcher mappingMatcher = MAPPING_PATTERN.matcher(line); + if (mappingMatcher.matches()) { + final String from = mappingMatcher.group(1); + final String to = mappingMatcher.group(2); + final Mapping mapping = new Mapping(from, to, previousComment, + previousDate, false); + Util.addMappingToDb(mapping); + previousComment = null; + previousDate = null; + } + } + } + } + line = reader.readLine(); + } + } + } + + public synchronized SaveMappingsResult writeVirtualFile() + throws IOException, InterruptedException, SQLException, NamingException { + final DateFormat df = new SimpleDateFormat("yyyy-MM-dd"); + final File userHome = new File(System.getProperty("user.home")); + final File stagingDir = new File(userHome, "staging"); + stagingDir.mkdirs(); + final File file = new File(stagingDir, "virtual"); + if (file.exists()) { + file.delete(); + } + file.createNewFile(); + try (PrintWriter writer = new PrintWriter(new FileWriter(file))) { + final List list = Util.getMappingsFromDb(); + for (final Mapping mapping : list) { + final String description = mapping.getDescription(); + if (description != null) { + final Date date = mapping.getDate(); + if (date != null) { + writer.println(String.format("#@%s", df.format(date))); + } + writer.println(String.format("# %s", description)); + } + final boolean disabled = mapping.isDisabled(); + if (disabled) { + writer.print("#-"); + } + writer.println(String.format("%s %s", mapping.getAddress(), mapping.getTarget())); + } + } + final Process process = Runtime.getRuntime().exec( + new File(userHome, "update-virtual").getAbsolutePath()); + final String error = Util.streamToString(process.getErrorStream()); + final String output = Util.streamToString(process.getInputStream()); + final int exitValue = process.waitFor(); + return new SaveMappingsResult() { + @Override + public int getExitValue() { + return exitValue; + } + + @Override + public String getOutput() { + return output; + } + + @Override + public String getError() { + return error; + } + }; + } + + public synchronized boolean addressExists(final String address) + throws SQLException, NamingException, IOException { + initialize(); + return Util.getMappingFromDb(address) != null; + } + + public synchronized void addMapping(final Mapping mapping) + throws IOException, SQLException, NamingException { + initialize(); + Util.addMappingToDb(mapping); + } + + public synchronized void addHistory(final Mapping mapping) + throws IOException, SQLException, NamingException { + initialize(); + Util.addHistoryToDb(mapping); + } + + public void updateMapping(final Mapping mapping) + throws IOException, SQLException, NamingException { + initialize(); + Util.updateMappingToDb(mapping); + } + + public synchronized Mapping getMapping(final String address) + throws IOException, SQLException, NamingException { + initialize(); + return Util.getMappingFromDb(address); + } + + public synchronized List getMappings() + throws IOException, SQLException, NamingException { + initialize(); + return Util.getMappingsFromDb(); + } + + public synchronized List getAliases(final String target) + throws IOException, SQLException, NamingException { + initialize(); + return Util.getAliasesFromDb(target); + } +} diff --git a/src/com/stephenschafer/email/Logger.java b/src/com/stephenschafer/email/Logger.java new file mode 100644 index 0000000..cc1eb7f --- /dev/null +++ b/src/com/stephenschafer/email/Logger.java @@ -0,0 +1,76 @@ +package com.stephenschafer.email; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintStream; +import java.io.PrintWriter; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; + +public class Logger { + public static String logFilename; + public static boolean debugLogging; + + public static void debugLog(final Object message) { + if (Logger.debugLogging) { + log(message); + } + } + + public static void debugLog(final Object message, final Throwable t) { + if (Logger.debugLogging) { + log(message, t); + } + } + + public static void log(final Object message) { + log(message, null); + } + + public static void log(final Object message, final Throwable t) { + final DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS "); + try { + final String filename = Logger.logFilename; + if (filename == null) { + final PrintStream out = System.out; + out.print(df.format(new Date())); + out.print(" "); + out.println(message); + if (t != null) { + t.printStackTrace(out); + } + } + else { + final File file = new File(filename); + final FileWriter fw = new FileWriter(file, true); + final PrintWriter pw = new PrintWriter(fw); + try { + pw.print(df.format(new Date())); + pw.print(" "); + pw.println(message); + if (t != null) { + t.printStackTrace(pw); + } + } + finally { + pw.close(); + } + } + } + catch (final IOException e) { + System.out.println("Logger: Unable to log " + message); + if (t != null) { + t.printStackTrace(); + } + log("Log failure caused by:"); + e.printStackTrace(); + } + } + + static { + Logger.logFilename = null; + Logger.debugLogging = false; + } +} diff --git a/src/com/stephenschafer/email/Logout.java b/src/com/stephenschafer/email/Logout.java new file mode 100644 index 0000000..1ac84fc --- /dev/null +++ b/src/com/stephenschafer/email/Logout.java @@ -0,0 +1,18 @@ +package com.stephenschafer.email; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class Logout extends HttpServlet { + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) + throws ServletException, IOException { + Util.logout(req.getSession()); + } +} diff --git a/src/com/stephenschafer/email/Mapping.java b/src/com/stephenschafer/email/Mapping.java new file mode 100644 index 0000000..44ab34e --- /dev/null +++ b/src/com/stephenschafer/email/Mapping.java @@ -0,0 +1,93 @@ +package com.stephenschafer.email; + +import java.sql.SQLException; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.naming.NamingException; + +public class Mapping { + private static final Pattern ADDRESS_PATTERN = Pattern.compile("^([^@]+)@(.*)$"); + private final String address; + private final String target; + private final String description; + private final Date date; + private final boolean disabled; + + public Mapping(final String address, final String target, final String description, + final Date date, final boolean disabled) { + this.address = address; + this.target = target; + this.description = description; + this.date = date; + this.disabled = disabled; + } + + public String getAddress() { + return address; + } + + public String getTarget() { + return target; + } + + public String getDescription() { + return description; + } + + public String getDisplayDescription() { + if (description == null) { + return ""; + } + return description; + } + + public boolean isDisabled() { + return disabled; + } + + public String getDomain() { + final Matcher matcher = ADDRESS_PATTERN.matcher(address); + if (matcher.matches()) { + return matcher.group(2); + } + return null; + } + + public String getName() { + final Matcher matcher = ADDRESS_PATTERN.matcher(address); + if (matcher.matches()) { + return matcher.group(1); + } + return null; + } + + public boolean qualifies(final User user) throws SQLException, NamingException { + final List validTargets = user.getValidTargets(); + if (validTargets == null) { + return true; + } + for (final Target validTarget : validTargets) { + if (validTarget.getAddress().equals(target)) { + return true; + } + } + return false; + } + + public Date getDate() { + return date; + } + + public String getFormattedDate() { + if (date == null) { + return ""; + } + final DateFormat df = new SimpleDateFormat("yyyy-MM-dd"); + return df.format(date); + } +} diff --git a/src/com/stephenschafer/email/NewAlias.java b/src/com/stephenschafer/email/NewAlias.java new file mode 100644 index 0000000..9087684 --- /dev/null +++ b/src/com/stephenschafer/email/NewAlias.java @@ -0,0 +1,87 @@ +package com.stephenschafer.email; + +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintWriter; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.naming.NamingException; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class NewAlias extends HttpServlet { + private static final long serialVersionUID = 1L; + + @Override + protected void doPost(final HttpServletRequest request, final HttpServletResponse response) + throws ServletException, IOException { + try { + final InputStream inputStream = request.getInputStream(); + final StringBuilder sb = new StringBuilder(); + final byte[] buffer = new byte[0x1000]; + int bytesRead = inputStream.read(buffer); + while (bytesRead >= 0) { + final String string = new String(buffer, 0, bytesRead); + sb.append(string); + bytesRead = inputStream.read(buffer); + } + String target = sb.toString(); + final int indexOfAt = target.indexOf("@"); + if (indexOfAt >= 0) { + target = target.substring(0, indexOfAt); + } + final String address = getAddress(target); + final PrintWriter out = response.getWriter(); + out.println("{\"value\":\"" + address + "\"}"); + } + catch (final Exception e) { + throw new ServletException(e); + } + } + + private String getAddress(final String target) + throws IOException, SQLException, NamingException { + final String defaultDomain = Configuration.INSTANCE.getDefaultDomain(); + final String patternString = target + "([0-9]+)@" + defaultDomain.replace(".", "\\."); + Logger.log("pattern = " + patternString); + final Pattern pattern = Pattern.compile(patternString); + final List aliasList = Util.getAliases(target); + Logger.log("aliasList = " + aliasList); + final Set indexSet = new HashSet<>(); + for (final String alias : aliasList) { + final Matcher matcher = pattern.matcher(alias); + if (matcher.matches()) { + indexSet.add(Integer.valueOf(matcher.group(1))); + } + } + final List indexList = new ArrayList<>(indexSet); + Collections.sort(indexList); + Logger.log("indexList = " + indexList); + Integer prevIndex = null; + for (final Integer index : indexList) { + if (prevIndex == null && index.intValue() > 1) { + prevIndex = Integer.valueOf(0); + break; + } + if (prevIndex != null && index.intValue() - prevIndex.intValue() > 1) { + break; + } + prevIndex = index; + } + int useIndex = 1; + if (prevIndex != null) { + useIndex = prevIndex.intValue() + 1; + } + Logger.log("useIndex = " + useIndex); + return target + useIndex + "@" + defaultDomain; + } +} diff --git a/src/com/stephenschafer/email/PostMapping.java b/src/com/stephenschafer/email/PostMapping.java new file mode 100644 index 0000000..c6de2f7 --- /dev/null +++ b/src/com/stephenschafer/email/PostMapping.java @@ -0,0 +1,103 @@ +package com.stephenschafer.email; + +import java.io.IOException; +import java.io.PrintWriter; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class PostMapping extends HttpServlet { + private static final long serialVersionUID = 1L; + + @Override + public void init(final ServletConfig config) throws ServletException { + try { + Configuration.INSTANCE.load(); + GlobalCache.INSTANCE.initialize(); + } + catch (final Exception e) { + e.printStackTrace(); + } + super.init(config); + } + + @Override + protected void doPost(final HttpServletRequest request, final HttpServletResponse response) + throws ServletException, IOException { + try { + final boolean newMapping = "true".equals(request.getParameter("new")); + final boolean mobile = "true".equals(request.getParameter("mobile")); + final String address = request.getParameter("email-address"); + final String description = request.getParameter("description"); + final String target = request.getParameter("target"); + final boolean disabled = "true".equalsIgnoreCase(request.getParameter("disabled")); + if (newMapping) { + if (Util.addressExists(address)) { + response.sendError(404, "Address already exists"); + return; + } + if (address == null || address.length() == 0) { + response.sendError(400, "Please supply an address"); + return; + } + if (address.indexOf('@') < 0) { + response.sendError(400, "Not a valid email address"); + return; + } + final Date date = new Date(); + final Mapping mapping = new Mapping(address, target, description, date, disabled); + Util.addMapping(mapping); + Util.addHistory(mapping); + } + else { + if (!Util.addressExists(address)) { + response.sendError(404, "Address does not exist"); + return; + } + final String dateString = request.getParameter("date"); + final DateFormat df = new SimpleDateFormat("yyyy-MM-dd"); + Date date; + try { + date = dateString != null && dateString.trim().length() > 0 + ? df.parse(dateString) + : null; + } + catch (final ParseException e1) { + date = null; + } + final Mapping mapping = new Mapping(address, target, description, date, disabled); + Util.updateMapping(mapping); + Util.addHistory(mapping); + } + Util.SaveMappingsResult result; + try { + result = Util.saveMappings(); + } + catch (final InterruptedException e) { + e.printStackTrace(); + result = null; + } + if (result != null && result.getExitValue() != 0) { + final PrintWriter out = response.getWriter(); + out.println("saveMappings returned " + result.getExitValue()); + out.println("Error:"); + out.println(result.getError()); + out.println("Output:"); + out.println(result.getOutput()); + } + else { + response.sendRedirect(mobile ? "mapping.jsp?address=" + address : "index.jsp"); + } + } + catch (final Exception e) { + throw new ServletException(e); + } + } +} diff --git a/src/com/stephenschafer/email/ResetSession.java b/src/com/stephenschafer/email/ResetSession.java new file mode 100644 index 0000000..0182717 --- /dev/null +++ b/src/com/stephenschafer/email/ResetSession.java @@ -0,0 +1,18 @@ +package com.stephenschafer.email; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class ResetSession extends HttpServlet { + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) + throws ServletException, IOException { + Util.resetSession(req.getSession()); + } +} diff --git a/src/com/stephenschafer/email/Server.java b/src/com/stephenschafer/email/Server.java new file mode 100644 index 0000000..eb86746 --- /dev/null +++ b/src/com/stephenschafer/email/Server.java @@ -0,0 +1,13 @@ +package com.stephenschafer.email; + +public class Server { + private final String serverName; + + public Server(final String serverName) { + this.serverName = serverName; + } + + public String getServerName() { + return serverName; + } +} diff --git a/src/com/stephenschafer/email/Session.java b/src/com/stephenschafer/email/Session.java new file mode 100644 index 0000000..3daea69 --- /dev/null +++ b/src/com/stephenschafer/email/Session.java @@ -0,0 +1,13 @@ +package com.stephenschafer.email; + +public class Session { + private User user; + + public User getUser() { + return user; + } + + public void setUser(final User user) { + this.user = user; + } +} diff --git a/src/com/stephenschafer/email/Target.java b/src/com/stephenschafer/email/Target.java new file mode 100644 index 0000000..f90f9c5 --- /dev/null +++ b/src/com/stephenschafer/email/Target.java @@ -0,0 +1,55 @@ +package com.stephenschafer.email; + +import java.util.Date; + +public class Target { + private final int id; + private final String address; + private final String description; + private final Date created; + + public Target(final int id, final String address, final String description, + final Date created) { + if (address == null) { + throw new IllegalArgumentException("address may not be null"); + } + if (created == null) { + throw new IllegalArgumentException("created may not be null"); + } + this.id = id; + this.address = address; + this.description = description; + this.created = created; + } + + public int getId() { + return id; + } + + public String getAddress() { + return address; + } + + public String getDescription() { + return description; + } + + public Date getCreated() { + return created; + } + + @Override + public boolean equals(final Object obj) { + if (!(obj instanceof Target)) { + return false; + } + final Target that = (Target) obj; + return this.id == that.id; + } + + @Override + public String toString() { + // TODO Auto-generated method stub + return super.toString(); + } +} diff --git a/src/com/stephenschafer/email/User.java b/src/com/stephenschafer/email/User.java new file mode 100644 index 0000000..ac67221 --- /dev/null +++ b/src/com/stephenschafer/email/User.java @@ -0,0 +1,43 @@ +package com.stephenschafer.email; + +import java.sql.SQLException; +import java.util.List; + +import javax.naming.NamingException; + +public class User { + private final int id; + private final String name; + private final String displayName; + private final boolean canWrite; + + public User(final int id, final String name, final String displayName, final boolean canWrite) { + this.id = id; + this.name = name; + this.displayName = displayName; + this.canWrite = canWrite; + } + + public int getId() { + return id; + } + + public String getName() { + return name; + } + + public String getDisplayName() { + return displayName; + } + + public boolean isCanWrite() { + return canWrite; + } + + public List getValidTargets() throws SQLException, NamingException { + if ("elephant".equals(name)) { + return null; // all targets are valid + } + return Util.getTargetsForUser(this); + } +} diff --git a/src/com/stephenschafer/email/Util.java b/src/com/stephenschafer/email/Util.java new file mode 100644 index 0000000..67f5c66 --- /dev/null +++ b/src/com/stephenschafer/email/Util.java @@ -0,0 +1,697 @@ +package com.stephenschafer.email; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.sql.Types; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import javax.naming.NamingException; +import javax.servlet.http.HttpSession; + +public class Util { + private Util() { + } + + public static void resetSession(final HttpSession session) { + session.removeAttribute("target-list"); + session.removeAttribute("domain-list"); + } + + public static void updateSession(final HttpSession session) + throws IOException, SQLException, NamingException { + if (session.getAttribute("target-list") != null) { + Logger.log("Util.updateSession session is already up to date"); + return; + } + final User user = identify(session); + final Set targetSet = new HashSet<>(); + final Set domainSet = new HashSet<>(); + for (final Mapping mapping : GlobalCache.INSTANCE.getMappings()) { + if (mapping.qualifies(user)) { + targetSet.add(mapping.getTarget()); + } + if (mapping.qualifies(user)) { + final String domain = mapping.getDomain(); + if (domain != null) { + domainSet.add(domain); + } + } + } + Logger.log("Util.updateSession targets: " + targetSet.size()); + Logger.log("Util.updateSession domains: " + domainSet.size()); + final List targetList = new ArrayList<>(targetSet); + Collections.sort(targetList); + final List domainList = new ArrayList<>(domainSet); + Collections.sort(domainList); + session.setAttribute("target-list", targetList); + session.setAttribute("domain-list", domainList); + } + + public static User register(final String username, final String password, + final String displayName, final boolean canWrite) + throws NoSuchAlgorithmException, SQLException, NamingException { + final SecureRandom random = new SecureRandom(); + final byte[] salt = new byte[64]; + random.nextBytes(salt); + final byte[] passwordBytes = password.getBytes(); + final byte[] saltPlusPassword = new byte[salt.length + passwordBytes.length]; + int i = 0; + for (final byte b : salt) { + saltPlusPassword[i++] = b; + } + for (final byte b : passwordBytes) { + saltPlusPassword[i++] = b; + } + final MessageDigest md = MessageDigest.getInstance("SHA-256"); + md.update(saltPlusPassword); + final byte[] digest = md.digest(); + final Connection connection = Configuration.INSTANCE.getConnection(); + try { + final String sql = "insert into user" + + " (username, password, salt, display_name, can_write)" + + " values (?, ?, ?, ?, ?)"; + final PreparedStatement statement = connection.prepareStatement(sql, + PreparedStatement.RETURN_GENERATED_KEYS); + try { + statement.setString(1, username); + statement.setBytes(2, digest); + statement.setBytes(3, salt); + statement.setString(4, displayName); + statement.setBoolean(5, canWrite); + final int rowCount = statement.executeUpdate(); + if (rowCount == 0) { + return null; + } + final ResultSet resultSet = statement.getGeneratedKeys(); + try { + if (resultSet.next()) { + final int id = resultSet.getInt(1); + return new User(id, username, displayName, canWrite); + } + return null; + } + finally { + resultSet.close(); + } + } + finally { + statement.close(); + } + } + finally { + connection.close(); + } + } + + public static void changePassword(final int id, final String password) + throws NoSuchAlgorithmException, SQLException, NamingException { + final SecureRandom random = new SecureRandom(); + final byte[] salt = new byte[64]; + random.nextBytes(salt); + final byte[] passwordBytes = password.getBytes(); + final byte[] saltPlusPassword = new byte[salt.length + passwordBytes.length]; + int i = 0; + for (final byte b : salt) { + saltPlusPassword[i++] = b; + } + for (final byte b : passwordBytes) { + saltPlusPassword[i++] = b; + } + final MessageDigest md = MessageDigest.getInstance("SHA-256"); + md.update(saltPlusPassword); + final byte[] digest = md.digest(); + final Connection connection = Configuration.INSTANCE.getConnection(); + try { + final String sql = "update user set" + " password = ?, salt = ?" + " where id = ?"; + final PreparedStatement statement = connection.prepareStatement(sql); + try { + statement.setBytes(1, digest); + statement.setBytes(2, salt); + statement.executeUpdate(); + } + finally { + statement.close(); + } + } + finally { + connection.close(); + } + } + + public static User login(final String username, final String password) + throws SQLException, NamingException, NoSuchAlgorithmException { + Logger.log("Util.login " + username); + final Connection connection = Configuration.INSTANCE.getConnection(); + try { + final String sql = "select id, password, salt, display_name, can_write, username" + + " from user where username = ?"; + final PreparedStatement statement = connection.prepareStatement(sql); + try { + statement.setString(1, username); + final ResultSet resultSet = statement.executeQuery(); + try { + if (!resultSet.next()) { + return null; + } + int i = 0; + final int id = resultSet.getInt(++i); + final byte[] passwordDigestBytes = resultSet.getBytes(++i); + final byte[] salt = resultSet.getBytes(++i); + final String displayName = resultSet.getString(++i); + final boolean canWrite = resultSet.getBoolean(++i); + final String actualUsername = resultSet.getString(++i); + final byte[] passwordBytes = password.getBytes(); + final byte[] saltPlusPassword = new byte[salt.length + passwordBytes.length]; + i = 0; + for (final byte b : salt) { + saltPlusPassword[i++] = b; + } + for (final byte b : passwordBytes) { + saltPlusPassword[i++] = b; + } + final MessageDigest md = MessageDigest.getInstance("SHA-256"); + md.update(saltPlusPassword); + final byte[] digest = md.digest(); + if (Arrays.equals(passwordDigestBytes, digest)) { + Logger.log(username + " successfully logged in"); + return new User(id, actualUsername, displayName, canWrite); + } + Logger.log(username + " failed to log in"); + return null; + } + finally { + resultSet.close(); + } + } + finally { + statement.close(); + } + } + finally { + connection.close(); + } + } + + public static User login(final String username) + throws SQLException, NamingException, NoSuchAlgorithmException { + Logger.log("Util.login " + username); + final Connection connection = Configuration.INSTANCE.getConnection(); + try { + final String sql = "select id, display_name, can_write, username" + + " from user where username = ?"; + final PreparedStatement statement = connection.prepareStatement(sql); + try { + statement.setString(1, username); + final ResultSet resultSet = statement.executeQuery(); + try { + if (!resultSet.next()) { + return null; + } + int i = 0; + final int id = resultSet.getInt(++i); + final String displayName = resultSet.getString(++i); + final boolean canWrite = resultSet.getBoolean(++i); + final String actualUsername = resultSet.getString(++i); + return new User(id, actualUsername, displayName, canWrite); + } + finally { + resultSet.close(); + } + } + finally { + statement.close(); + } + } + finally { + connection.close(); + } + } + + static int getMappingsCountFromDb() throws SQLException, NamingException { + final Connection connection = Configuration.INSTANCE.getConnection(); + try { + final String sql = "select count(*) from mapping"; + final PreparedStatement statement = connection.prepareStatement(sql); + try { + final ResultSet resultSet = statement.executeQuery(); + try { + if (resultSet.next()) { + return resultSet.getInt(1); + } + } + finally { + resultSet.close(); + } + } + finally { + statement.close(); + } + } + finally { + connection.close(); + } + return 0; + } + + static List getAliasesFromDb(final String target) throws SQLException, NamingException { + final List list = new ArrayList<>(); + final Connection connection = Configuration.INSTANCE.getConnection(); + try { + final String sql = "select distinct m.address" + " from mapping m" + + " inner join target t on t.id = m.target_id" + " where t.address = ?" + + " order by 1"; + Logger.log(sql); + final PreparedStatement statement = connection.prepareStatement(sql); + try { + Logger.log("target = " + target); + statement.setString(1, target); + final ResultSet resultSet = statement.executeQuery(); + try { + while (resultSet.next()) { + list.add(resultSet.getString(1)); + } + } + finally { + resultSet.close(); + } + } + finally { + statement.close(); + } + } + finally { + connection.close(); + } + Logger.log(String.format("returning %d aliases", Integer.valueOf(list.size()))); + return list; + } + + static List getMappingsFromDb() throws SQLException, NamingException { + final List list = new ArrayList<>(); + final Connection connection = Configuration.INSTANCE.getConnection(); + try { + final String sql = "select m.address, t.address, m.description, m.created, m.disabled" + + " from mapping m" + " inner join target t on t.id = m.target_id" + " order by 1"; + Logger.log(sql); + final PreparedStatement statement = connection.prepareStatement(sql); + try { + final ResultSet resultSet = statement.executeQuery(); + try { + while (resultSet.next()) { + int i = 0; + final String address = resultSet.getString(++i); + final String target = resultSet.getString(++i); + final String description = resultSet.getString(++i); + final Timestamp created = resultSet.getTimestamp(++i); + final boolean disabled = resultSet.getBoolean(++i); + final Mapping mapping = new Mapping(address, target, description, created, + disabled); + list.add(mapping); + Logger.debugLog(address); + } + } + finally { + resultSet.close(); + } + } + finally { + statement.close(); + } + } + finally { + connection.close(); + } + Logger.log(String.format("returning %d mappings", Integer.valueOf(list.size()))); + return list; + } + + static Mapping getMappingFromDb(final String address) throws SQLException, NamingException { + final Connection connection = Configuration.INSTANCE.getConnection(); + try { + final String sql = "select t.address, m.description, m.created, m.disabled" + + " from mapping m" + " inner join target t on t.id = m.target_id" + + " where m.address = ?"; + final PreparedStatement statement = connection.prepareStatement(sql); + try { + statement.setString(1, address); + final ResultSet resultSet = statement.executeQuery(); + try { + if (resultSet.next()) { + int i = 0; + final String target = resultSet.getString(++i); + final String description = resultSet.getString(++i); + final Timestamp created = resultSet.getTimestamp(++i); + final boolean disabled = resultSet.getBoolean(++i); + return new Mapping(address, target, description, created, disabled); + } + } + finally { + resultSet.close(); + } + } + finally { + statement.close(); + } + } + finally { + connection.close(); + } + return null; + } + + private static Target getTarget(final Connection connection, final String address) + throws SQLException { + if (address == null) { + throw new IllegalArgumentException("address may not be null"); + } + Target target = null; + boolean targetFound = false; + try { + target = getTargetFromDb(connection, address); + targetFound = true; + } + catch (final TargetNotFoundException e) { + } + if (!targetFound) { + target = addTargetToDb(connection, address, ""); + } + return target; + } + + static void addMappingToDb(final Mapping mapping) throws SQLException, NamingException { + final Connection connection = Configuration.INSTANCE.getConnection(); + try { + final Target target = getTarget(connection, mapping.getTarget()); + final String sql = "insert into mapping" + + " (address, target_id, description, created, disabled)" + + " values (?, ?, ?, ?, ?)"; + final PreparedStatement statement = connection.prepareStatement(sql, + PreparedStatement.RETURN_GENERATED_KEYS); + try { + int i = 0; + statement.setString(++i, mapping.getAddress()); + statement.setInt(++i, target.getId()); + statement.setString(++i, mapping.getDescription()); + final Date created = mapping.getDate(); + if (created == null) { + statement.setNull(++i, Types.TIMESTAMP); + } + else { + statement.setTimestamp(++i, new Timestamp(created.getTime())); + } + statement.setBoolean(++i, mapping.isDisabled()); + statement.executeUpdate(); + } + finally { + statement.close(); + } + } + finally { + connection.close(); + } + } + + static void updateMappingToDb(final Mapping mapping) throws SQLException, NamingException { + final Connection connection = Configuration.INSTANCE.getConnection(); + try { + final Target target = getTarget(connection, mapping.getTarget()); + final String sql = "update mapping set" + + " target_id = ?, description = ?, created = ?, disabled = ?" + + " where address = ?"; + final PreparedStatement statement = connection.prepareStatement(sql, + PreparedStatement.RETURN_GENERATED_KEYS); + try { + int i = 0; + statement.setInt(++i, target.getId()); + statement.setString(++i, mapping.getDescription()); + final Date created = mapping.getDate(); + if (created == null) { + statement.setNull(++i, Types.TIMESTAMP); + } + else { + statement.setTimestamp(++i, new Timestamp(created.getTime())); + } + statement.setBoolean(++i, mapping.isDisabled()); + statement.setString(++i, mapping.getAddress()); + statement.executeUpdate(); + } + finally { + statement.close(); + } + } + finally { + connection.close(); + } + } + + static int addHistoryToDb(final Mapping mapping) throws SQLException, NamingException { + final Connection connection = Configuration.INSTANCE.getConnection(); + try { + final Target target = getTarget(connection, mapping.getTarget()); + final String sql = "insert into history" + + " (address, target_id, description, created, changed, disabled)" + + " values (?, ?, ?, ?, ?, ?)"; + final PreparedStatement statement = connection.prepareStatement(sql, + PreparedStatement.RETURN_GENERATED_KEYS); + try { + int i = 0; + statement.setString(++i, mapping.getAddress()); + statement.setInt(++i, target.getId()); + statement.setString(++i, mapping.getDescription()); + final Date created = mapping.getDate(); + if (created == null) { + statement.setNull(++i, Types.TIMESTAMP); + } + else { + statement.setTimestamp(++i, new Timestamp(created.getTime())); + } + statement.setTimestamp(++i, new Timestamp(System.currentTimeMillis())); + statement.setBoolean(++i, mapping.isDisabled()); + final int rowCount = statement.executeUpdate(); + if (rowCount == 0) { + throw new RuntimeException("Failed to insert new mapping"); + } + final ResultSet resultSet = statement.getGeneratedKeys(); + try { + if (resultSet.next()) { + return resultSet.getInt(1); + } + throw new RuntimeException("Failed to get new mapping id"); + } + finally { + resultSet.close(); + } + } + finally { + statement.close(); + } + } + finally { + connection.close(); + } + } + + private static class TargetNotFoundException extends Exception { + private static final long serialVersionUID = 1L; + } + + private static Target getTargetFromDb(final Connection connection, final String address) + throws SQLException, TargetNotFoundException { + if (address == null) { + throw new IllegalArgumentException("address may not be null"); + } + final String sql = "select id, description, created from target where address = ?"; + final PreparedStatement statement = connection.prepareStatement(sql); + try { + statement.setString(1, address); + final ResultSet resultSet = statement.executeQuery(); + try { + if (resultSet.next()) { + final int id = resultSet.getInt(1); + final String description = resultSet.getString(2); + final Timestamp created = resultSet.getTimestamp(3); + return new Target(id, address, description, created); + } + } + finally { + resultSet.close(); + } + } + finally { + statement.close(); + } + throw new TargetNotFoundException(); + } + + private static Target addTargetToDb(final Connection connection, final String address, + final String description) throws SQLException { + if (address == null) { + throw new IllegalArgumentException("address may not be null"); + } + final Timestamp created = new Timestamp(System.currentTimeMillis()); + final String sql = "insert into target (address, description, created)" + + " values (?, ?, ?)"; + final PreparedStatement statement = connection.prepareStatement(sql, + PreparedStatement.RETURN_GENERATED_KEYS); + try { + int i = 0; + statement.setString(++i, address); + statement.setString(++i, description); + statement.setTimestamp(++i, created); + final int rowCount = statement.executeUpdate(); + if (rowCount == 0) { + throw new RuntimeException("Failed to insert new target"); + } + final ResultSet resultSet = statement.getGeneratedKeys(); + try { + if (resultSet.next()) { + final int id = resultSet.getInt(1); + return new Target(id, address, description, created); + } + throw new RuntimeException("Failed to get new target id"); + } + finally { + resultSet.close(); + } + } + finally { + statement.close(); + } + } + + public static List getTargetsForUser(final User user) + throws SQLException, NamingException { + final Connection connection = Configuration.INSTANCE.getConnection(); + try { + return getTargetsForUser(connection, user); + } + finally { + connection.close(); + } + } + + private static List getTargetsForUser(final Connection connection, final User user) + throws SQLException { + final List list = new ArrayList<>(); + final String sql = "select t.id, t.address, t.description, t.created" + + " from target t inner join user_target ut on t.id = ut.target_id" + + " where ut.user_id = ?"; + final PreparedStatement statement = connection.prepareStatement(sql); + try { + statement.setInt(1, user.getId()); + final ResultSet resultSet = statement.executeQuery(); + try { + while (resultSet.next()) { + int i = 0; + final int id = resultSet.getInt(++i); + final String address = resultSet.getString(++i); + final String description = resultSet.getString(++i); + final Date created = resultSet.getDate(++i); + final Target target = new Target(id, address, description, created); + list.add(target); + } + } + finally { + resultSet.close(); + } + } + finally { + statement.close(); + } + return list; + } + + public static void logout(final HttpSession session) { + if (session == null) { + return; + } + session.removeAttribute("email-session"); + } + + public static User identify(final HttpSession session) { + if (session == null) { + return null; + } + final Object object = session.getAttribute("email-session"); + if (object == null || !(object instanceof Session)) { + return null; + } + final Session appSession = (Session) object; + return appSession.getUser(); + } + + public interface SaveMappingsResult { + int getExitValue(); + + String getOutput(); + + String getError(); + } + + static String streamToString(final InputStream inputStream) throws IOException { + final Reader reader = new InputStreamReader(inputStream); + final StringBuilder sb = new StringBuilder(); + final char[] buffer = new char[0x1000]; + int charsRead = reader.read(buffer); + while (charsRead >= 0) { + sb.append(buffer, 0, charsRead); + charsRead = reader.read(buffer); + } + return sb.toString(); + } + + static boolean addressExists(final String address) + throws SQLException, NamingException, IOException { + return GlobalCache.INSTANCE.addressExists(address); + } + + static void addMapping(final Mapping mapping) + throws IOException, SQLException, NamingException { + GlobalCache.INSTANCE.addMapping(mapping); + } + + static void updateMapping(final Mapping mapping) + throws IOException, SQLException, NamingException { + GlobalCache.INSTANCE.updateMapping(mapping); + } + + static void addHistory(final Mapping mapping) + throws IOException, SQLException, NamingException { + GlobalCache.INSTANCE.addHistory(mapping); + } + + public static Mapping getMapping(final String address) + throws IOException, SQLException, NamingException { + return GlobalCache.INSTANCE.getMapping(address); + } + + static SaveMappingsResult saveMappings() + throws IOException, InterruptedException, SQLException, NamingException { + return GlobalCache.INSTANCE.writeVirtualFile(); + } + + public static List getMappings() throws IOException, SQLException, NamingException { + return GlobalCache.INSTANCE.getMappings(); + } + + public static List getAliases(final String target) + throws IOException, SQLException, NamingException { + return GlobalCache.INSTANCE.getAliases(target); + } +}