Saturday, April 20, 2019

Setting up local mail server using Apache James Server App



Apache James is a popular open source mail server. It supports SMTP, POP3 and IMAP protocols. This can be very useful for testing email functionality of an application which is being developed and if you do not have any mail server available.


For more details visit: https://james.apache.org/documentation.html

We are going to see following:

1- Install Apache James on windows
2- Add local domain
3- Add user account
4- Use of Mozilla Thunderbird to send and receive emails

Pre-requisites:

1- Java 1.8 or later. Download Java 1.8 From here

2- Apache james-server-app-3.2.0 or newer. Download James Server from here


List of all versions can be found here: https://james.apache.org/download.cgi#Apache_James_Server


3- Mozilla Thunderbird email client. Download latest version from here

Let's get started....
-------------------------------------------------------------------------------------------------------------------
Installing the james-server-app:
-------------------------------------------------------------------------------------------------------------------

Assuming you have already downloaded latest distribution from the download site. Please follow these steps:

1- Extract the james-server-app-3.2.0-app.zip to a local folder say C:\Personal\ProjectSofts\MailServer

2- Open windows command prompt 

3- Change the directory to bin folder (<installationDir>/james-server-app-3.2.0\bin) for example C:\Personal\ProjectSofts\MailServer\james-server-app-3.2.0\bin.

Note: Before run Apache James mail server, make sure you have Java 1.8 installed on your machine

-------------------------------------------------------------------------------------------------------------------
Starting up james mail server:
-------------------------------------------------------------------------------------------------------------------

Follow these steps:

1- Execute run.bat in the dos window then Apache James server started.



2- When Apache James mail server starts, it starts both SMTP and POP3 mail server.
     As we know that SMTP is used to send email out while POP3 is used to receive income email.
     Port number 25 is the default SMTP server port, and Port number 110 is the default for POP3.

3- If you want to change the port numbers, then you can do following:

    3.1- Go to <installationDir>/james-server-app-3.2.0\conf. for example:             

C:\Personal\ProjectSofts\MailServer\james-server-app-3.2.0\conf
   
    3.2- Open smtpserver.xml in notepad or any text editor
    3.3- Find <bind> element and update the port number as needed. I am using default port for now
            
<smtpservers>
   <!-- The SMTP server is enabled by default -->
   <!-- Disabling blocks will stop them from listening, -->
   <!-- but does not free as many resources as removing them would -->
   <smtpserver enabled="true">
     <jmxName>smtpserver</jmxName>
     <!-- Configure this to bind to a specific inetaddress -->
     <!-- Please NOTE: you should add this IP also to your RemoteAddrNotInNetwork -->
     <!-- in order to avoid relay check for locallly generated bounces -->
     <!--
            Port 25 is the well-known/IANA registered port for SMTP.
            Port 465 is the well-known/IANA registered port for SMTP over TLS.
      -->
     <bind>0.0.0.0:25</bind>

     <connectionBacklog>200</connectionBacklog>
…..
….
</smtpservers>
    
    3.4- Open pop3server.xml in notepad or any text editor
    3.5- Find <bind> element and update the port number as needed. I am using default port for now
   
<pop3servers>
    <!-- The POP3 server is enabled by default -->
    <!-- Disabling blocks will stop them from listening, -->
    <!-- but does not free as many resources as removing them would -->
    <pop3server enabled="true">
       <jmxName>pop3server</jmxName>
       <!-- Configure this to bind to a specific inetaddress -->
       <!-- port 995 is the well-known/IANA registered port for POP3S  ie over SSL/TLS -->
       <!-- port 110 is the well-known/IANA registered port for Standard POP3 -->
       <bind>0.0.0.0:110</bind>

       <connectionBacklog>200</connectionBacklog>…..
….
</pop3servers>

    3.6- Now restart Apache James server, it will use the new SMTP and POP3 port number if changed


So far we have seen how to install, start and simple configurations of Apache james mail server. Let's proceed further with its usage. 

