Compare commits

...

2 commits

Author SHA1 Message Date
Steve Schafer
f5639ddf44 Initial commit 2024-12-05 14:04:39 -07:00
Steve Schafer
ccbc9dc4be Initial commit to forgejo 2024-12-05 13:35:23 -07:00
38 changed files with 3503 additions and 0 deletions

21
.classpath Normal file
View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry including="**/*.java" kind="src" output="target/classes" path="src">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
<attribute name="org.eclipse.jst.component.dependency" value="/WEB-INF/lib"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
target/
sql/
hs_err*.log

37
.project Normal file
View file

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>com.stephenschafer.email</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.wst.common.project.facet.core.builder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.wst.validation.validationbuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
<nature>org.eclipse.jem.workbench.JavaEMFNature</nature>
<nature>org.eclipse.wst.common.modulecore.ModuleCoreNature</nature>
<nature>org.eclipse.wst.common.project.facet.core.nature</nature>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.wst.jsdt.core.jsNature</nature>
</natures>
</projectDescription>

12
.settings/.jsdtscope Normal file
View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry excluding="**/bower_components/*|**/node_modules/*|**/*.min.js" kind="src" path="WebContent"/>
<classpathentry kind="con" path="org.eclipse.wst.jsdt.launching.JRE_CONTAINER"/>
<classpathentry kind="con" path="org.eclipse.wst.jsdt.launching.WebProject">
<attributes>
<attribute name="hide" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.wst.jsdt.launching.baseBrowserLibrary"/>
<classpathentry kind="output" path=""/>
</classpath>

View file

@ -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

View file

@ -0,0 +1,4 @@
activeProfiles=
eclipse.preferences.version=1
resolveWorkspaceProjects=true
version=1

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?><project-modules id="moduleCoreId" project-version="1.5.0">
<wb-module deploy-name="com.stephenschafer.email-manager-0.0.1-SNAPSHOT">
<wb-resource deploy-path="/" source-path="/target/m2e-wtp/web-resources"/>
<wb-resource deploy-path="/" source-path="/WebContent" tag="defaultRootSource"/>
<wb-resource deploy-path="/WEB-INF/classes" source-path="/src"/>
<property name="context-root" value="com.stephenschafer.email-manager"/>
<property name="java-output-path" value="/com.stephenschafer.email-manager/build/classes"/>
</wb-module>
</project-modules>

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<faceted-project>
<fixed facet="jst.web"/>
<fixed facet="java"/>
<fixed facet="wst.jsdt.web"/>
<installed facet="java" version="1.8"/>
<installed facet="wst.jsdt.web" version="1.0"/>
<installed facet="jst.web" version="2.5"/>
</faceted-project>

View file

@ -0,0 +1 @@
org.eclipse.wst.jsdt.launching.baseBrowserLibrary

View file

@ -0,0 +1 @@
Window

View file

@ -0,0 +1,2 @@
disabled=06target
eclipse.preferences.version=1

View file

@ -0,0 +1,3 @@
Manifest-Version: 1.0
Class-Path:

View file

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/j2ee" version="2.4"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd http://xmlns.jcp.org/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
xmlns:web="http://xmlns.jcp.org/xml/ns/javaee">
<session-config>
<session-timeout>1440</session-timeout>
</session-config>
<servlet>
<servlet-name>PostMapping</servlet-name>
<servlet-class>com.stephenschafer.email.PostMapping</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>PostMapping</servlet-name>
<url-pattern>/post-mapping</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>NewAlias</servlet-name>
<servlet-class>com.stephenschafer.email.NewAlias</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>NewAlias</servlet-name>
<url-pattern>/new-alias</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>reset</servlet-name>
<servlet-class>com.stephenschafer.email.ResetSession</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>reset</servlet-name>
<url-pattern>/reset</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>logout</servlet-name>
<servlet-class>com.stephenschafer.email.Logout</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>logout</servlet-name>
<url-pattern>/logout</url-pattern>
</servlet-mapping>
</web-app>

202
WebContent/edit.jsp Normal file
View file

