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 \
--- /dev/null
+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");
+// }
+
+}
--- /dev/null
+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> T extractFrom(Set<T> 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;
+ }
+}
--- /dev/null
+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> T extractFrom(Set<T> creds) {
+ if (creds.size() > 0)
+ return creds.iterator().next();
+ else
+ return null;
+ }
+
+ protected String redirectTo(HttpServletRequest request) {
+ return null;
+ }
+}
--- /dev/null
+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<String, String> 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<Void>() {
+//
+// @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;
+ }
+}
--- /dev/null
+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<String> 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();
+ }
+
+}
--- /dev/null
+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> T extractFrom(Set<T> creds) {
+ if (creds.size() > 0)
+ return creds.iterator().next();
+ else
+ return null;
+ }
+
+ public void setUserManager(CmsUserManager userManager) {
+ this.userManager = userManager;
+ }
+}
--- /dev/null
+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<EventHandler> eventHandlerSr;
+
+ // json
+ private ObjectMapper objectMapper = new ObjectMapper();
+
+ private WebSocketView view;
+
+ @OnOpen
+ public void onOpen(Session session, EndpointConfig endpointConfig) {
+ Map<String, List<String>> 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<String, Object> 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<String, Object> 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);
+ }
+ }
+}
--- /dev/null
+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<String> 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<String> getRoles() {
+// return roles;
+// }
+//
+// public void setRoles(Set<String> roles) {
+// this.roles = roles;
+// }
+
+ public String getExpiryDate() {
+ return expiryDate;
+ }
+
+ public void setExpiryDate(String expiryDate) {
+ this.expiryDate = expiryDate;
+ }
+
+}
--- /dev/null
+/** Argeo CMS integration (JSON, web services). */
+package org.argeo.cms.integration;
\ No newline at end of file
--- /dev/null
+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<String> 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: <source IMAP server> <source username> <source password> <target IMAP server> <target username> <target password>");
+ 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();
+ }
+}
--- /dev/null
+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() : "<N/A>";
+ } 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() {
+ }
+}
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