Thursday, September 08, 2022

Java Freemarker with Domino

There are plenty of different Java template engines but for last years I used to stick to FreeMarker. It's open sourced and licensed under the Apache License, Version 2.0.

Here I only want to demonstrate how to integrate it with Domino nicely as it requires to write TemplateLoader class.

Build result based on tempalte "page"
Configuration cfg = new Configuration(Configuration.VERSION_2_3_31);
cfg.setDefaultEncoding("UTF-8");
cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
cfg.setLogTemplateExceptions(false);

Get the template (uses cache internally)
DominoTemplateLoader dominoLoader = new DominoTemplateLoader(getDatabase());
cfg.setTemplateLoader(dominoLoader);

Template template = cfg.getTemplate("page");

/* Merge data-model with template */
HashMap tags = new HashMap();
tags.put("title", "hellow world");
tags.put("description", "Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit.");
			
Writer out = new StringWriter();
template.process(tags, out);
String html = out.toString();
The most important part was actually to build DominoTemplateLoader class and below you can see it
public class DominoTemplateLoader implements TemplateLoader {
	private View m_view;

	public DominoTemplateLoader(Database database) throws NotesException {
		m_view = database.getView("($Template)");
	}

	public void closeTemplateSource(Object templateSource) throws IOException {
		Document doc = (Document) templateSource;
		try {
			doc.recycle();
		} catch (NotesException e) {
			e.printStackTrace();
		}
	}

	public Object findTemplateSource(String id) throws IOException {
		try {
			return m_view.getDocumentByKey(id, true);
		} catch (NotesException e) {
			e.printStackTrace();
		}

		return null;
	}

	public long getLastModified(Object templateSource) {
		Document doc = (Document) templateSource;

		try {
			return doc.getLastModified().toJavaDate().getTime();
		} catch (NotesException e) {
			e.printStackTrace();
		}

		return 0;
	}

	public Reader getReader(Object templateSource, String encoding) throws IOException {
		if (templateSource == null) return null;
		
		Document doc = (Document) templateSource;
		try {
			return doc.getFirstItem("Body").getReader();
		} catch (NotesException e) {
			e.printStackTrace();
		}
		return null;
	}
}
As you can see the Loader class get document form a view and simply get data from item Body.

Friday, December 10, 2021

Journalize email from Exchange to Domino using Addin

One of my customer moves to Office 365 and also wants to move to to Exchange/Outlook 356 during next year while keeping Domino app running as it is of now.

They have customized mail boxes with few actions which allow to journalize emails into their Domino applications and that is quite critical functionality so that would need to be mirrored.

We have decided to built Outlook Add-in (works in web, client and also with mobile devices).

Here are a few advises to those who would need to developer similar functionality.

1. Create a outlook add-in project and define manifest

You would have to create a project with manifest and needed html, css, js elementets. You can easily find information how to do that on MS sites (not going to provide any links as they could change in future). That will allow you to define UI, see my example.


2. Send memo ID to Domino

We need to get information about email from Outlook Add in and send those items to Domino (you would have to build REST API on Domino side that can receive data from Outlook).

Office.context.mailbox.getCallbackTokenAsync(function(result) {

  ...

  var token = result.value;
  var ewsurl = Office.context.mailbox.restUrl;
  var ewsItemId = Office.context.mailbox.item.itemId;
  const itemId = Office.context.mailbox.convertToRestId(ewsItemId,Office.MailboxEnums.RestVersion.v2_0);

  // send token, ewsurl and itemId to Domino endpoint
  ...

}

Having those keys (token url and itemId) you can pull email in Mime format

3. Convert Mime to Notes email

So at this point Domino received data from Add in and can finally do another request to Exchange server (using token, ewsurl and itemId) to read the memo MIME

Dim http As NotesHTTPRequest
Dim enpoint As string
		
Set http = m_app.NotesSession.Createhttprequest()
Call http.Setheaderfield("Authorization", "Bearer " + token)
enpoint = ewsurl + |/v2.0/me/messages/| + itemId + |/$value|
		
getItemMIME = http.get(enpoint)

There is no native Domino LS/Java Mime Parser however I found working example by Stephan: Importing EML files into Notes (lots of them). It worked well, but seems it does not handle inline images (need to do more testing etc).

Alternatively I was told that there is MimeMessageParser.java that writes MIME to a Notes document. This class is part of the XPages Extension Library. So it has sense to compare them.

Monday, November 22, 2021

Alter user during authentication using DSAPI

I had a need to alter user during web-authentication process on fly (skipping password validation). Initially the task looked impossible but I managed to solve it using DSAPI filter.
Though the solution looks quite unsecure it could be very useful in some cases (by high level administrators) who needs to 'signin' as a user in their organization to do some checks.

Here are few most important snippets how to do that:

1. Subscribe for the event kFilterAuthenticate

That means that our dsapi filter only intercepts one specific event: kFilterAuthenticate), as there are other 10-15 other events which we do not wanna touch.

EXPORT unsigned int FilterInit(FilterInitData* filterInitData) {
	STATUS   error = NOERROR;

	filterInitData->appFilterVersion = kInterfaceVersion;
	filterInitData->eventFlags = kFilterAuthenticate;

	// other logic
	// ...
}

2. Catch the authenticate event and process it

Get our event and associate it with a C function

EXPORT unsigned int HttpFilterProc(FilterContext* context, unsigned int eventType, void* eventData) {
	/* Include only those events we want to handle */
	switch (eventType) {
	case kFilterAuthenticate:
		return Authenticate(context, (FilterAuthenticate *) eventData);
	default:
		break;
	}

   return kFilterNotHandled;
}	// end HttpFilterProc