@ -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<String> targetList = (List<String>) 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();
%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Edit Mapping</title>
<style>
body {
font-family: sans-serif;
font-size: 10pt;
color: #333;
}
table {
font-family: inherit;
font-size: inherit;
}
table.mapping {
}
table.mapping tr.mapping {
}
table.mapping tr.mapping td:first-of-type {
padding-right: 10px;
white-space: nowrap;
}
@media only screen and (min-resolution:2x) {
body {
font-size: 300%;
}
h1 {
display: none;
}
button {
margin-bottom: .5em;
font-size: inherit;
}
input {
display: block;
margin-bottom: .5em;
font-size: inherit;
}
input[type="checkbox"] {
width: 1em;
height: 1em;
}
select {
display: block;
margin-bottom: .5em;
font-size: inherit;
}
label {
display: block;
margin-bottom: .5em;
font-size: inherit;
}
}
</style>
<script>
function targetChanged() {
var targetSelect = document.mapping.target;
var emailInput = document.mapping["email-address"];
var request = new XMLHttpRequest();
request.open("POST", "new-alias", true);
request.onreadystatechange = function() {
if(request.readyState != 4) {
return;
}
if(request.status != 200) {
alert("Http error " + request.status + " on " + request.statusText);
debugger;
return;
}
try {
var response = JSON.parse(request.responseText);
if(response.error != null) {
}
else {
emailInput.value = response.value;
}
}
catch(e) {
debugger;
}
};
request.setRequestHeader("Content-Type", "application/json");
request.setRequestHeader("Accept", "application/json");
request.send(targetSelect.value);
}
</script>
</head>
<body onload="targetChanged()">
<h1><%=header %></h1>
<form action="post-mapping" method="post" name="mapping">
<input type="hidden" name="new" value="<%=mapping == null ? "true" : "false" %>"/>
<input type="hidden" name="mobile" value="<%=mobile ? "true" : "false" %>"/>
<table class="mapping">
<tr style="<%=targetDisplay%>" >
<td>Target</td>
<td>
<select name="target" onchange="targetChanged()">
<%
for(final String target : targetList) {
final boolean selected = mapping != null ? target.equals(mapping.getTarget()) : selectedTarget.equals(target);
final String selectedAttr = selected ? " selected" : "";
%> <option value="<%=target%>"<%=selectedAttr%>><%=target%></option>
<%
}
%> </select>
</td>
</tr>
<tr>
<td>Email Address</td>
<td>
<%
if(mapping == null) {
%> <input type="text" name="email-address" value="" size="24"/>
<%
}
else {
%> <input type="hidden" name="email-address" value="<%=mapping.getAddress()%>" size="24"/>
<%=mapping.getAddress()%>
<%
}
%></td>
</tr>
<tr>
<td>Description</td>
<td>
<input type="text" name="description" value="<%=descriptionValue %>" size="24"/>
</td>
</tr>
<%
if(mapping != null) {
%><tr>
<td>Date</td>
<td>
<input type="text" name="date" value="<%=dateValue %>" size="10"/>
</td>
</tr>
<%
}
%><tr style="<%=disableDisplay %>">
<td>Disabled</td>
<td>
<input type="checkbox" name="disabled" value="true"<%=disabledAttr %>/>
</td>
</tr>
</table>
<div class="buttons">
<button type="submit">Submit</button>
<%
if(!mobile) {
%> <button type="button" onclick="window.location.href='index.jsp'">List</button>
<button type="button" onclick="window.history.back()">Back</button>
<%
}
%></div>
</form>
</body>
</html>

361
WebContent/index.jsp Normal file
View file