Apache James is shipped with another tool called james-cli. Its a tool used for command line inputs. We will use james-cli command line interface for adding/removing domain, users etc.

-------------------------------------------------------------------------------------------------------------------
Add a local domain: It will be used to create users against this domain.
-------------------------------------------------------------------------------------------------------------------

Follow these steps:

 1- Open command prompt and go to <installationDir>/james-server-app-3.2.0\bin. Example as shown above: 

C:\Personal\ProjectSofts\MailServer\james-server-app-3.2.0\bin

 2- Start the james server if not already started. 

 3- On command prompt execute following command:

c:\Personal\ProjectSofts\MailServer\james-server-app-3.2.0\bin>james-cli.bat

This will print all available command line options for Apache james mail server configuration. 

usage: java org.apache.james.cli.ServerCmd --host <arg> <command>
 -h,--host <arg>   node hostname or ip address
 -p,--port <arg>   remote jmx agent port number
AddUser <username> <password>
RemoveUser <username>
ListUsers
AddDomain <domainName>
RemoveDomain <domainName>
ContainsDomain <domainName>
ListDomains
ListMappings
ListUserDomainMappings <user> <domain>
AddAddressMapping <fromUser> <fromDomain> <toAddress>
RemoveAddressMapping <fromUser> <fromDomain> <toAddress>
AddRegexMapping <user> <domain> <regex>
RemoveRegexMapping <user> <domain> <regex>
SetPassword <username> <password>
CopyMailbox <srcBean> <dstBean>
DeleteUserMailboxes <user>
CreateMailbox <namespace> <user> <name>
ListUserMailboxes <user>
DeleteMailbox <namespace> <user> <name>
ImportEml <namespace> <user> <name> <path>
GetStorageQuota <quotaroot>
GetMessageCountQuota <quotaroot>
GetQuotaroot <namespace> <user> <name>
GetMaxStorageQuota <quotaroot>
GetMaxMessageCountQuota <quotaroot>
SetMaxStorageQuota <quotaroot> <maxMessageCount>
SetMaxMessageCountQuota <quotaroot> <maxStorage>
SetGlobalMaxStorageQuota <maxStorage>
SetGlobalMaxMessageCountQuota <maxMessageCount>
GetGlobalMaxStorageQuota
GetGlobalMaxMessageCountQuota
ReindexMailbox <namespace> <user> <name>
ReindexAll
GetSieveQuota
SetSieveQuota <quota>
RemoveSieveQuota
GetSieveUserQuota <username>
SetSieveUserQuota <username> <quota>
RemoveSieveUserQuota <username>
AddActiveSieveScript <username> <scriptname> <path>


 4- Let's say i want to add domain as "abhinav.com", so to add domain use following command. 

C:\Personal\ProjectSofts\MailServer\james-server-app-3.2.0\bin>james-cli.bat AddDomain abhinav.com

AddDomain command executed successfully in 276 ms.
It will show the message on command prompt if domain is added successfully or else you will get error if there is any.

-------------------------------------------------------------------------------------------------------------------
Add an user: Since we have created domain already, let's add a user which will send/receive emails
-------------------------------------------------------------------------------------------------------------------

Follow these steps:

 1- Open command prompt and go to <installationDir>/james-server-app-3.2.0\bin. Example as shown above: 

C:\Personal\ProjectSofts\MailServer\james-server-app-3.2.0\bin

 2- Start the james server if not already started. 
 3- Let's say i want to add user as "admin@abhinav.com" with password as "admin". 
 4- Run following command to add user against the domain specified above. We will use "AddUser" command to add users

C:\Personal\ProjectSofts\MailServer\james-server-app-3.2.0\bin>james-cli.bat AddUser admin@abhinav.com admin

AddUser command executed successfully in 225 ms.

It will show the message on command prompt if user is added successfully or else you will get error if there is any.
-------------------------------------------------------------------------------------------------------------------
Now it's time to use Thunderbird To Connect Apache James Mail Server On Localhost.
--------------------------------------------------------------------------------------------------------------------

