Option Explicit Private Declare PtrSafe Function CoCreateGuid Lib "ole32" (ByRef GUID As Byte) As Long Private has_more As Boolean Public Sub SendToJoplin() Dim sToken As String Dim sUrl As String Dim sMailFolderName As String Dim sNotesFolderName As String sToken = "REPLACE ME WITH YOUR TOKEN" sUrl = "http://127.0.0.1:41184" sMailFolderName = "Outlook Mail" sNotesFolderName = "Outlook Notes" Dim sMailFolderID As String Dim sNotesFolderID As String Dim oNoteIDs Set oNoteIDs = CreateObject("Scripting.Dictionary") sMailFolderID = "" sNotesFolderID = "" Dim nExport As Integer Dim nError As Integer nExport = 0 nError = 0 Dim oItem As Object ' Outlook.MailItem or Outlook.PostItem or Outlook.DocumentItem or Outlook.NoteItem For Each oItem In Application.ActiveExplorer.Selection Dim sJSONString As String Dim sItemID As String If TypeOf oItem Is Outlook.MailItem Or TypeOf oItem Is Outlook.PostItem Or TypeOf oItem Is Outlook.DocumentItem Then If sMailFolderID = "" Then sMailFolderID = CreateJoplinItem("folder", sMailFolderName, sUrl, sToken) If sMailFolderID = "" Then Return End If Dim sAttachmentInfo As String sAttachmentInfo = ImportAttachments(oItem, sUrl, sToken) sJSONString = HttpRequest(sUrl & "/notes?token=" & sToken, "POST", "{ " _ & """is_todo"": 0, ""title"": """ & EscapeBody(oItem.ConversationTopic) & """" _ & ", ""parent_id"": """ & sMailFolderID & """" _ & ", ""user_created_time"": """ & ToUnixTime(oItem.CreationTime) & """" _ & ", ""user_updated_time"": """ & ToUnixTime(UpdateTime(oItem)) & """" _ & ", """ & IIf(IsHtml(oItem), "body_html", "body") & """: """ & EscapeBody(MakeBody(oItem, sAttachmentInfo)) & """" _ & " }") sItemID = ParseJsonResponse(sJSONString, "id", "AddNote") ElseIf TypeOf oItem Is Outlook.NoteItem Then If sNotesFolderID = "" Then sNotesFolderID = CreateJoplinItem("folder", sNotesFolderName, sUrl, sToken) If sNotesFolderID = "" Then Return End If sJSONString = HttpRequest(sUrl & "/notes?token=" & sToken, "POST", "{ " _ & """is_todo"": 0, ""title"": """ & EscapeBody(oItem.Subject) & """" _ & ", ""parent_id"": """ & sNotesFolderID & """" _ & ", ""user_created_time"": """ & ToUnixTime(oItem.CreationTime) & """" _ & ", ""user_updated_time"": """ & ToUnixTime(oItem.LastModificationTime) & """" _ & ", ""body"": """ & EscapeBody(oItem.Body) & """" _ & " }") sItemID = ParseJsonResponse(sJSONString, "id", "AddNote") Else MsgBox "Outlook " & TypeName(oItem) & " is not supported: " & oItem.Subject sItemID = "" End If If sItemID <> "" Then nExport = nExport + 1 Debug.Print nExport & " " & EscapeBody(oItem.Subject) Else nError = nError + 1 End If If oItem.Categories <> "" And sItemID <> "" Then Dim aCategories() As String aCategories = Split(oItem.Categories, ", ") Dim vCategory As Variant For Each vCategory In aCategories Dim sCategory As String Dim sTagID As String sCategory = vCategory If oNoteIDs.Exists(sCategory) Then sTagID = oNoteIDs.item(sCategory) Else sTagID = CreateJoplinItem("tag", sCategory, sUrl, sToken) oNoteIDs.Add sCategory, sTagID End If If sTagID = "" Then nError = nError + 1 Else sJSONString = HttpRequest(sUrl & "/tags/" & sTagID & "/notes?token=" & sToken, "POST", "{ ""id"": """ & sItemID & """ }") Dim sTaggedID As String sTaggedID = ParseJsonResponse(sJSONString, "id", "AddNote") If sTaggedID = "" Then nError = nError + 1 End If End If Next End If Next Dim sMsg As String sMsg = nExport & " notes exported to Joplin folder " If sMailFolderID = "" Then sMsg = sMsg & """" & sNotesFolderName & """" ElseIf sNotesFolderID = "" Then sMsg = sMsg & """" & sMailFolderName & """" Else sMsg = sMsg & """" & sNotesFolderName & """ and """ & sMailFolderName & """" End If If nError = 0 Then MsgBox sMsg Else MsgBox nError & " errors encountered. " & sMsg End If End Sub Private Function UpdateTime(oItem As Object) As Date If TypeOf oItem Is Outlook.DocumentItem Then UpdateTime = oItem.LastModificationTime Else UpdateTime = oItem.ReceivedTime End If End Function Private Function ImportAttachments(oItem As Object, sUrl As String, sToken As String) As String Dim oAttachment As Object Dim sTemp As String Dim sJSONString As String Dim sFileID As String ImportAttachments = "" sTemp = Environ("TEMP") For Each oAttachment In oItem.Attachments Dim sSaveFile As String sSaveFile = sTemp & "\" & NewGuid() If InStrRev(oAttachment.FileName, ".") > 0 Then sSaveFile = sSaveFile & Mid(oAttachment.FileName, InStrRev(oAttachment.FileName, ".")) oAttachment.SaveAsFile sSaveFile sJSONString = HttpUpload(sUrl & "/resources?token=" & sToken, sSaveFile, "{ ""title"":""" & oAttachment.DisplayName & """ }") Kill sSaveFile sFileID = ParseJsonResponse(sJSONString, "id", "ImportAttachments") If ImportAttachments <> "" Then ImportAttachments = ImportAttachments & ", " If IsHtml(oItem) Then ImportAttachments = ImportAttachments & "" & oAttachment.FileName & "" Else ImportAttachments = ImportAttachments & "[" & oAttachment.FileName & "](:/" & sFileID & ")" End If Next End Function Private Function IsHtml(oItem As Object) As Boolean If TypeOf oItem Is Outlook.DocumentItem Then IsHtml = False Else IsHtml = (oItem.BodyFormat = olFormatHTML) End If End Function Private Function MakeBody(oItem As Object, sAttachmentInfo As String) As String Dim sFrom As String Dim sNl As String If Not (TypeOf oItem Is Outlook.DocumentItem) Then sFrom = oItem.SenderEmailAddress If oItem.SenderName <> "" Then If sFrom <> "" And oItem.SenderEmailType = "SMTP" Then sFrom = oItem.SenderName & " <" & sFrom & ">" Else sFrom = oItem.SenderName End If End If End If If IsHtml(oItem) Then MakeBody = oItem.HTMLBody sFrom = EscapeHtml(sFrom) sNl = "
" & vbLf Else MakeBody = oItem.Body sNl = vbLf End If If sAttachmentInfo <> "" Then MakeBody = "Attachments: " & sAttachmentInfo & sNl & sNl & MakeBody If TypeOf oItem Is Outlook.MailItem Then If oItem.To <> "" Then If sAttachmentInfo = "" Then MakeBody = sNl & MakeBody MakeBody = "To: " & oItem.To & sNl & MakeBody If sFrom <> "" Then MakeBody = "From: " & sFrom & sNl & MakeBody End If End If End Function Private Function EscapeBody(sText As String) As String EscapeBody = sText EscapeBody = Replace(EscapeBody, "\", "\\") 'Backslash is replaced with \\ EscapeBody = Replace(EscapeBody, Chr(34), "\" & Chr(34)) 'Double quote is replaced with \" EscapeBody = Replace(EscapeBody, vbCr + vbLf, "\n") 'Carriage return + Newline is replaced with \n EscapeBody = Replace(EscapeBody, vbCr, "\r") 'Carriage return is replaced with \r EscapeBody = Replace(EscapeBody, vbLf, "\n") 'Newline is replaced with \n EscapeBody = Replace(EscapeBody, Chr(8), "\b") 'Backspace is replaced with \b EscapeBody = Replace(EscapeBody, Chr(12), "\f") 'Form feed is replaced with \f EscapeBody = Replace(EscapeBody, vbTab, "\t") 'Tab is replaced with \t End Function Private Function EscapeHtml(sText As String) As String EscapeHtml = sText EscapeHtml = Replace(EscapeHtml, "&", "&") EscapeHtml = Replace(EscapeHtml, "<", "<") EscapeHtml = Replace(EscapeHtml, ">", ">") End Function Private Function FindJoplinItem(sType As String, sItemName As String, sUrl As String, sToken As String) As String Dim page As Integer page = 1 Do Dim sJSONString As String Dim aItems As Variant sJSONString = HttpRequest(sUrl & "/search?query=" & sItemName & "&type=" & sType & "&page=" & page & "&token=" & sToken) page = page + 1 aItems = ParseJsonResponse(sJSONString, "items", "FindJoplinItem") If IsArray(aItems) Then Dim jItem As Variant For Each jItem In aItems If VarType(jItem) = vbObject Then If jItem.Exists("id") And jItem.Exists("title") And jItem.Exists("parent_id") Then ' Debug.Print jItem.Item("id") & " " & jItem.item("title") If jItem.item("parent_id") = "" And LCase(jItem.item("title")) = LCase(sItemName) Then FindJoplinItem = jItem.item("id") Exit Function End If End If End If Next End If Loop While has_more FindJoplinItem = "" End Function Private Function CreateJoplinItem(sType As String, sItemName As String, sUrl As String, sToken As String) As String CreateJoplinItem = FindJoplinItem(sType, sItemName, sUrl, sToken) If CreateJoplinItem <> "" Then Exit Function Dim sJSONString As String sJSONString = HttpRequest(sUrl & "/" & sType & "s?token=" & sToken, "POST", "{ ""title"": """ & EscapeBody(sItemName) & """ }") CreateJoplinItem = ParseJsonResponse(sJSONString, "id", "CreateJoplinItem") End Function Private Function ParseJsonResponse(sJSONString As String, sItem As String, sOp As String) Dim vJSON As Variant Dim sState As String has_more = False JSON.Parse sJSONString, vJSON, sState ParseJsonResponse = "" If sState <> "Object" Then MsgBox sOp & ": invalid response from Joplin server: " & sJSONString ElseIf vJSON.Exists("error") Then MsgBox sOp & " error: " & vJSON.item("error") ElseIf Not vJSON.Exists(sItem) Then MsgBox sOp & ": no item """ & sItem & """ in response from Joplin server: " & sJSONString Else ParseJsonResponse = vJSON.item(sItem) End If If vJSON.Exists("has_more") Then has_more = vJSON.item("has_more") End If End Function Private Function HttpRequest(sUrl As String, Optional sMethod As String = "GET", Optional sPost As String = "") As String Dim sResponse As String With CreateObject("Msxml2.ServerXMLHTTP") .Open sMethod, sUrl, False .setRequestHeader "Cache-Control", "no-cache" .setRequestHeader "Pragma", "no-cache" .Send sPost Do Until .ReadyState = 4: DoEvents: Loop sResponse = .ResponseText End With ' Debug.Print sResponse & " <- " & sMethod & " " & sURL & " " & sPost HttpRequest = sResponse End Function Private Function HttpUpload(sUrl As String, sFileName As String, sPost As String) As String ' upload file based on XMLHTTP example from https://wqweto.wordpress.com/2011/07/12/vb6-using-wininet-to-post-binary-file/ Dim STR_BOUNDARY As String STR_BOUNDARY = "SendToJoplin-" & NewGuid() Dim fileNo As Integer Dim baFileData() As Byte Dim sResponse As String ' read file fileNo = FreeFile Open sFileName For Binary Access Read As fileNo If LOF(fileNo) > 0 Then ReDim baFileData(0 To LOF(fileNo) - 1) As Byte Get fileNo, , baFileData End If Close fileNo ' upload file With CreateObject("Msxml2.ServerXMLHTTP") .Open "POST", sUrl, False .setRequestHeader "Cache-Control", "no-cache" .setRequestHeader "Pragma", "no-cache" .setRequestHeader "Content-Type", "multipart/form-data; boundary=" & STR_BOUNDARY .Send CombineArrays( _ pvToByteArray( _ "--" & STR_BOUNDARY & vbCrLf & _ "Content-Disposition: form-data; name=""props""" & vbCrLf & vbCrLf & _ sPost & vbCrLf & _ "--" & STR_BOUNDARY & vbCrLf & _ "Content-Disposition: form-data; name=""data""; filename=""" & Mid(sFileName, InStrRev(sFileName, "\") + 1) & """" & vbCrLf & _ "Content-Type: application/octet-stream" & vbCrLf & vbCrLf _ ), _ baFileData, _ pvToByteArray(vbCrLf & "--" & STR_BOUNDARY & "--") _ ) Do Until .ReadyState = 4: DoEvents: Loop sResponse = .ResponseText End With ' Debug.Print sResponse HttpUpload = sResponse End Function Public Function CombineArrays(ParamArray arraysToMerge() As Variant) As Byte() ' Adapted from https://stackoverflow.com/a/51407942/6199960 Dim CombinedArrayLength As Long Dim i As Long, j As Long CombinedArrayLength = 0 For i = LBound(arraysToMerge) To UBound(arraysToMerge) CombinedArrayLength = CombinedArrayLength + (UBound(arraysToMerge(i)) - LBound(arraysToMerge(i)) + 1) Next i Dim combinedArray() As Byte ReDim combinedArray(0 To CombinedArrayLength - 1) Dim combinedArrayIndex As Long combinedArrayIndex = LBound(combinedArray) For i = LBound(arraysToMerge) To UBound(arraysToMerge) For j = LBound(arraysToMerge(i)) To UBound(arraysToMerge(i)) combinedArray(combinedArrayIndex) = arraysToMerge(i)(j) combinedArrayIndex = combinedArrayIndex + 1 Next j Next i ' Debug.Print StrConv(combinedArray, vbUnicode) CombineArrays = combinedArray End Function Private Function pvToByteArray(sText As String) As Byte() pvToByteArray = StrConv(sText, vbFromUnicode) End Function Private Function NewGuid() As String ' based on https://stackoverflow.com/a/23126614/6199960 Dim ID(0 To 15) As Byte Dim N As Integer Dim GUID As String Dim Res As Long Res = CoCreateGuid(ID(0)) For N = 0 To 15 GUID = GUID & Right("0" & Hex(ID(N)), 2) If N = 3 Or N = 5 Or N = 7 Or N = 9 Then GUID = GUID & "-" Next N NewGuid = GUID End Function Private Function ToUnixTime(ByVal dt As Date) As LongLong ' ToUnixTime convert Date value in the local timezone to Unix timestamp in milliseconds, UTC ' Based on example from https://gist.github.com/seakintruth/ddcc3d5e400a5083458494ae30d55466 Dim objDateTime Set objDateTime = CreateObject("WbemScripting.SWbemDateTime") objDateTime.SetVarDate dt ToUnixTime = DateDiff("s", "01/01/1970 00:00:00", CDate(objDateTime.GetVarDate(False))) * 1000 + Fix((dt - Fix(dt)) * 1000) ' Debug.Print dt & Format(dt - Fix(dt), ".000") & " -> " & ToUnixTime End Function