@ -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<String> targetList = (List<String>) session.getAttribute("target-list");
@SuppressWarnings("unchecked")
final List<String> domainList = (List<String>) session.getAttribute("domain-list");
final List<Mapping> mappings = Util.getMappings();
Logger.log(String.format("got %d mappings", Integer.valueOf(mappings.size())));
final List<String> 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("<option value=\"%s\"%s>%s</option>", domain, selected, domain));
}
if(!domainFound) {
domainString = null;
session.removeAttribute("domain-filter");
}
final List<String> 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("<option value=\"%s\"%s>%s</option>", target, selected, target));
}
if(!targetFound) {
targetString = null;
session.removeAttribute("target-filter");
}
final Map<String, Set<String>> names = new HashMap<>();
%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name=""viewport" content="width=device-width, initial-scale=1.0">
<title>Virtual Addresses</title>
<style>
body {
font-family: sans-serif;
font-size: 10pt;
color: #333;
}
button {
font-size: inherit;
}
input {
font-size: inherit;
}
select {
font-family: inherit;
font-size: inherit;
}
table {
font-family: inherit;
font-size: inherit;
}
table.mappings {
}
table.mappings tr.mapping {
}
table.mappings tr.mapping td {
padding-left: 10px;
padding-right: 10px;
}
table.mappings tr.mapping td:first-of-type {
padding-left: 0px;
padding-right: 10px;
}
table.mappings tr.mapping td:last-of-type {
padding-left: 10px;
padding-right: 0px;
}
table.mappings tr.mapping.disabled {
color: #999;
}
table.mappings tr.divider td {
height: 1px;
border-bottom: 1px solid #000;
}
table.mappings tr.mapping td.address {
white-space: nowrap;
}
table.mappings tr.mapping td.date {
white-space: nowrap;
}
a {
text-decoration: none;
color: #333;
}
table.mappings tr.mapping.disabled a {
color: #999;
}
div.new {
margin-bottom: 10px;
}
div.new button {
color: red;
}
@media only screen and (min-resolution:2x) {
body {
font-size: 300%;
}
h1 {
display: none;
}
div.new>button {
display: block;
margin-bottom: .5em;
font-size: inherit;
}
div.new>input {
display: block;
margin-bottom: .5em;
font-size: inherit;
}
div.new>select {
display: block;
margin-bottom: .5em;
font-size: inherit;
}
div.new>label {
display: block;
margin-bottom: .5em;
font-size: inherit;
}
table.mappings>tbody>tr.mapping>td {
display: block;
}
}
</style>
<script>
<%
if(domainString != null) {
out.println(String.format("var domain = \"%s\";\n", domainString));
}
else {
out.println("var domain = null;\n");
}
if(targetString != null) {
out.println(String.format("var target = \"%s\";\n", targetString));
}
else {
out.println("var target = null;\n");
}
out.println("var hideDisabled = " + hideDisabled + ";\n");
%>
function doQuery() {
var args = [];
if(domain != null) {
args.push({name: "domain", value: domain});
}
if(target != null) {
args.push({name: "target", value: target});
}
if(hideDisabled) {
args.push({name: "hide-disabled", value: "hide"});
}
for(var i = 0; i < args.length; i++) {
var arg = args[i];
args[i] = arg.name + "=" + arg.value;
}
args = args.join("&");
var href = "index.jsp";
if(args != "") {
href += "?" + args;
}
window.location.href = href;
}
function selectDomain(select) {
domain = select.value;
doQuery();
}
function selectTarget(select) {
target = select.value;
doQuery();
}
function addressFilter(input) {
textFilter(input, "address");
}
function descriptionFilter(input) {
textFilter(input, "description");
}
function textFilter(input, name) {
var regex = new RegExp(input.value == "" ? ".*" : input.value, "i");
var tds = document.body.querySelectorAll("table.mappings tr.mapping td." + name);
for(var i = 0; i < tds.length; i++) {
var td = tds[i];
var address = td.textContent;
var show = regex.test(address);
var tr = td.parentNode;
tr.style.display = show ? "" : "none";
var tr = tr.nextSibling;
while(tr != null) {
if(tr.nodeName == "TR") {
tr.style.display = show ? "" : "none";
break;
}
tr = tr.nextSibling;
}
}
}
</script>
</head>
<body>
<h1>Virtual Addresses</h1>
<div class="new">
<button type="button" onclick="window.location.href='edit.jsp'">Add a new mapping</button>
<input type="text" name="address-filter" onkeyup="addressFilter(this)" placeholder="address"/>
<select onchange="selectDomain(this)">
<option value="">All domains</option>
<%
for(final String option : domainOptions) {
%> <%=option %>
<%
}
%> </select>
<select onchange="selectTarget(this)">
<option value="">All targets</option>
<%
for(final String option : targetOptions) {
%> <%=option %>
<%
}
%> </select>
<input type="text" name="description-filter" onkeyup="descriptionFilter(this)" placeholder="description"/>
<%
final String hideDisabledChecked = hideDisabled ? " checked" : "";
%> <label>
&nbsp;
<input type="checkbox" name="hide-disabled" value="hide"<%=hideDisabledChecked %>
onclick="hideDisabled = this.checked; doQuery()"/> Hide disabled
</label>
</div>
<table class="mappings">
<%
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" : "";
%><tr class="mapping<%=disabledClass%>">
<td class="address"><a href="edit.jsp?address=<%=mapping.getAddress()%>"><%=mapping.getAddress()%></a></td>
<td class="target"><%=mapping.getTarget()%></td>
<td class="description"><%=mapping.getDisplayDescription()%></td>
<td class="disabled"><%=mapping.isDisabled() ? "disabled" : "" %></td>
<td class="date"><%=mapping.getFormattedDate() %></td>
</tr>
<tr class="divider">
<td colspan="5"></td>
</tr>
<%
if(!mapping.isDisabled()) {
String mappingDomain = mapping.getDomain();
if(mappingDomain == null) {
mappingDomain = "";
}
Set<String> namesForDomain = names.get(mappingDomain);
if(namesForDomain == null) {
namesForDomain = new HashSet<>();
names.put(mappingDomain, namesForDomain);
}
namesForDomain.add(mapping.getName());
}
}
%></table>
<%
final List<String> domains = new ArrayList<>(names.keySet());
Collections.sort(domains);
for(String mappingDomain : domains) {
List<String> namesForDomain = new ArrayList<>(names.get(mappingDomain));
Collections.sort(namesForDomain);
%><div class="names">
<h2>Names for <%=mappingDomain %></h2>
<%
String sep = "";
for(final String name : namesForDomain) {
out.print(sep);
sep = ", ";
out.println(name);
}
%></div>
<%
}
%></body>
</html>