Follow these steps as given below:

1- Download and install Thunderbird client.
2- Edit hosts file in Windows, add below IP domain mappings.With this change Mozilla Thunderbird will know that both domain smtp.abhinav.com and pop3.abhinav.com will be resolved to IP 127.0.0.1.

127.0.0.1 abhinav.com
127.0.0.1 smtp.abhinav.com
127.0.0.1 pop3.abhinav.com

Note: Close the host file after editing.

3- Start Thunderbird for the first time, then click Email in Accounts —> Set up an account section.


4- Input Your name : Admin, Email address : admin@abhinav.com, Password : admin, in the popup dialog.


5- Click "Continue". It will check the configurations and prompt for next steps.

6- In the next step, Select "POP3" radio button. 



7- Select "Manual config". It will give some additional options to configure ports, authentication mechanisms and security. 

8- Update the outgoing email port to 25, change the host from mail.abhinav.com to just "abhinav.com", update the SSL option to "None" and leave other values as is.




9- Click "Re-test" button to check whether the configuration is correct or not.

10- Click "Done" button in above picture then it goes to domain not use encryption security warning dialog, check the checkbox and click Done button to finish the settings. 


11- Let's add another user so that we can test sending and receiving of emails. 

C:\Personal\ProjectSofts\MailServer\james-server-app-3.2.0\bin>james-cli.bat AddUser test@abhinav.com test

AddUser command executed successfully in 225 ms.


12- Add the newly added account to the Mozilla Thunderbird client following the same above steps.


13- Let's send and receive and emails to these two email Ids.




Now you can see we are able to send and receive emails successfully. 



-------------------------------------------------------------------------------------------------------

There are many other open source alternatives for using local SMTP servers.

1- Hedwid: It is an open source IMAP, SMTP, POP3 server written in Java, designed with ease of installation and configuration in mind. Hedwig enables storage of mail message headers in a relational database(MySQL or Oracle) and mail messages in a file system.


Download and Try:


2- FakeSMTP: It is a Free Fake SMTP Server with GUI for testing emails in applications easily. It is written in Java.
Configure your application to use "localhost" as your SMTP server, and all emails will be intercepted and displayed in this software.

Download latest version: 


For more details see here: http://nilhcem.com/FakeSMTP/index.html





Saturday, August 12, 2017

Downloading content from alfresco repository via custom Download Webscript


Alfresco provides Content REST API for downloading the content from repository as given below.

GET /alfresco/service/api/node/content/{store_type}/{store_id}/{id}?a=false&alf_ticket={TICKET}

Example:

http://127.0.0.1:8080/alfresco/service/api/node/content/workspace/SpacesStore/85d2a84d-271b-41b1-9449-02f5942893a0?a=false&alf_ticket=TICKET_5550fa5e9b87bead8f008e906185e023b7ce21ed

Where: 
a: attach. if true, force download of content as attachment. Possible values are true/false
store_type: workspace
store_id: SpacesStore
id: nodeRefId (UUID of the node)

A StoreRef is comprised of:

Store Protocol - that is, the type of store
Store Identifier - the id of the store

Example storeRefs are:

workspace://SpacesStore  (store_type://store_id)
version://versionStore  (store_type://store_id)
archive://SpacesStore  (store_type://store_id)

See here for Content API Webscript definition: content.get.desc.xml

Any external system or client can use this API to download content from Alfresco repository via an authenticated user which would be registered in Alfresco.

If you want to know how to get auth ticket (alf_ticket) then visit: Alfresco Login REST API

OOTB Download REST API will allow to download the content to any user who is registered in alfresco, since every user has consumer access to every site by default via "EVERYONE" group. But let's suppose you want to put some kind of restrictions to the Download API. Let's say for example:

1- Allow download if requesting user is authorized to download the content.
2- Want to validate the site level user role e.g. only Managers/Collaborators/Contributors can download, Consumers should not be allowed to download.
3- Want to check if user is part of DOWNLOADERS groups then allow them to download 
..... 
and so on. 