3. Finally set a desired username

Below I only show the key moment - replace user name with another name

unsigned int Authenticate(FilterContext* context, FilterAuthenticate* authData) {
	/* logic that calculate username  */
    // .................................
    // char[] fullName = "CN=T5 Tester5/O=DmytroDev";
    // .................................

	/* Copy the canonical name for this user that dsapi requires.  */
	strncpy ((char *)authData->authName, fullName, authData->authNameSize);
	authData->authNameSize = strlen(alterAuthToken);
	authData->authType = kAuthenticBasic;
	authData->foundInCache = TRUE;

	return kFilterHandledEvent;
}

In order to improve security I have built an application on Domino side that generates tokens which have to be set in cookie and then DSAPI filter reads the cookie and get username from database. Tokens could be generated only by certain people are will be deleted by schedule agents after some time.



On the screenshot below you can see that I signed in as a "T5 Tester5" using my custom token AlterAuthToken while I am Anonymous.



Friday, November 12, 2021

Clear database replication history programatically

Recently I had a need to make a solution that can periodically clean replication history for list of databases.

Native LotusScript/Java classes do not allow that, but there is an C API for that.

Here is a cross platform solution (works for Windows/Linux)

Declare

Public Const W32_LIB = {nnotes.dll}
Public Const LINUX_LIB = {libnotes.so}

Declare Function W32_NSFDbOpen Lib W32_LIB Alias {NSFDbOpen} (ByVal dbName As String, hDb As Long) As Integer
Declare Function W32_NSFDbClose Lib W32_LIB Alias {NSFDbClose} (ByVal hDb As Long) As Integer
Declare Function W32_NSFDbClearReplHistory Lib W32_LIB Alias {NSFDbClearReplHistory} (ByVal hDb As Long, flags As Integer) As Integer

Declare Function LINUX_NSFDbOpen Lib LINUX_LIB Alias {NSFDbOpen} (ByVal dbName As String, hDb As Long) As Integer
Declare Function LINUX_NSFDbClose Lib LINUX_LIB Alias {NSFDbClose} (ByVal hDb As Long) As Integer
Declare Function LINUX_NSFDbClearReplHistory Lib LINUX_LIB Alias {NSFDbClearReplHistory} (ByVal hDb As Long, flags As Integer) As Integer

Using C API functions

// get a handler to database
If IS_WINDOWS Then
	rc = W32_NSFDbOpen(Server & "!!" & FileName, hDb)
Else
	rc = LINUX_NSFDbOpen(Server & "!!" & FileName, hDb)
End If

// clear replication history
If IS_WINDOWS Then
	rc = W32_NSFDbClearReplHistory(hDb, 0)
Else
	rc = LINUX_NSFDbClearReplHistory(hDb, 0)
End If

// close datababase (be sure you always close hDb if you opened it, otherwise memory leak).
If IS_WINDOWS Then
	rc = W32_NSFDbClose(hDb)
Else
	rc = LINUX_NSFDbClose(hDb)
End If

Be sure that you always close hDb handler if you opened the database, otherwise it would lead to memory leak)

See the full solution on GitHub: DominoReplicationHistoryCleaner

Thursday, April 08, 2021

Checking if database is encrypted with LotusScript (C API)

Since it's not possible to identify encryption status and level using native LotusScript/Java classes here is a way to do that. The solution is based on Notes CAPI (within LotusScript) but it works for both Linux/Windows environment.

I will omit NSFDbOpen and NSFDbClose since it's easy to find out and focus instead on the main function: NSFDbLocalSecInfoGetLocal.

Declaration

Const NNOTES ="nnotes.dll"
Const LIBNOTES ="libnotes.so"

Declare Public Function WIN_NSFDbLocalSecInfoGetLocal Lib NNOTES Alias "NSFDbLocalSecInfoGetLocal"(ByVal hDb As Long, state As Long, strength As Long) As Integer
Declare Public Function LIN_NSFDbLocalSecInfoGetLocal Lib LIBNOTES Alias "NSFDbLocalSecInfoGetLocal"(ByVal hDb As Long, state As Long, strength As Long) As integer

Function check encryption status

public Function NSFDbLocalSecInfoGetLocal(hDB As Long, state As Long, strength As long) As Integer
 If isDefined("WINDOWS") Then
  NSFDbLocalSecInfoGetLocal = WIN_NSFDbLocalSecInfoGetLocal(hDb, state, strength)
 ElseIf isDefined("LINUX") Then
  NSFDbLocalSecInfoGetLocal = LIN_NSFDbLocalSecInfoGetLocal(hDb, state, strength)
 End If
End Function

Example how to use it

Private Function calcEncryption(database As NotesDatabase, doc As notesdocument)
 Dim sDb As String
 Dim hDb As Long
 Dim state As Long
 Dim encrypt As Long
 Dim rc As Integer

 sDb = database.server & "!!" & database.filepath

 rc = NSFDbOpen(sDb, hDb)
 If rc <> 0 Then Exit function

 rc = NSFDbLocalSecInfoGetLocal(hDB, state, encrypt)
 If rc <> 0 Then
  Error 9001, "Impossible to read encryption. Error code: " & CStr(rc)
 End If

 rc = NSFDbClose(hDb)
End Function
  • state: 0 (not encrypted), 1 (encrypted) or 2 (will be encrypted after compact)
  • encrypt: 1 (easy), 2 (middle), 3 (strong)