121
WebContent/login.jsp Normal file
View file

@ -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;
}
%><!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Login</title>
<style>
body {
font-family: sans-serif;
font-size: 10pt;
color: #333;
}
table {
font-family: inherit;
font-size: inherit;
}
table {
border-spacing: 0px;
}
table.td {
padding: 0px;
}
div.buttons {
margin-top: 10px;
}
@media only screen and (min-resolution:2x) {
body {
font-size: 300%;
}
h1 {
display: none;
}
button {
display: block;
margin-bottom: .5em;
font-size: inherit;
}
input {
display: block;
margin-bottom: .5em;
font-size: inherit;
}
select {
display: block;
margin-bottom: .5em;
font-size: inherit;
}
label {
display: block;
margin-bottom: .5em;
font-size: inherit;
}
}
</style>
</head>
<body>
<%
if (failed) {
%><div class="fail">That didn't work.</div>
<%
}
%><form name="login" action="login.jsp" method="post">
<table>
<tr>
<td>Username</td>
<td><input type="text" name="username" value="<%=username == null ? "" : username%>" /></td>
</tr>
<tr>
<td>Password</td>
<td><input type="password" name="password" /></td>
</tr>
</table>
<div class="buttons">
<button type="submit">Login</button>
</div>
</form>
</body>
</html>

133
WebContent/mapping.jsp Normal file
View file

@ -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<String> targetList = (List<String>) 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();
%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Mapping</title>
<style>
body {
font-family: sans-serif;
font-size: 10pt;
color: #333;
}
table {
font-family: inherit;
font-size: inherit;
}
table.mapping tr td {
padding-right: 1em;
}
table.mapping tr td:first-of-type {
white-space: nowrap;
}
div.buttons {
margin-top: 2em;
}
@media only screen and (min-resolution:2x) {
body {
font-size: 300%;
}
h1 {
display: none;
}
button {
margin-bottom: .5em;
font-size: inherit;
}
input {
display: block;
margin-bottom: .5em;
font-size: inherit;
}
input[type="checkbox"] {
width: 1em;
height: 1em;
}
select {
display: block;
margin-bottom: .5em;
font-size: inherit;
}
label {
display: block;
margin-bottom: .5em;
font-size: inherit;
}
}
</style>
</head>
<body">
<h1><%=header %></h1>
<table class="mapping">
<tr>
<td>Target</td>
<td>
<%=mapping.getTarget()%>
</td>
</tr>
<tr>
<td>Email Address</td>
<td>
<%=mapping.getAddress()%>
</td>
</tr>
<tr>
<td>Description</td>
<td>
<%=mapping.getDisplayDescription()%>
</td>
</tr>
<tr>
<td>Date</td>
<td>
<%=mapping.getFormattedDate()%>
</td>
</tr>
<tr>
<td>Disabled</td>
<td>
<%=mapping.isDisabled() ? "true" : "false" %>
</td>
</tr>
</table>
<div class="buttons">
<button type="button" onclick="window.location.href='edit.jsp?mobile=true'">New</button>
</div>
</body>
</html>

87
WebContent/password.jsp Normal file
View file

@ -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;
}
%><!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Password</title>
<style>
body {
font-family: sans-serif;
font-size: 10pt;
}
table {
font-family: inherit;
font-size: inherit;
}
table {
border-spacing: 0px;
}
table.td {
padding: 0px;
}
div.buttons {
margin-top: 10px;
}
</style>
</head>
<body>
<%
if (message != null) {
%><div class="fail">
<%=message%>
</div>
<%
}
%><form name="register" action="register.jsp" method="post">
<table>
<tr>
<td>Password</td>
<td><input type="password" name="password1" /></td>
</tr>
<tr>
<td>Confirm password</td>
<td><input type="password" name="password2" /></td>
</tr>
<tr>
<td>Display name</td>
<td><input type="text" name="displayName" /></td>
</tr>
</table>
<div class="buttons">
<button type="submit">Register</button>
</div>
</form>
</body>
</html>