There could be many such cases which we can not achieve via OOTB REST API provided by alfresco. If your contents has copyrights you will definitely not allow users to download the content who are unauthorized. 

To handle such scenarios you need to write a custom Download webscript.

Alfresco provides a Webscript called "org.alfresco.repo.web.scripts.content.StreamContent". 

By extending this class we can add our custom user validation logic and leave the streaming and download handling part to this OOTB Webscript.

So, let's take a use case where you don't want a consumer user to download the content from a site. To achieve this use case a custom webscript will be written as given below:


DownloadContentWebscript.java


/*
 * Created By: Abhinav Kumar Mishra
 * Copyright &copy; 2017. Abhinav Kumar Mishra. 
 * All rights reserved.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.github.abhinavmishra14.webscript;

import java.io.IOException;
import java.util.Locale;
import java.util.Set;

import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.security.permissions.AccessDeniedException;
import org.alfresco.repo.web.scripts.content.StreamContent;
import org.alfresco.service.cmr.repository.ContentService;
import org.alfresco.service.cmr.repository.InvalidNodeRefException;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.security.AccessPermission;
import org.alfresco.service.cmr.security.AuthenticationService;
import org.alfresco.service.cmr.security.AuthorityService;
import org.alfresco.service.cmr.security.PermissionService;
import org.alfresco.service.cmr.site.SiteInfo;
import org.alfresco.service.cmr.site.SiteService;
import org.alfresco.service.namespace.QName;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.extensions.webscripts.Status;
import org.springframework.extensions.webscripts.WebScriptException;
import org.springframework.extensions.webscripts.WebScriptRequest;
import org.springframework.extensions.webscripts.WebScriptResponse;

/**
 * The Class DownloadContentWebscript.
 * 
 */
public class DownloadContentWebscript extends StreamContent {

 /** The Constant LOGGER. */
 private static final Logger LOGGER = LoggerFactory.getLogger(DownloadContentWebscript.class);

 /** The content service. */
 private ContentService contentService;

 /** The authentication service. */
 private AuthenticationService authenticationService;

 /** The site service. */
 private SiteService siteService;

 /** The authority service. */
 private AuthorityService authorityService;

 /* (non-Javadoc)
  * @see org.springframework.extensions.webscripts.WebScript#execute(org.springframework.extensions.webscripts.WebScriptRequest, org.springframework.extensions.webscripts.WebScriptResponse)
  */
 @Override
 public void execute(final WebScriptRequest request,
   final WebScriptResponse response) throws IOException {
  LOGGER.info("Started executing DownloadContentWebscript...");
  try {
   final NodeRef nodeRef = getParameterAsNodeRef(request, "nodeRef");
   final String userName = authenticationService.getCurrentUserName();
   if(isNotAuthorised(nodeRef, userName, siteService, permissionService, authorityService)) {
    response.setStatus(401);
    response.getWriter().write("User is unauthorised to download the requested content!");
   } else {
    if(LOGGER.isDebugEnabled()) {
     LOGGER.debug("Processing the download requested by: {}", userName);
    }
    final boolean attach = Boolean.valueOf(request.getParameter("attach"));
    processDownload(request, response, nodeRef, attach, ContentModel.PROP_CONTENT);
   }
  } catch (AccessDeniedException accessDenied) {
   LOGGER.error("Access denied while downloading content", accessDenied);
   throw new WebScriptException(Status.STATUS_UNAUTHORIZED,
     accessDenied.getMessage(), accessDenied);
  } catch (IOException | AlfrescoRuntimeException
    | InvalidNodeRefException excp) {
   LOGGER.error("Exception occurred while downloading content", excp);
   throw new WebScriptException(Status.STATUS_INTERNAL_SERVER_ERROR,
     excp.getMessage(), excp);
  }
  LOGGER.info("Existing from DownloadContentWebscript...");
 }

