From: Mathieu Baudier Date: Mon, 4 Dec 2023 15:56:55 +0000 (+0100) Subject: Adapt to changes in Argeo TP X-Git-Tag: v2.3.13~7 X-Git-Url: https://git.argeo.org/?p=gpl%2Fargeo-slc.git;a=commitdiff_plain;h=a99ed0697b50b1945689c767cd752fc8ea8a6f04 Adapt to changes in Argeo TP --- diff --git a/Makefile b/Makefile index 37eed20b3..65c241c94 100644 --- a/Makefile +++ b/Makefile @@ -28,7 +28,7 @@ crypto/fips/org.argeo.tp.crypto \ log/syslogger/org.argeo.tp \ org.argeo.tp \ org.argeo.tp.httpd \ -org.argeo.tp.utils \ +org.argeo.tp.sys \ osgi/api/org.argeo.tp.osgi \ osgi/equinox/org.argeo.tp.eclipse \ swt/rap/org.argeo.tp.swt \ diff --git a/org.argeo.slc.cms/src/org/argeo/cms/integration/CmsExceptionsChain.java b/org.argeo.slc.cms/src/org/argeo/cms/integration/CmsExceptionsChain.java new file mode 100644 index 000000000..672722946 --- /dev/null +++ b/org.argeo.slc.cms/src/org/argeo/cms/integration/CmsExceptionsChain.java @@ -0,0 +1,80 @@ +package org.argeo.cms.integration; + +import java.io.IOException; +import java.io.Writer; + +import javax.servlet.http.HttpServletResponse; + +import org.argeo.api.cms.CmsLog; +import org.argeo.cms.util.ExceptionsChain; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** Serialisable wrapper of a {@link Throwable}. */ +public class CmsExceptionsChain extends ExceptionsChain { + public final static CmsLog log = CmsLog.getLog(CmsExceptionsChain.class); + + public CmsExceptionsChain() { + super(); + } + + public CmsExceptionsChain(Throwable exception) { + super(exception); + if (log.isDebugEnabled()) + log.error("Exception chain", exception); + } + + public String toJsonString(ObjectMapper objectMapper) { + try { + return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(this); + } catch (JsonProcessingException e) { + throw new IllegalStateException("Cannot write system exceptions " + toString(), e); + } + } + + public void writeAsJson(ObjectMapper objectMapper, Writer writer) { + try { + JsonGenerator jg = objectMapper.writerWithDefaultPrettyPrinter().getFactory().createGenerator(writer); + jg.writeObject(this); + } catch (IOException e) { + throw new IllegalStateException("Cannot write system exceptions " + toString(), e); + } + } + + public void writeAsJson(ObjectMapper objectMapper, HttpServletResponse resp) { + try { + resp.setContentType("application/json"); + resp.setStatus(500); + writeAsJson(objectMapper, resp.getWriter()); + } catch (IOException e) { + throw new IllegalStateException("Cannot write system exceptions " + toString(), e); + } + } + +// public static void main(String[] args) throws Exception { +// try { +// try { +// try { +// testDeeper(); +// } catch (Exception e) { +// throw new Exception("Less deep exception", e); +// } +// } catch (Exception e) { +// throw new RuntimeException("Top exception", e); +// } +// } catch (Exception e) { +// CmsExceptionsChain systemErrors = new CmsExceptionsChain(e); +// ObjectMapper objectMapper = new ObjectMapper(); +// System.out.println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(systemErrors)); +// System.out.println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(e)); +// e.printStackTrace(); +// } +// } +// +// static void testDeeper() throws Exception { +// throw new IllegalStateException("Deep exception"); +// } + +} diff --git a/org.argeo.slc.cms/src/org/argeo/cms/integration/CmsLoginServlet.java b/org.argeo.slc.cms/src/org/argeo/cms/integration/CmsLoginServlet.java new file mode 100644 index 000000000..29a3137bb --- /dev/null +++ b/org.argeo.slc.cms/src/org/argeo/cms/integration/CmsLoginServlet.java @@ -0,0 +1,112 @@ +package org.argeo.cms.integration; + +import java.io.IOException; +import java.util.Locale; +import java.util.Set; + +import javax.security.auth.Subject; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.auth.login.LoginContext; +import javax.security.auth.login.LoginException; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.argeo.api.cms.CmsAuth; +import org.argeo.api.cms.CmsSessionId; +import org.argeo.cms.auth.RemoteAuthCallback; +import org.argeo.cms.auth.RemoteAuthCallbackHandler; +import org.argeo.cms.servlet.ServletHttpRequest; +import org.argeo.cms.servlet.ServletHttpResponse; +import org.osgi.service.useradmin.Authorization; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** Externally authenticate an http session. */ +public class CmsLoginServlet extends HttpServlet { + public final static String PARAM_USERNAME = "username"; + public final static String PARAM_PASSWORD = "password"; + + private static final long serialVersionUID = 2478080654328751539L; + private ObjectMapper objectMapper = new ObjectMapper(); + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + doPost(request, response); + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + LoginContext lc = null; + String username = req.getParameter(PARAM_USERNAME); + String password = req.getParameter(PARAM_PASSWORD); + ServletHttpRequest request = new ServletHttpRequest(req); + ServletHttpResponse response = new ServletHttpResponse(resp); + try { + lc = new LoginContext(CmsAuth.LOGIN_CONTEXT_USER, new RemoteAuthCallbackHandler(request, response) { + public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { + for (Callback callback : callbacks) { + if (callback instanceof NameCallback && username != null) + ((NameCallback) callback).setName(username); + else if (callback instanceof PasswordCallback && password != null) + ((PasswordCallback) callback).setPassword(password.toCharArray()); + else if (callback instanceof RemoteAuthCallback) { + ((RemoteAuthCallback) callback).setRequest(request); + ((RemoteAuthCallback) callback).setResponse(response); + } + } + } + }); + lc.login(); + + Subject subject = lc.getSubject(); + CmsSessionId cmsSessionId = extractFrom(subject.getPrivateCredentials(CmsSessionId.class)); + if (cmsSessionId == null) { + resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return; + } + Authorization authorization = extractFrom(subject.getPrivateCredentials(Authorization.class)); + Locale locale = extractFrom(subject.getPublicCredentials(Locale.class)); + + CmsSessionDescriptor cmsSessionDescriptor = new CmsSessionDescriptor(authorization.getName(), + cmsSessionId.getUuid().toString(), authorization.getRoles(), authorization.toString(), + locale != null ? locale.toString() : null); + + resp.setContentType("application/json"); + JsonGenerator jg = objectMapper.getFactory().createGenerator(resp.getWriter()); + jg.writeObject(cmsSessionDescriptor); + + String redirectTo = redirectTo(req); + if (redirectTo != null) + resp.sendRedirect(redirectTo); + } catch (LoginException e) { + resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return; + } + } + + protected T extractFrom(Set creds) { + if (creds.size() > 0) + return creds.iterator().next(); + else + return null; + } + + /** + * To be overridden in order to return a richer {@link CmsSessionDescriptor} to + * be serialized. + */ + protected CmsSessionDescriptor enrichJson(CmsSessionDescriptor cmsSessionDescriptor) { + return cmsSessionDescriptor; + } + + protected String redirectTo(HttpServletRequest request) { + return null; + } +} diff --git a/org.argeo.slc.cms/src/org/argeo/cms/integration/CmsLogoutServlet.java b/org.argeo.slc.cms/src/org/argeo/cms/integration/CmsLogoutServlet.java new file mode 100644 index 000000000..d18637d3f --- /dev/null +++ b/org.argeo.slc.cms/src/org/argeo/cms/integration/CmsLogoutServlet.java @@ -0,0 +1,79 @@ +package org.argeo.cms.integration; + +import java.io.IOException; +import java.util.Set; + +import javax.security.auth.Subject; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.auth.login.LoginContext; +import javax.security.auth.login.LoginException; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.argeo.api.cms.CmsAuth; +import org.argeo.api.cms.CmsSessionId; +import org.argeo.cms.CurrentUser; +import org.argeo.cms.auth.RemoteAuthCallback; +import org.argeo.cms.auth.RemoteAuthCallbackHandler; +import org.argeo.cms.servlet.ServletHttpRequest; +import org.argeo.cms.servlet.ServletHttpResponse; + +/** Externally authenticate an http session. */ +public class CmsLogoutServlet extends HttpServlet { + private static final long serialVersionUID = 2478080654328751539L; + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + doPost(request, response); + } + + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + ServletHttpRequest httpRequest = new ServletHttpRequest(request); + ServletHttpResponse httpResponse = new ServletHttpResponse(response); + LoginContext lc = null; + try { + lc = new LoginContext(CmsAuth.LOGIN_CONTEXT_USER, + new RemoteAuthCallbackHandler(httpRequest, httpResponse) { + public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { + for (Callback callback : callbacks) { + if (callback instanceof RemoteAuthCallback) { + ((RemoteAuthCallback) callback).setRequest(httpRequest); + ((RemoteAuthCallback) callback).setResponse(httpResponse); + } + } + } + }); + lc.login(); + + Subject subject = lc.getSubject(); + CmsSessionId cmsSessionId = extractFrom(subject.getPrivateCredentials(CmsSessionId.class)); + if (cmsSessionId != null) {// logged in + CurrentUser.logoutCmsSession(subject); + } + + } catch (LoginException e) { + // ignore + } + + String redirectTo = redirectTo(request); + if (redirectTo != null) + response.sendRedirect(redirectTo); + } + + protected T extractFrom(Set creds) { + if (creds.size() > 0) + return creds.iterator().next(); + else + return null; + } + + protected String redirectTo(HttpServletRequest request) { + return null; + } +} diff --git a/org.argeo.slc.cms/src/org/argeo/cms/integration/CmsPrivateServletContext.java b/org.argeo.slc.cms/src/org/argeo/cms/integration/CmsPrivateServletContext.java new file mode 100644 index 000000000..09f17ae02 --- /dev/null +++ b/org.argeo.slc.cms/src/org/argeo/cms/integration/CmsPrivateServletContext.java @@ -0,0 +1,80 @@ +package org.argeo.cms.integration; + +import java.io.IOException; +import java.security.AccessControlContext; +import java.util.Map; + +import javax.security.auth.login.LoginContext; +import javax.security.auth.login.LoginException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.argeo.api.cms.CmsAuth; +import org.argeo.cms.auth.RemoteAuthCallbackHandler; +import org.argeo.cms.auth.RemoteAuthUtils; +import org.argeo.cms.servlet.ServletHttpRequest; +import org.argeo.cms.servlet.ServletHttpResponse; +import org.osgi.service.http.context.ServletContextHelper; + +/** Manages security access to servlets. */ +public class CmsPrivateServletContext extends ServletContextHelper { + public final static String LOGIN_PAGE = "argeo.cms.integration.loginPage"; + public final static String LOGIN_SERVLET = "argeo.cms.integration.loginServlet"; + private String loginPage; + private String loginServlet; + + public void init(Map properties) { + loginPage = properties.get(LOGIN_PAGE); + loginServlet = properties.get(LOGIN_SERVLET); + } + + /** + * Add the {@link AccessControlContext} as a request attribute, or redirect to + * the login page. + */ + @Override + public boolean handleSecurity(final HttpServletRequest req, HttpServletResponse resp) throws IOException { + LoginContext lc = null; + ServletHttpRequest request = new ServletHttpRequest(req); + ServletHttpResponse response = new ServletHttpResponse(resp); + + String pathInfo = req.getPathInfo(); + String servletPath = req.getServletPath(); + if ((pathInfo != null && (servletPath + pathInfo).equals(loginPage)) || servletPath.contentEquals(loginServlet)) + return true; + try { + lc = CmsAuth.USER.newLoginContext(new RemoteAuthCallbackHandler(request, response)); + lc.login(); + } catch (LoginException e) { + lc = processUnauthorized(req, resp); + if (lc == null) + return false; + } +// Subject.doAs(lc.getSubject(), new PrivilegedAction() { +// +// @Override +// public Void run() { +// // TODO also set login context in order to log out ? +// RemoteAuthUtils.configureRequestSecurity(request); +// return null; +// } +// +// }); + + return true; + } + +// @Override +// public void finishSecurity(HttpServletRequest req, HttpServletResponse resp) { +// RemoteAuthUtils.clearRequestSecurity(new ServletHttpRequest(req)); +// } + + protected LoginContext processUnauthorized(HttpServletRequest request, HttpServletResponse response) { + try { + response.sendRedirect(loginPage); + } catch (IOException e) { + throw new RuntimeException("Cannot redirect to login page", e); + } + return null; + } +} diff --git a/org.argeo.slc.cms/src/org/argeo/cms/integration/CmsSessionDescriptor.java b/org.argeo.slc.cms/src/org/argeo/cms/integration/CmsSessionDescriptor.java new file mode 100644 index 000000000..30de616a2 --- /dev/null +++ b/org.argeo.slc.cms/src/org/argeo/cms/integration/CmsSessionDescriptor.java @@ -0,0 +1,96 @@ +package org.argeo.cms.integration; + +import java.io.Serializable; +import java.util.Arrays; +import java.util.Collections; +import java.util.Set; +import java.util.TreeSet; + +import org.argeo.api.cms.CmsSession; +import org.osgi.service.useradmin.Authorization; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** A serializable descriptor of an internal {@link CmsSession}. */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class CmsSessionDescriptor implements Serializable, Authorization { + private static final long serialVersionUID = 8592162323372641462L; + + private String name; + private String cmsSessionId; + private String displayName; + private String locale; + private Set roles; + + public CmsSessionDescriptor() { + } + + public CmsSessionDescriptor(String name, String cmsSessionId, String[] roles, String displayName, String locale) { + this.name = name; + this.displayName = displayName; + this.cmsSessionId = cmsSessionId; + this.locale = locale; + this.roles = Collections.unmodifiableSortedSet(new TreeSet<>(Arrays.asList(roles))); + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDisplayName() { + return displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + public String getCmsSessionId() { + return cmsSessionId; + } + + public void setCmsSessionId(String cmsSessionId) { + this.cmsSessionId = cmsSessionId; + } + + public Boolean isAnonymous() { + return name == null; + } + + public String getLocale() { + return locale; + } + + public void setLocale(String locale) { + this.locale = locale; + } + + @Override + public boolean hasRole(String name) { + return roles.contains(name); + } + + @Override + public String[] getRoles() { + return roles.toArray(new String[roles.size()]); + } + + public void setRoles(String[] roles) { + this.roles = Collections.unmodifiableSortedSet(new TreeSet<>(Arrays.asList(roles))); + } + + @Override + public int hashCode() { + return cmsSessionId != null ? cmsSessionId.hashCode() : super.hashCode(); + } + + @Override + public String toString() { + return displayName != null ? displayName : name != null ? name : super.toString(); + } + +} diff --git a/org.argeo.slc.cms/src/org/argeo/cms/integration/CmsTokenServlet.java b/org.argeo.slc.cms/src/org/argeo/cms/integration/CmsTokenServlet.java new file mode 100644 index 000000000..c355ecd8d --- /dev/null +++ b/org.argeo.slc.cms/src/org/argeo/cms/integration/CmsTokenServlet.java @@ -0,0 +1,117 @@ +package org.argeo.cms.integration; + +import java.io.IOException; +import java.time.ZonedDateTime; +import java.util.Set; +import java.util.UUID; + +import javax.security.auth.Subject; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.auth.login.LoginContext; +import javax.security.auth.login.LoginException; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.argeo.api.acr.ldap.NamingUtils; +import org.argeo.api.cms.CmsAuth; +import org.argeo.api.cms.directory.CmsUserManager; +import org.argeo.cms.auth.RemoteAuthCallback; +import org.argeo.cms.auth.RemoteAuthCallbackHandler; +import org.argeo.cms.servlet.ServletHttpRequest; +import org.argeo.cms.servlet.ServletHttpResponse; +import org.osgi.service.useradmin.Authorization; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** Provides access to tokens. */ +public class CmsTokenServlet extends HttpServlet { + private static final long serialVersionUID = 302918711430864140L; + + public final static String PARAM_EXPIRY_DATE = "expiryDate"; + public final static String PARAM_TOKEN = "token"; + + private final static int DEFAULT_HOURS = 24; + + private CmsUserManager userManager; + private ObjectMapper objectMapper = new ObjectMapper(); + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + ServletHttpRequest request = new ServletHttpRequest(req); + ServletHttpResponse response = new ServletHttpResponse(resp); + LoginContext lc = null; + try { + lc = new LoginContext(CmsAuth.LOGIN_CONTEXT_USER, new RemoteAuthCallbackHandler(request, response) { + public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { + for (Callback callback : callbacks) { + if (callback instanceof RemoteAuthCallback) { + ((RemoteAuthCallback) callback).setRequest(request); + ((RemoteAuthCallback) callback).setResponse(response); + } + } + } + }); + lc.login(); + } catch (LoginException e) { + // ignore + } + + try { + Subject subject = lc.getSubject(); + Authorization authorization = extractFrom(subject.getPrivateCredentials(Authorization.class)); + String token = UUID.randomUUID().toString(); + String expiryDateStr = req.getParameter(PARAM_EXPIRY_DATE); + ZonedDateTime expiryDate; + if (expiryDateStr != null) { + expiryDate = NamingUtils.ldapDateToZonedDateTime(expiryDateStr); + } else { + expiryDate = ZonedDateTime.now().plusHours(DEFAULT_HOURS); + expiryDateStr = NamingUtils.instantToLdapDate(expiryDate); + } + userManager.addAuthToken(authorization.getName(), token, expiryDate); + + TokenDescriptor tokenDescriptor = new TokenDescriptor(); + tokenDescriptor.setUsername(authorization.getName()); + tokenDescriptor.setToken(token); + tokenDescriptor.setExpiryDate(expiryDateStr); +// tokenDescriptor.setRoles(Collections.unmodifiableSortedSet(new TreeSet<>(Arrays.asList(roles)))); + + resp.setContentType("application/json"); + JsonGenerator jg = objectMapper.getFactory().createGenerator(resp.getWriter()); + jg.writeObject(tokenDescriptor); + } catch (Exception e) { + new CmsExceptionsChain(e).writeAsJson(objectMapper, resp); + } + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + // temporarily wrap POST for ease of testing + doPost(req, resp); + } + + @Override + protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + try { + String token = req.getParameter(PARAM_TOKEN); + userManager.expireAuthToken(token); + } catch (Exception e) { + new CmsExceptionsChain(e).writeAsJson(objectMapper, resp); + } + } + + protected T extractFrom(Set creds) { + if (creds.size() > 0) + return creds.iterator().next(); + else + return null; + } + + public void setUserManager(CmsUserManager userManager) { + this.userManager = userManager; + } +} diff --git a/org.argeo.slc.cms/src/org/argeo/cms/integration/TestEndpoint.java b/org.argeo.slc.cms/src/org/argeo/cms/integration/TestEndpoint.java new file mode 100644 index 000000000..a09d83e13 --- /dev/null +++ b/org.argeo.slc.cms/src/org/argeo/cms/integration/TestEndpoint.java @@ -0,0 +1,184 @@ +package org.argeo.cms.integration; + +import java.util.Hashtable; +import java.util.List; +import java.util.Map; + +import javax.security.auth.Subject; +import javax.websocket.CloseReason; +import javax.websocket.EndpointConfig; +import javax.websocket.OnClose; +import javax.websocket.OnError; +import javax.websocket.OnMessage; +import javax.websocket.OnOpen; +import javax.websocket.RemoteEndpoint; +import javax.websocket.Session; +import javax.websocket.server.PathParam; +import javax.websocket.server.ServerEndpoint; + +import org.argeo.api.acr.ldap.NamingUtils; +import org.argeo.api.cms.CmsLog; +import org.argeo.cms.websocket.server.CmsWebSocketConfigurator; +import org.argeo.cms.websocket.server.WebSocketView; +import org.osgi.framework.BundleContext; +import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.event.Event; +import org.osgi.service.event.EventConstants; +import org.osgi.service.event.EventHandler; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** Provides WebSocket access. */ +@ServerEndpoint(value = "/cms/status/test/{topic}", configurator = CmsWebSocketConfigurator.class) +public class TestEndpoint implements EventHandler { + private final static CmsLog log = CmsLog.getLog(TestEndpoint.class); + + final static String TOPICS_BASE = "/test"; + final static String INPUT = "input"; + final static String TOPIC = "topic"; + final static String VIEW_UID = "viewUid"; + final static String COMPUTATION_UID = "computationUid"; + final static String MESSAGES = "messages"; + final static String ERRORS = "errors"; + + final static String EXCEPTION = "exception"; + final static String MESSAGE = "message"; + + private BundleContext bc = FrameworkUtil.getBundle(TestEndpoint.class).getBundleContext(); + + private String wsSessionId; + private RemoteEndpoint.Basic remote; + private ServiceRegistration eventHandlerSr; + + // json + private ObjectMapper objectMapper = new ObjectMapper(); + + private WebSocketView view; + + @OnOpen + public void onOpen(Session session, EndpointConfig endpointConfig) { + Map> parameters = NamingUtils.queryToMap(session.getRequestURI()); + String path = NamingUtils.getQueryValue(parameters, "path"); + log.debug("WS Path: " + path); + + wsSessionId = session.getId(); + + // 24h timeout + session.setMaxIdleTimeout(1000 * 60 * 60 * 24); + + Map userProperties = session.getUserProperties(); + Subject subject = null; +// AccessControlContext accessControlContext = (AccessControlContext) userProperties +// .get(ServletContextHelper.REMOTE_USER); +// Subject subject = Subject.getSubject(accessControlContext); +// // Deal with authentication failure +// if (subject == null) { +// try { +// CloseReason.CloseCode closeCode = new CloseReason.CloseCode() { +// +// @Override +// public int getCode() { +// return 4001; +// } +// }; +// session.close(new CloseReason(closeCode, "Unauthorized")); +// if (log.isTraceEnabled()) +// log.trace("Unauthorized web socket " + wsSessionId + ". Closing with code " + closeCode.getCode() +// + "."); +// return; +// } catch (IOException e) { +// // silent +// } +// return;// ignore +// } + + if (log.isDebugEnabled()) + log.debug("WS#" + wsSessionId + " open for: " + subject); + remote = session.getBasicRemote(); + view = new WebSocketView(subject); + + // OSGi events + String[] topics = new String[] { TOPICS_BASE + "/*" }; + Hashtable ht = new Hashtable<>(); + ht.put(EventConstants.EVENT_TOPIC, topics); + ht.put(EventConstants.EVENT_FILTER, "(" + VIEW_UID + "=" + view.getUid() + ")"); + eventHandlerSr = bc.registerService(EventHandler.class, this, ht); + + if (log.isDebugEnabled()) + log.debug("New view " + view.getUid() + " opened, via web socket."); + } + + @OnMessage + public void onWebSocketText(@PathParam("topic") String topic, Session session, String message) + throws JsonMappingException, JsonProcessingException { + try { + if (log.isTraceEnabled()) + log.trace("WS#" + view.getUid() + " received:\n" + message + "\n"); +// JsonNode jsonNode = objectMapper.readTree(message); +// String topic = jsonNode.get(TOPIC).textValue(); + + final String computationUid = null; +// if (MY_TOPIC.equals(topic)) { +// view.checkRole(SPECIFIC_ROLE); +// computationUid= process(); +// } + remote.sendText("ACK " + topic); + } catch (Exception e) { + log.error("Error when receiving web socket message", e); + sendSystemErrorMessage(e); + } + } + + @OnClose + public void onWebSocketClose(CloseReason reason) { + if (eventHandlerSr != null) + eventHandlerSr.unregister(); + if (view != null && log.isDebugEnabled()) + log.debug("WS#" + view.getUid() + " closed: " + reason); + } + + @OnError + public void onWebSocketError(Throwable cause) { + if (view != null) { + log.error("WS#" + view.getUid() + " ERROR", cause); + } else { + if (log.isTraceEnabled()) + log.error("Error in web socket session " + wsSessionId, cause); + } + } + + @Override + public void handleEvent(Event event) { + try { + Object uid = event.getProperty(COMPUTATION_UID); + Exception exception = (Exception) event.getProperty(EXCEPTION); + if (exception != null) { + CmsExceptionsChain systemErrors = new CmsExceptionsChain(exception); + String sent = systemErrors.toJsonString(objectMapper); + remote.sendText(sent); + return; + } + String topic = event.getTopic(); + if (log.isTraceEnabled()) + log.trace("WS#" + view.getUid() + " " + topic + ": notify event " + topic + "#" + uid + ", " + event); + } catch (Exception e) { + log.error("Error when handling event for WebSocket", e); + sendSystemErrorMessage(e); + } + + } + + /** Sends an error message in JSON format. */ + protected void sendSystemErrorMessage(Exception e) { + CmsExceptionsChain systemErrors = new CmsExceptionsChain(e); + try { + if (remote != null) + remote.sendText(systemErrors.toJsonString(objectMapper)); + } catch (Exception e1) { + log.error("Cannot send WebSocket system error messages " + systemErrors, e1); + } + } +} diff --git a/org.argeo.slc.cms/src/org/argeo/cms/integration/TokenDescriptor.java b/org.argeo.slc.cms/src/org/argeo/cms/integration/TokenDescriptor.java new file mode 100644 index 000000000..1541b4f29 --- /dev/null +++ b/org.argeo.slc.cms/src/org/argeo/cms/integration/TokenDescriptor.java @@ -0,0 +1,49 @@ +package org.argeo.cms.integration; + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** A serializable descriptor of a token. */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class TokenDescriptor implements Serializable { + private static final long serialVersionUID = -6607393871416803324L; + + private String token; + private String username; + private String expiryDate; +// private Set roles; + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + +// public Set getRoles() { +// return roles; +// } +// +// public void setRoles(Set roles) { +// this.roles = roles; +// } + + public String getExpiryDate() { + return expiryDate; + } + + public void setExpiryDate(String expiryDate) { + this.expiryDate = expiryDate; + } + +} diff --git a/org.argeo.slc.cms/src/org/argeo/cms/integration/package-info.java b/org.argeo.slc.cms/src/org/argeo/cms/integration/package-info.java new file mode 100644 index 000000000..1405737ee --- /dev/null +++ b/org.argeo.slc.cms/src/org/argeo/cms/integration/package-info.java @@ -0,0 +1,2 @@ +/** Argeo CMS integration (JSON, web services). */ +package org.argeo.cms.integration; \ No newline at end of file diff --git a/org.argeo.slc.cms/src/org/argeo/cms/mail/EmailMigration.java b/org.argeo.slc.cms/src/org/argeo/cms/mail/EmailMigration.java new file mode 100644 index 000000000..30a74f3a8 --- /dev/null +++ b/org.argeo.slc.cms/src/org/argeo/cms/mail/EmailMigration.java @@ -0,0 +1,524 @@ +package org.argeo.cms.mail; + +import static java.lang.System.Logger.Level.DEBUG; +import static java.lang.System.Logger.Level.ERROR; +import static org.argeo.cms.mail.EmailUtils.describe; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.System.Logger; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.time.Instant; +import java.util.Date; +import java.util.Enumeration; +import java.util.Properties; + +import javax.mail.FetchProfile; +import javax.mail.Folder; +import javax.mail.Message; +import javax.mail.MessagingException; +import javax.mail.Multipart; +import javax.mail.Session; +import javax.mail.Store; +import javax.mail.URLName; +import javax.mail.internet.InternetHeaders; +import javax.mail.internet.MimeBodyPart; +import javax.mail.internet.MimeMessage; +import javax.mail.search.HeaderTerm; +import javax.mail.util.SharedFileInputStream; + +import com.sun.mail.imap.IMAPFolder; +import com.sun.mail.mbox.MboxFolder; +import com.sun.mail.mbox.MboxMessage; + +/** Migrates emails from one storage to the another one. */ +public class EmailMigration { + private final static Logger logger = System.getLogger(EmailMigration.class.getName()); + +// private String targetBaseDir; + + private String sourceServer; + private String sourceUsername; + private String sourcePassword; + + private String targetServer; + private String targetUsername; + private String targetPassword; + + private boolean targetSupportDualTypeFolders = true; + + public void process() throws MessagingException, IOException { +// Path baseDir = Paths.get(targetBaseDir).resolve(sourceUsername).resolve("mbox"); + + Store sourceStore = null; + try { + Properties sourceProperties = System.getProperties(); + sourceProperties.setProperty("mail.store.protocol", "imaps"); + + Session sourceSession = Session.getInstance(sourceProperties, null); + // session.setDebug(true); + sourceStore = sourceSession.getStore("imaps"); + sourceStore.connect(sourceServer, sourceUsername, sourcePassword); + + Folder defaultFolder = sourceStore.getDefaultFolder(); +// migrateFolders(baseDir, defaultFolder); + + // Always start with Inbox +// Folder inboxFolder = sourceStore.getFolder(EmailUtils.INBOX); +// migrateFolder(baseDir, inboxFolder); + + Properties targetProperties = System.getProperties(); + targetProperties.setProperty("mail.imap.starttls.enable", "true"); + targetProperties.setProperty("mail.imap.auth", "true"); + + Session targetSession = Session.getInstance(targetProperties, null); + // session.setDebug(true); + Store targetStore = targetSession.getStore("imap"); + targetStore.connect(targetServer, targetUsername, targetPassword); + +// Folder targetFolder = targetStore.getFolder(EmailUtils.INBOX); +// logger.log(DEBUG, "Source message count " + inboxFolder.getMessageCount()); +// logger.log(DEBUG, "Target message count " + targetFolder.getMessageCount()); + + migrateFolders(defaultFolder, targetStore); + } finally { + if (sourceStore != null) + sourceStore.close(); + + } + } + + protected void migrateFolders(Folder sourceParentFolder, Store targetStore) throws MessagingException, IOException { + folders: for (Folder sourceFolder : sourceParentFolder.list()) { + String sourceFolderName = sourceFolder.getName(); + + String sourceFolderFullName = sourceFolder.getFullName(); + char sourceFolderSeparator = sourceParentFolder.getSeparator(); + char targetFolderSeparator = targetStore.getDefaultFolder().getSeparator(); + String targetFolderFullName = sourceFolderFullName.replace(sourceFolderSeparator, targetFolderSeparator); + + // GMail specific + if (sourceFolderFullName.equals("[Gmail]")) { + migrateFolders(sourceFolder, targetStore); + continue folders; + } + if (sourceFolderFullName.startsWith("[Gmail]")) { + String subFolderName = null; + // Make it configurable + switch (sourceFolderName) { + case "All Mail": + case "Important": + case "Spam": + continue folders; + case "Sent Mail": + subFolderName = "Sent"; + default: + // does nothing + } + targetFolderFullName = subFolderName == null ? sourceFolder.getName() : subFolderName; + } + + // nature of the source folder + int messageCount = (sourceFolder.getType() & Folder.HOLDS_MESSAGES) != 0 ? sourceFolder.getMessageCount() + : 0; + boolean hasSubFolders = (sourceFolder.getType() & Folder.HOLDS_FOLDERS) != 0 + ? sourceFolder.list().length != 0 + : false; + + Folder targetFolder; + if (targetSupportDualTypeFolders) { + targetFolder = targetStore.getFolder(targetFolderFullName); + if (!targetFolder.exists()) { + targetFolder.create(Folder.HOLDS_FOLDERS | Folder.HOLDS_MESSAGES); + logger.log(DEBUG, "Created HOLDS_FOLDERS | HOLDS_MESSAGES folder " + targetFolder.getFullName()); + } + + } else { + if (hasSubFolders) {// has sub-folders + if (messageCount == 0) { + targetFolder = targetStore.getFolder(targetFolderFullName); + if (!targetFolder.exists()) { + targetFolder.create(Folder.HOLDS_FOLDERS); + logger.log(DEBUG, "Created HOLDS_FOLDERS folder " + targetFolder.getFullName()); + } + } else {// also has messages + Folder parentFolder = targetStore.getFolder(targetFolderFullName); + if (!parentFolder.exists()) { + parentFolder.create(Folder.HOLDS_FOLDERS); + logger.log(DEBUG, "Created HOLDS_FOLDERS folder " + parentFolder.getFullName()); + } + String miscFullName = targetFolderFullName + targetFolderSeparator + "_Misc"; + targetFolder = targetStore.getFolder(miscFullName); + if (!targetFolder.exists()) { + targetFolder.create(Folder.HOLDS_MESSAGES); + logger.log(DEBUG, "Created HOLDS_MESSAGES folder " + targetFolder.getFullName()); + } + } + } else {// no sub-folders + if (messageCount == 0) { // empty + logger.log(DEBUG, "Skip empty folder " + targetFolderFullName); + continue folders; + } + targetFolder = targetStore.getFolder(targetFolderFullName); + if (!targetFolder.exists()) { + targetFolder.create(Folder.HOLDS_MESSAGES); + logger.log(DEBUG, "Created HOLDS_MESSAGES folder " + targetFolder.getFullName()); + } + } + } + + if (messageCount != 0) { + + targetFolder.open(Folder.READ_WRITE); + try { + long begin = System.currentTimeMillis(); + sourceFolder.open(Folder.READ_ONLY); + migrateFolder(sourceFolder, targetFolder); + long duration = System.currentTimeMillis() - begin; + logger.log(DEBUG, targetFolderFullName + " - Migration of " + messageCount + " messages took " + + (duration / 1000) + " s (" + (duration / messageCount) + " ms per message)"); + } finally { + sourceFolder.close(); + targetFolder.close(); + } + } + + // recursive + if (hasSubFolders) { + migrateFolders(sourceFolder, targetStore); + } + } + } + + protected void migrateFoldersToFs(Path baseDir, Folder sourceFolder) throws MessagingException, IOException { + folders: for (Folder folder : sourceFolder.list()) { + String folderName = folder.getName(); + + if ((folder.getType() & Folder.HOLDS_MESSAGES) != 0) { + // Make it configurable + switch (folderName) { + case "All Mail": + case "Important": + continue folders; + default: + // doe nothing + } + migrateFolderToFs(baseDir, folder); + } + if ((folder.getType() & Folder.HOLDS_FOLDERS) != 0) { + migrateFoldersToFs(baseDir.resolve(folder.getName()), folder); + } + } + } + + protected void migrateFolderToFs(Path baseDir, Folder sourceFolder) throws MessagingException, IOException { + + String folderName = sourceFolder.getName(); + sourceFolder.open(Folder.READ_ONLY); + + Folder targetFolder = null; + try { + int messageCount = sourceFolder.getMessageCount(); + logger.log(DEBUG, folderName + " - Message count : " + messageCount); + if (messageCount == 0) + return; +// logger.log(DEBUG, folderName + " - Unread Messages : " + sourceFolder.getUnreadMessageCount()); + + boolean saveAsFiles = false; + + if (saveAsFiles) { + Message messages[] = sourceFolder.getMessages(); + + for (int i = 0; i < messages.length; ++i) { +// logger.log(DEBUG, "MESSAGE #" + (i + 1) + ":"); + Message msg = messages[i]; +// String from = "unknown"; +// if (msg.getReplyTo().length >= 1) { +// from = msg.getReplyTo()[0].toString(); +// } else if (msg.getFrom().length >= 1) { +// from = msg.getFrom()[0].toString(); +// } + String subject = msg.getSubject(); + Instant sentDate = msg.getSentDate().toInstant(); +// logger.log(DEBUG, "Saving ... " + subject + " from " + from + " (" + sentDate + ")"); + String fileName = sentDate + " " + subject; + Path file = baseDir.resolve(fileName); + savePartsAsFiles(msg.getContent(), file); + } + } else { + long begin = System.currentTimeMillis(); + targetFolder = openMboxTargetFolder(sourceFolder, baseDir); + migrateFolder(sourceFolder, targetFolder); + long duration = System.currentTimeMillis() - begin; + logger.log(DEBUG, folderName + " - Migration of " + messageCount + " messages took " + (duration / 1000) + + " s (" + (duration / messageCount) + " ms per message)"); + } + } finally { + sourceFolder.close(); + if (targetFolder != null) + targetFolder.close(); + } + } + + protected Folder migrateFolder(Folder sourceFolder, Folder targetFolder) throws MessagingException, IOException { + String folderName = targetFolder.getName(); + + int lastSourceNumber; + int currentTargetMessageCount = targetFolder.getMessageCount(); + if (currentTargetMessageCount != 0) { + MimeMessage lastTargetMessage = (MimeMessage) targetFolder.getMessage(currentTargetMessageCount); + logger.log(DEBUG, folderName + " - Last target message " + describe(lastTargetMessage)); + Date lastTargetSent = lastTargetMessage.getReceivedDate(); + Message[] lastSourceMessage = sourceFolder + .search(new HeaderTerm(EmailUtils.MESSAGE_ID, lastTargetMessage.getMessageID())); + if (lastSourceMessage.length == 0) + throw new IllegalStateException("No message found with message ID " + lastTargetMessage.getMessageID()); + if (lastSourceMessage.length != 1) { + for (Message msg : lastSourceMessage) { + logger.log(ERROR, "Message " + describe(msg)); + + } + throw new IllegalStateException( + lastSourceMessage.length + " messages found with received date " + lastTargetSent.toInstant()); + } + lastSourceNumber = lastSourceMessage[0].getMessageNumber(); + } else { + lastSourceNumber = 0; + } + logger.log(DEBUG, folderName + " - Last source message number " + lastSourceNumber); + + int countToRetrieve = sourceFolder.getMessageCount() - lastSourceNumber; + + FetchProfile fetchProfile = new FetchProfile(); + fetchProfile.add(FetchProfile.Item.FLAGS); + fetchProfile.add(FetchProfile.Item.ENVELOPE); + fetchProfile.add(FetchProfile.Item.CONTENT_INFO); + fetchProfile.add(FetchProfile.Item.SIZE); + if (sourceFolder instanceof IMAPFolder) { + // IMAPFolder sourceImapFolder = (IMAPFolder) sourceFolder; + fetchProfile.add(IMAPFolder.FetchProfileItem.HEADERS); + fetchProfile.add(IMAPFolder.FetchProfileItem.MESSAGE); + } + + int batchSize = 100; + int batchCount = countToRetrieve / batchSize; + if (countToRetrieve % batchSize != 0) + batchCount = batchCount + 1; + // int batchCount = 2; // for testing + for (int i = 0; i < batchCount; i++) { + long begin = System.currentTimeMillis(); + + int start = lastSourceNumber + i * batchSize + 1; + int end = lastSourceNumber + (i + 1) * batchSize; + if (end >= (lastSourceNumber + countToRetrieve + 1)) + end = lastSourceNumber + countToRetrieve; + Message[] sourceMessages = sourceFolder.getMessages(start, end); + sourceFolder.fetch(sourceMessages, fetchProfile); + // targetFolder.appendMessages(sourceMessages); + // sourceFolder.copyMessages(sourceMessages,targetFolder); + + copyMessages(sourceMessages, targetFolder); +// copyMessagesToMbox(sourceMessages, targetFolder); + + String describeLast = describe(sourceMessages[sourceMessages.length - 1]); + +// if (i % 10 == 9) { + // free memory from fetched messages + sourceFolder.close(); + targetFolder.close(); + + sourceFolder.open(Folder.READ_ONLY); + targetFolder.open(Folder.READ_WRITE); +// logger.log(DEBUG, "Open/close folder in order to free memory"); +// } + + long duration = System.currentTimeMillis() - begin; + logger.log(DEBUG, folderName + " - batch " + i + " took " + (duration / 1000) + " s, " + + (duration / (end - start + 1)) + " ms per message. Last message " + describeLast); + } + + return targetFolder; + } + + protected Folder openMboxTargetFolder(Folder sourceFolder, Path baseDir) throws MessagingException, IOException { + String folderName = sourceFolder.getName(); + if (sourceFolder.getName().equals(EmailUtils.INBOX_UPPER_CASE)) + folderName = EmailUtils.INBOX;// Inbox + + Path targetDir = baseDir;// .resolve("mbox"); + Files.createDirectories(targetDir); + Path targetPath; + if (((sourceFolder.getType() & Folder.HOLDS_FOLDERS) != 0) && sourceFolder.list().length != 0) { + Path dir = targetDir.resolve(folderName); + Files.createDirectories(dir); + targetPath = dir.resolve("_Misc"); + } else { + targetPath = targetDir.resolve(folderName); + } + if (!Files.exists(targetPath)) + Files.createFile(targetPath); + URLName targetUrlName = new URLName("mbox:" + targetPath.toString()); + Properties targetProperties = new Properties(); + // targetProperties.setProperty("mail.mime.address.strict", "false"); + Session targetSession = Session.getDefaultInstance(targetProperties); + Folder targetFolder = targetSession.getFolder(targetUrlName); + targetFolder.open(Folder.READ_WRITE); + + return targetFolder; + } + + protected void copyMessages(Message[] sourceMessages, Folder targetFolder) throws MessagingException { + targetFolder.appendMessages(sourceMessages); + } + + protected void copyMessagesToMbox(Message[] sourceMessages, Folder targetFolder) + throws MessagingException, IOException { + Message[] targetMessages = new Message[sourceMessages.length]; + for (int j = 0; j < sourceMessages.length; j++) { + MimeMessage sourceMm = (MimeMessage) sourceMessages[j]; + InternetHeaders ih = new InternetHeaders(); + for (Enumeration e = sourceMm.getAllHeaderLines(); e.hasMoreElements();) { + ih.addHeaderLine(e.nextElement()); + } + Path tmpFileSource = Files.createTempFile("argeo-mbox-source", ".txt"); + Path tmpFileTarget = Files.createTempFile("argeo-mbox-target", ".txt"); + Files.copy(sourceMm.getRawInputStream(), tmpFileSource, StandardCopyOption.REPLACE_EXISTING); + + // we use ISO_8859_1 because it is more robust than US_ASCII with regard to + // missing characters + try (BufferedReader reader = Files.newBufferedReader(tmpFileSource, StandardCharsets.ISO_8859_1); + BufferedWriter writer = Files.newBufferedWriter(tmpFileTarget, StandardCharsets.ISO_8859_1);) { + int lineNumber = 0; + String line = null; + try { + while ((line = reader.readLine()) != null) { + lineNumber++; + if (line.startsWith("From ")) { + writer.write(">" + line); + logger.log(DEBUG, + "Fix line " + lineNumber + " in " + EmailUtils.describe(sourceMm) + ": " + line); + } else { + writer.write(line); + } + writer.newLine(); + } + } catch (IOException e) { + logger.log(ERROR, "Error around line " + lineNumber + " of " + tmpFileSource); + throw e; + } + } + + MboxMessage mboxMessage = new MboxMessage((MboxFolder) targetFolder, ih, + new SharedFileInputStream(tmpFileTarget.toFile()), sourceMm.getMessageNumber(), + EmailUtils.getUnixFrom(sourceMm), true); + targetMessages[j] = mboxMessage; + + // clean up + Files.delete(tmpFileSource); + Files.delete(tmpFileTarget); + } + targetFolder.appendMessages(targetMessages); + + } + + /** Save body parts and attachments as plain files. */ + protected void savePartsAsFiles(Object content, Path fileBase) throws IOException, MessagingException { + OutputStream out = null; + InputStream in = null; + try { + if (content instanceof Multipart) { + Multipart multi = ((Multipart) content); + int parts = multi.getCount(); + for (int j = 0; j < parts; ++j) { + MimeBodyPart part = (MimeBodyPart) multi.getBodyPart(j); + if (part.getContent() instanceof Multipart) { + // part-within-a-part, do some recursion... + savePartsAsFiles(part.getContent(), fileBase); + } else { + String extension = ""; + if (part.isMimeType("text/html")) { + extension = "html"; + } else { + if (part.isMimeType("text/plain")) { + extension = "txt"; + } else { + // Try to get the name of the attachment + extension = part.getDataHandler().getName(); + } + } + String filename = fileBase + "." + extension; + System.out.println("... " + filename); + out = new FileOutputStream(new File(filename)); + in = part.getInputStream(); + int k; + while ((k = in.read()) != -1) { + out.write(k); + } + } + } + } + } finally { + if (in != null) { + in.close(); + } + if (out != null) { + out.flush(); + out.close(); + } + } + } + + public void setSourceServer(String sourceServer) { + this.sourceServer = sourceServer; + } + + public void setSourceUsername(String sourceUsername) { + this.sourceUsername = sourceUsername; + } + + public void setSourcePassword(String sourcePassword) { + this.sourcePassword = sourcePassword; + } + + public void setTargetServer(String targetServer) { + this.targetServer = targetServer; + } + + public void setTargetUsername(String targetUsername) { + this.targetUsername = targetUsername; + } + + public void setTargetPassword(String targetPassword) { + this.targetPassword = targetPassword; + } + + public static void main(String args[]) throws Exception { + if (args.length < 6) + throw new IllegalArgumentException( + "usage: "); + String sourceServer = args[0]; + String sourceUsername = args[1]; + String sourcePassword = args[2]; + String targetServer = args[3]; + String targetUsername = args[4]; + String targetPassword = args[5]; + + EmailMigration emailMigration = new EmailMigration(); + emailMigration.setSourceServer(sourceServer); + emailMigration.setSourceUsername(sourceUsername); + emailMigration.setSourcePassword(sourcePassword); + emailMigration.setTargetServer(targetServer); + emailMigration.setTargetUsername(targetUsername); + emailMigration.setTargetPassword(targetPassword); + + emailMigration.process(); + } +} diff --git a/org.argeo.slc.cms/src/org/argeo/cms/mail/EmailUtils.java b/org.argeo.slc.cms/src/org/argeo/cms/mail/EmailUtils.java new file mode 100644 index 000000000..09b731032 --- /dev/null +++ b/org.argeo.slc.cms/src/org/argeo/cms/mail/EmailUtils.java @@ -0,0 +1,118 @@ +package org.argeo.cms.mail; + +import java.util.Date; + +import javax.mail.Address; +import javax.mail.Flags; +import javax.mail.Message; +import javax.mail.MessagingException; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeMessage; + +/** Utilities around emails. */ +public class EmailUtils { + public final static String INBOX = "Inbox"; + public final static String INBOX_UPPER_CASE = "INBOX"; + public final static String MESSAGE_ID = "Message-ID"; + + public static String getMessageId(Message msg) { + try { + return msg instanceof MimeMessage ? ((MimeMessage) msg).getMessageID() : ""; + } catch (MessagingException e) { + throw new IllegalStateException("Cannot extract message id from " + msg, e); + } + } + + public static String describe(Message msg) { + try { + return "Message " + msg.getMessageNumber() + " " + msg.getSentDate().toInstant() + " " + getMessageId(msg); + } catch (MessagingException e) { + throw new IllegalStateException("Cannot describe " + msg, e); + } + } + + static void setHeadersFromFlags(MimeMessage msg, Flags flags) { + try { + StringBuilder status = new StringBuilder(); + if (flags.contains(Flags.Flag.SEEN)) + status.append('R'); + if (!flags.contains(Flags.Flag.RECENT)) + status.append('O'); + if (status.length() > 0) + msg.setHeader("Status", status.toString()); + else + msg.removeHeader("Status"); + + boolean sims = false; + String s = msg.getHeader("X-Status", null); + // is it a SIMS 2.0 format X-Status header? + sims = s != null && s.length() == 4 && s.indexOf('$') >= 0; + //status.setLength(0); + if (flags.contains(Flags.Flag.DELETED)) + status.append('D'); + else if (sims) + status.append('$'); + if (flags.contains(Flags.Flag.FLAGGED)) + status.append('F'); + else if (sims) + status.append('$'); + if (flags.contains(Flags.Flag.ANSWERED)) + status.append('A'); + else if (sims) + status.append('$'); + if (flags.contains(Flags.Flag.DRAFT)) + status.append('T'); + else if (sims) + status.append('$'); + if (status.length() > 0) + msg.setHeader("X-Status", status.toString()); + else + msg.removeHeader("X-Status"); + + String[] userFlags = flags.getUserFlags(); + if (userFlags.length > 0) { + status.setLength(0); + for (int i = 0; i < userFlags.length; i++) + status.append(userFlags[i]).append(' '); + status.setLength(status.length() - 1); // smash trailing space + msg.setHeader("X-Keywords", status.toString()); + } + if (flags.contains(Flags.Flag.DELETED)) { + s = msg.getHeader("X-Dt-Delete-Time", null); + if (s == null) + // XXX - should be time + msg.setHeader("X-Dt-Delete-Time", "1"); + } + } catch (MessagingException e) { + // ignore it + } + } + + protected static String getUnixFrom(MimeMessage msg) { + Address[] afrom; + String from; + Date ddate; + String date; + try { + if ((afrom = msg.getFrom()) == null || + !(afrom[0] instanceof InternetAddress) || + (from = ((InternetAddress)afrom[0]).getAddress()) == null) + from = "UNKNOWN"; + if ((ddate = msg.getReceivedDate()) == null || + (ddate = msg.getSentDate()) == null) + ddate = new Date(); + } catch (MessagingException e) { + from = "UNKNOWN"; + ddate = new Date(); + } + date = ddate.toString(); + // date is of the form "Sat Aug 12 02:30:00 PDT 1995" + // need to strip out the timezone + return "From " + from + " " + + date.substring(0, 20) + date.substring(24); + } + + /** Singleton. */ + private EmailUtils() { + } +} diff --git a/org.argeo.slc.runtime/build.properties b/org.argeo.slc.runtime/build.properties index 7786bd6ec..5d082eaf6 100644 --- a/org.argeo.slc.runtime/build.properties +++ b/org.argeo.slc.runtime/build.properties @@ -2,8 +2,4 @@ source.. = src/ output.. = bin/ bin.includes = META-INF/,\ . -additional.bundles = org.w3c.dom.svg,\ - org.w3c.dom.smil,\ - org.w3c.css.sac,\ - org.apache.xmlgraphics,\ - org.argeo.init +additional.bundles = org.argeo.init