100
WebContent/register.jsp Normal file
View file

@ -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;
}
%><!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Register</title>
<style>
body {
font-family: sans-serif;
font-size: 10pt;
}
table {
font-family: inherit;
font-size: inherit;
}
table {
border-spacing: 0px;
}
table.td {
padding: 0px;
}
div.buttons {
margin-top: 10px;
}
</style>
</head>
<body>
<%
if(message != null) {
%><div class="fail">
<%=message %>
</div>
<%
}
%><form name="register" action="register.jsp" method="post">
<table>
<tr>
<td>Username</td>
<td><input type="text" name="username"/></td>
</tr>
<tr>
<td>Password</td>
<td><input type="password" name="password1"/></td>
</tr>
<tr>
<td>Confirm password</td>
<td><input type="password" name="password2"/></td>
</tr>
<tr>
<td>Display name</td>
<td><input type="text" name="displayName"/></td>
</tr>
<tr>
<td>Can write</td>
<td><input type="checkbox" name="canWrite" value="true"/></td>
</tr>
</table>
<div class="buttons">
<button type="submit">Register</button>
</div>
</form>
</body>
</html>

9
build Executable file
View file

@ -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"

5
deploy Executable file
View file

@ -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

0
logs/build.err.log Normal file
View file

42
logs/build.log Normal file
View file

@ -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] ------------------------------------------------------------------------

51
pom.xml Normal file
View file

@ -0,0 +1,51 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.stephenschafer.email-manager</groupId>
<artifactId>com.stephenschafer.email-manager</artifactId>
<version>0.0.2-SNAPSHOT</version>
<packaging>war</packaging>
<build>
<sourceDirectory>src</sourceDirectory>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<artifactId>maven-war-plugin</artifactId>
<version>3.4.0</version>
<configuration>
<warSourceDirectory>WebContent</warSourceDirectory>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.38</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.18.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.18.1</version>
</dependency>
</dependencies>
</project>

View file

@ -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;
}
}

View file

@ -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<PooledConnection> connections;
private long openConnectionIndex;
private final Map<Long, OpenConnectionInfo> 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<Long, Long> getOpenConnections() {
final Map<Long, Long> 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<StackTraceElement> stackTrace;
public OpenConnectionInfo() {
this.timestamp = System.currentTimeMillis();
final StackTraceElement[] steArray = Thread.currentThread().getStackTrace();
final List<StackTraceElement> stackTrace = new ArrayList<>(
steArray.length);
for (final StackTraceElement element : steArray) {
stackTrace.add(element);
}
this.stackTrace = Collections.unmodifiableList(
(List<? extends StackTraceElement>) 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<String, Class<?>> 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<String, Class<?>> 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> T unwrap(final Class<T> 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();
}
}
}

View file

@ -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<Mapping> 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<Mapping> getMappings()
throws IOException, SQLException, NamingException {
initialize();
return Util.getMappingsFromDb();
}
public synchronized List<String> getAliases(final String target)
throws IOException, SQLException, NamingException {
initialize();
return Util.getAliasesFromDb(target);
}
}

View file

@ -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;
}
}

View file

@ -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());
}
}

View file

@ -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<Target> 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);
}
}

View file

@ -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<String> aliasList = Util.getAliases(target);
Logger.log("aliasList = " + aliasList);
final Set<Integer> 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<Integer> 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;
}
}

View file

@ -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);
}
}
}

View file

@ -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());
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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();
}
}

View file

@ -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<Target> getValidTargets() throws SQLException, NamingException {
if ("elephant".equals(name)) {
return null; // all targets are valid
}
return Util.getTargetsForUser(this);
}
}

View file

@ -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<String> targetSet = new HashSet<>();
final Set<String> 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<String> targetList = new ArrayList<>(targetSet);
Collections.sort(targetList);
final List<String> 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<String> getAliasesFromDb(final String target) throws SQLException, NamingException {
final List<String> 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<Mapping> getMappingsFromDb() throws SQLException, NamingException {
final List<Mapping> 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<Target> getTargetsForUser(final User user)
throws SQLException, NamingException {
final Connection connection = Configuration.INSTANCE.getConnection();
try {
return getTargetsForUser(connection, user);
}
finally {
connection.close();
}
}
private static List<Target> getTargetsForUser(final Connection connection, final User user)
throws SQLException {
final List<Target> 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<Mapping> getMappings() throws IOException, SQLException, NamingException {
return GlobalCache.INSTANCE.getMappings();
}
public static List<String> getAliases(final String target)
throws IOException, SQLException, NamingException {
return GlobalCache.INSTANCE.getAliases(target);
}
}