 /**
  * Process download.
  *
  * @param request the request
  * @param response the response
  * @param nodeRef the node ref
  * @param attach the attach
  * @param propertyQName the property q name
  * @throws IOException the IO exception
  */
 private void processDownload(final WebScriptRequest request,
   final WebScriptResponse response, final NodeRef nodeRef, final boolean attach,
   final QName propertyQName) throws IOException {
  String userAgent = request.getHeader("User-Agent");
  userAgent = StringUtils.isNotBlank(userAgent) ? userAgent.toLowerCase(Locale.ENGLISH) : StringUtils.EMPTY;
  final boolean isClientSupported= userAgent.contains("msie")
    || userAgent.contains(" trident/")
    || userAgent.contains(" chrome/")
    || userAgent.contains(" firefox/");

  if (attach && isClientSupported) {
   String fileName = (String) this.nodeService.getProperty(nodeRef, ContentModel.PROP_NAME);
   if (userAgent.contains("msie") || userAgent.contains(" trident/")) {
    final String mimeType = contentService.getReader(nodeRef, propertyQName).getMimetype();
    if (!(this.mimetypeService.getMimetypes(FilenameUtils.getExtension(fileName)).contains(mimeType))) {
     fileName = FilenameUtils.removeExtension(fileName)+ FilenameUtils.EXTENSION_SEPARATOR_STR
       + this.mimetypeService.getExtension(mimeType);
    }
   }
   streamContent(request, response, nodeRef, propertyQName, attach, fileName, null);
  } else {
   streamContent(request, response, nodeRef, propertyQName, attach, null, null);
  }
 }

 /**
     * Create NodeRef instance from a WebScriptRequest parameter.
     *
     * @param req the req
     * @param paramName the param name
     * @return the parameter as node ref
     */
    private NodeRef getParameterAsNodeRef(final WebScriptRequest req, final String paramName) {
        final String nodeRefStr = StringUtils.trimToNull(req.getParameter(paramName));
        if (StringUtils.isBlank(nodeRefStr)) {
            throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Missing " + paramName + " parameter");
        }
        if (!NodeRef.isNodeRef(nodeRefStr)) {
            throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Incorrect format for " + paramName + " paramater");
        }
        final NodeRef nodeRef = new NodeRef(nodeRefStr);
        if (!nodeService.exists(nodeRef)) {
            throw new WebScriptException(Status.STATUS_BAD_REQUEST, paramName + " not found");
        }
        return nodeRef;
    }
   
 /**
  * Checks if is not authorised.
  *
  * @param nodeRef the node ref
  * @param userName the user name
  * @param siteService the site service
  * @param permissionService the permission service
  * @param authorityService the authority service
  * @return true, if checks if is not authorised
  */
 private boolean isNotAuthorised(final NodeRef nodeRef,
   final String userName, final SiteService siteService,
   final PermissionService permissionService,
   final AuthorityService authorityService) {
  boolean isNotAuthorised = false;
  final SiteInfo siteInfo = siteService.getSite(nodeRef);
  // Checking siteInfo, If it is null that means user is not a member of site and 
  // hence isNotAuthorised is default to false.
  if (null != siteInfo) {
   if (siteService.isMember(siteInfo.getShortName(), userName)) {
    final Set<AccessPermission> permissions = permissionService.getAllSetPermissions(nodeRef);
    if(LOGGER.isDebugEnabled()) {
     LOGGER.debug("Checking isNotAuthorised, Available access permissions are: {}", permissions);
    }
    for (final AccessPermission permission : permissions) {
     if (permission.getPermission().equals("SiteConsumer")
       || permission.getPermission().equals("Consumer")) {
      if (permission.getAuthorityType().equals("USER")
        && permission.getAuthority().equals(userName)) {
       isNotAuthorised = true;
       break;
      } else if (permission.getAuthorityType().toString().equals("GROUP")) {
       //Set run as system user since other users including consumers can not fetch authorities
       AuthenticationUtil.setRunAsUserSystem();
       final Set<String> authorities = authorityService.getAuthoritiesForUser(userName);
       //Clear system user context and set original user context
       AuthenticationUtil.clearCurrentSecurityContext();
       AuthenticationUtil.setFullyAuthenticatedUser(userName);
       if(LOGGER.isDebugEnabled()) {
        LOGGER.debug("Checking permissions at GROUP level, user has following authorities: {}", authorities);
       }
       for (final String authority : authorities) {
        if (authority.equals(permission.getAuthority())) {
         isNotAuthorised = true;
         break;
        }
       }
      }
     }
    }
   } else {
    isNotAuthorised = true;//Not a member in the site.
   }
  }
  return isNotAuthorised;
 }
 
 /**
  * Sets the content service.
  *
  * @param contentService the content service
  */
 public void setContentService(final ContentService contentService) {
  this.contentService = contentService;
 }

 /**
  * Sets the authentication service.
  *
  * @param authenticationService the authentication service
  */
 public void setAuthenticationService(final AuthenticationService authenticationService) {
  this.authenticationService = authenticationService;
 }

 /**
  * Sets the site service.
  *
  * @param siteService the site service
  */
 public void setSiteService(final SiteService siteService) {
  this.siteService = siteService;
 }

 /**
  * Sets the authority service.
  *
  * @param authorityService the authority service
  */
 public void setAuthorityService(final AuthorityService authorityService) {
  this.authorityService = authorityService;
 }
}

WebScript Description:
alfresco/extension/templates/webscripts/com/github/abhinavmishra14/downloadContent.get.desc.xml


<webscript>
 <shortname>Download Content</shortname>
 <description>
  <![CDATA[
      Download Content based on role and permissions check.
      Where:
      nodeRef- NodeRef of the content e.g. workspace://SpacesStore/5cee9f74-eb2a-43a4-965d-6e4fcde4fb9e
      attach (Optional)- if true, force download of content as attachment (Possible values are true/false)
      
      Sample URL:
      http://127.0.0.1:8080/alfresco/service/abhinavmishra14/downloadContent?nodeRef=workspace://SpacesStore/5cee9f74-eb2a-43a4-965d-6e4fcde4fb9e&attach=true
   ]]>
 </description>
 <url>/abhinavmishra14/downloadContent?nodeRef={nodeRef}&amp;attach={attach?}</url>
 <format default="">argument</format>
 <authentication>user</authentication>
 <transaction allow="readonly" />
 <family>common</family>
</webscript>


Bean definition:
webscript-context.xml


<bean id="webscript.com.github.abhinavmishra14.downloadContent.get" class="com.github.abhinavmishra14.webscript.DownloadContentWebscript" parent="webscript">
  <property name="permissionService" ref="PermissionService" />
  <property name="nodeService" ref="NodeService" />
  <property name="mimetypeService" ref="MimetypeService" />
  <property name="delegate" ref="webscript.content.streamer" />
  <property name="repository" ref="repositoryHelper" />
  
  <property name="contentService" ref="ContentService" />
  <property name="authenticationService" ref="AuthenticationService" />
  <property name="siteService" ref="SiteService" />
  <property name="authorityService" ref="AuthorityService"/>
 </bean>


To test the above web-script it is assumed that you have a site where there are users with SiteConsumer/Consumer role.


Test URL:

GET http://127.0.0.1:8080/alfresco/service/abhinavmishra14/downloadContent?nodeRef=workspace://SpacesStore/5cee9f74-eb2a-43a4-965d-6e4fcde4fb9e&attach=true

OR

GET http://127.0.0.1:8080/alfresco/service/abhinavmishra14/downloadContent?nodeRef=workspace://SpacesStore/5cee9f74-eb2a-43a4-965d-6e4fcde4fb9e&attach=true&alf_ticket=TICKET_4e3c094e886a54893a267cd3cac402c1cc5b4fd9 



Note: Custom download webscript can also be implemented using org.springframework.extensions.webscripts.AbstractWebScript but streaming logic would have to be written by developer itself. Like setting the stream to response, preparing the attachment etc. 

For more on upload/delete/download visit my post here: Upload/Download/Delete REST APIs