test: migration from Python to JavaScript

This commit is contained in:
Francis Lachapelle
2021-11-09 11:13:23 -05:00
parent 2f739fdc21
commit 5afb659dc0
8 changed files with 372 additions and 76 deletions
+111
View File
@@ -0,0 +1,111 @@
import config from '../lib/config'
class ManageSieve {
constructor(login, authname, password) {
const Telnet = require('telnet-client')
this.login = login
this.authname = authname
this.password = password
this.params = {
host: config.sieve_server,
port: config.sieve_port,
shellPrompt: /\bOK "/,
timeout: 1500
}
this.connection = new Telnet()
this.sasl = []
this.ready = false
}
async connect() {
let response, parsedResponse
if (!this.ready) {
await this.connection.connect(this.params)
response = await this.connection.send('CAPABILITY', { waitfor: /\b(OK|NO) "/ })
// console.debug(`ManageSieve.connect => ${response}`)
parsedResponse = this.parseResponse(response)
if (!parsedResponse['OK']) {
throw new Error(`Connection failed: ${parsedResponse['NO']}`)
}
this.ready = true
if (parsedResponse['SASL']) {
this.sasl = parsedResponse['SASL'].split(/ /)
}
}
}
async authenticate() {
let buff, base64, response, parsedResponse
await this.connect()
buff = Buffer.from(`${this.login}\0${this.authname}\0${this.password}`)
base64 = buff.toString('base64')
response = await this.connection.send(`AUTHENTICATE "PLAIN" {${base64.length}+}\n${base64}`, { waitfor: /\b(OK|NO) "/ })
// console.debug(`ManageSieve.authenticate => ${response}`)
parsedResponse = this.parseResponse(response)
if (!parsedResponse['OK']) {
throw new Error(`Authentication failed: ${parsedResponse['NO']}`)
}
}
async listScripts() {
let response, parsedResponse
await this.connect()
response = await this.connection.send(`LISTSCRIPTS`, { waitfor: /\b(OK|NO) "/ })
parsedResponse = this.parseResponse(response)
// console.debug(`ManageSieve.listScripts => ${JSON.stringify(parsedResponse, undefined, 2)}`)
if (!parsedResponse['OK']) {
throw new Error(`List scripts failed: ${parsedResponse['NO']}`)
}
return parsedResponse
}
async getScript(scriptname) {
let response, parsedResponse, script = null
await this.connect()
response = await this.connection.send(`GETSCRIPT "${scriptname}"`, { waitfor: /\b(OK|NO) "/ })
// console.debug(`ManageSieve.getScript(${scriptname}) => |${response}|`)
const lengthMatch = response.match(/{([0-9]+)}\r?\n/)
if (lengthMatch) {
const scriptLength = lengthMatch[1]
script = response.substr(lengthMatch[0].length, scriptLength)
}
else
throw new Error(`Can't find length of Sieve script`)
return script
}
parseResponse(str) {
const re = new RegExp(/[^\s"]+|"([^"]*)"/gi)
let parsed = {}
for (let line of (str.split(/\r?\n/))) {
if (line.length) {
let rematch, key, value, i = 0
while ((rematch = re.exec(line))) {
value = rematch[1] ? rematch[1] : rematch[0]
if (key && i > 0)
parsed[key] = value
else
key = value
i++
}
if (key && i == 1) {
parsed[key] = null
}
}
}
// console.debug(`ManageSieve.parseResponse => ${JSON.stringify(parsed, undefined, 2)}`)
return parsed
}
}
export default ManageSieve
+18 -16
View File
@@ -28,17 +28,21 @@ class Preferences {
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({userName: this.username, password: this.password})
})
const values = response.headers.get('set-cookie').split(/, /)
let authCookies = []
for (let v of values) {
let c = cookie.parse(v)
for (let authCookie of ['0xHIGHFLYxSOGo', 'XSRF-TOKEN']) {
if (Object.keys(c).includes('0xHIGHFLYxSOGo')) {
authCookies.push(cookie.serialize(authCookie, c[authCookie]))
if (response.status == 200) {
const values = response.headers.get('set-cookie').split(/, /)
let authCookies = []
for (let v of values) {
let c = cookie.parse(v)
for (let authCookie of ['0xHIGHFLYxSOGo', 'XSRF-TOKEN']) {
if (Object.keys(c).includes('0xHIGHFLYxSOGo')) {
authCookies.push(cookie.serialize(authCookie, c[authCookie]))
}
}
}
this.cookie = authCookies.join('; ')
}
this.cookie = authCookies.join('; ')
else
throw new Error(`Can't authenticate with username ${this.username} (HTTP status code ${response.status})`)
}
return this.cookie
}
@@ -137,15 +141,13 @@ class Preferences {
if (!this.preferences)
await this.loadPreferences()
let obj = this.findKey(this.preferences, preference)
if (obj == null) {
obj = this.preferences
for (let path of paths) {
if (typeof obj[path] == 'undefined')
obj[path] = {}
obj = obj[path]
}
let obj = this.preferences
for (let path of paths) {
if (typeof obj[path] == 'undefined')
obj[path] = {}
obj = obj[path]
}
obj[preference] = value
}
+45 -41
View File
@@ -5,9 +5,10 @@ import {
davRequest,
deleteObject,
formatProps,
getBasicAuthHeaders,
getDAVAttribute,
propfind,
syncCollection,
calendarMultiGet,
createCalendarObject,
@@ -15,8 +16,6 @@ import {
createVCard
} from 'tsdav'
import { formatProps, getDAVAttribute } from 'tsdav/dist/util/requestHelpers';
import { makeCollection } from 'tsdav/dist/collection';
import convert from 'xml-js'
import { fetch } from 'cross-fetch'
import config from './config'
@@ -132,10 +131,14 @@ class WebDAV {
}
makeCollection(resource) {
return makeCollection({
return davRequest({
url: this.serverUrl + resource,
headers: this.headers
});
init: {
method: 'MKCOL',
headers: this.headers,
namespace: DAVNamespaceShorthandMap[DAVNamespace.DAV]
}
})
}
propfindWebdav(resource, properties, namespace = DAVNamespace.DAV, headers = {}) {
@@ -325,6 +328,42 @@ class WebDAV {
})
}
// https://datatracker.ietf.org/doc/html/rfc6578#section-3.2
syncQuery(resource, token = '', properties) {
const formattedProperties = properties.map((p) => {
return { [`${DAVNamespaceShorthandMap[DAVNamespace.DAV]}:${p}`]: '' }
});
let xmlBody = convert.js2xml(
{
'sync-collection': {
_attributes: getDAVAttribute([DAVNamespace.DAV]),
'sync-level': 1,
'sync-token': token,
prop: formattedProperties
}
},
{
compact: true,
spaces: 2,
elementNameFn: (name) => {
// add namespace to all keys without namespace
if (!/^.+:.+/.test(name)) {
return `${DAVNamespaceShorthandMap[DAVNamespace.DAV]}:${name}`
}
return name
}
}
)
return fetch(this.serverUrl + resource, {
headers: {
'Content-Type': 'application/xml; charset="utf-8"',
...this.headers
},
method: 'REPORT',
body: xmlBody
})
}
// CalDAV operations
makeCalendar(resource) {
@@ -426,41 +465,6 @@ class WebDAV {
})
}
syncQuery(resource, token = '', properties) {
const formattedProperties = properties.map((p) => {
return { [`${DAVNamespaceShorthandMap[DAVNamespace.DAV]}:${p}`]: '' }
});
let xmlBody = convert.js2xml(
{
'sync-collection': {
_attributes: getDAVAttribute([DAVNamespace.DAV]),
'sync-level': 1,
'sync-token': token,
prop: formattedProperties
}
},
{
compact: true,
spaces: 2,
elementNameFn: (name) => {
// add namespace to all keys without namespace
if (!/^.+:.+/.test(name)) {
return `${DAVNamespaceShorthandMap[DAVNamespace.DAV]}:${name}`
}
return name
}
}
)
return fetch(this.serverUrl + resource, {
headers: {
'Content-Type': 'application/xml; charset="utf-8"',
...this.headers
},
method: 'REPORT',
body: xmlBody
})
}
propfindCaldav(resource, properties, depth = 0) {
return this.propfindWebdav(resource, properties, DAVNamespace.CALDAV, { depth: new String(depth) })
}
+4 -3
View File
@@ -6,14 +6,15 @@
"test": "jasmine"
},
"dependencies": {
"@babel/core": "^7.15.8",
"@babel/preset-env": "^7.15.8",
"@babel/core": "^7.16.0",
"@babel/preset-env": "^7.16.0",
"babel-cli": "^6.26.0",
"cookie": "^0.4.1",
"cross-fetch": "^3.1.4",
"esm": "^3.2.25",
"ical.js": "^1.4.0",
"jasmine": "^3.10.0",
"tsdav": "^1.1.1"
"telnet-client": "^1.4.10",
"tsdav": "^1.1.5"
}
}
+10 -8
View File
@@ -82,14 +82,6 @@ describe('PreventInvitations', function() {
beforeAll(async function() {
prefs = new Preferences(config.attendee1_username, config.attendee1_password)
const calendarPrefs = prefs.get('Calendar')
if (!calendarPrefs.PreventInvitationsWhitelist)
calendarPrefs.PreventInvitationsWhitelist = {}
await prefs.set('PreventInvitationsWhitelist', {})
if (!calendarPrefs.PreventInvitations)
calendarPrefs.PreventInvitations = 0
await prefs.set('PreventInvitations', 0)
webdav = new WebDAV(config.username, config.password)
webdav_su = new WebDAV(config.superuser, config.superuser_password)
webdavAttendee1 = new WebDAV(config.attendee1, config.attendee1_password)
@@ -114,6 +106,16 @@ describe('PreventInvitations', function() {
icsList = []
})
beforeEach(async function() {
const calendarPrefs = prefs.get('Calendar')
if (!calendarPrefs.PreventInvitationsWhitelist)
calendarPrefs.PreventInvitationsWhitelist = {}
await prefs.set('PreventInvitationsWhitelist', {})
if (!calendarPrefs.PreventInvitations)
calendarPrefs.PreventInvitations = 0
await prefs.set('PreventInvitations', 0)
})
afterAll(async function() {
await prefs.set('PreventInvitationsWhitelist', {})
await prefs.set('PreventInvitations', 0)
+3 -2
View File
@@ -5,9 +5,10 @@ import TestUtility from '../lib/utilities'
import {
DAVNamespace,
DAVNamespaceShorthandMap,
davRequest
davRequest,
formatProps,
getDAVAttribute
} from 'tsdav'
import { formatProps, getDAVAttribute } from 'tsdav/dist/util/requestHelpers';
const cards = {
'new.vcf': `BEGIN:VCARD
+165
View File
@@ -0,0 +1,165 @@
import config from '../lib/config'
import WebDAV from '../lib/WebDAV'
import TestUtility from '../lib/utilities'
import Preferences from '../lib/Preferences'
import ManageSieve from '../lib/ManageSieve'
let prefs, webdav, utility, manageSieve, user
describe('Sieve', function() {
async function _getSogoSieveScript() {
const scripts = await manageSieve.listScripts()
expect(Object.keys(scripts))
.withContext(`sogo sieve script has been created`)
.toContain('sogo')
expect(scripts['sogo'])
.withContext(`sogo sieve script is active`)
.toMatch(/ACTIVE/i)
const script = await manageSieve.getScript('sogo')
return script
}
async function _killFilters() {
// kill existing filters
await prefs.setOrCreate('SOGoSieveFilters', [], ['defaults'])
// vacation filters
await prefs.setOrCreate('autoReplyText', '', ['defaults', 'Vacation'])
await prefs.setOrCreate('customSubjectEnabled', 0, ['defaults', 'Vacation'])
await prefs.setOrCreate('customSubject', '', ['defaults', 'Vacation'])
await prefs.setOrCreate('autoReplyEmailAddresses', [], ['defaults', 'Vacation'])
await prefs.setOrCreate('daysBetweenResponse', 7, ['defaults', 'Vacation'])
await prefs.setOrCreate('ignoreLists', 0, ['defaults', 'Vacation'])
await prefs.setOrCreate('startDate', 0, ['defaults', 'Vacation'])
await prefs.setOrCreate('endDate', 0, ['defaults', 'Vacation'])
await prefs.setOrCreate('enabled', 0, ['defaults', 'Vacation'])
// forwarding filters
await prefs.setOrCreate('forwardAddress', [], ['defaults', 'Forward'])
await prefs.setOrCreate('keepCopy', 0, ['defaults', 'Forward'])
}
beforeAll(async function() {
prefs = new Preferences(config.username, config.password)
webdav = new WebDAV(config.username, config.password)
utility = new TestUtility(webdav)
manageSieve = new ManageSieve(config.username, config.username, config.password)
user = await utility.fetchUserInfo(config.username)
await manageSieve.authenticate()
})
beforeEach(async function() {
await _killFilters()
})
afterAll(async function() {
await _killFilters()
await prefs.save()
})
it('enable simple vacation script', async function() {
const vacationMsg = 'vacation test'
const daysInterval = 5
const mailaddr = user.email
const sieveSimpleVacation = `require ["vacation"];\r\nvacation :days ${daysInterval} :addresses ["${mailaddr}"] text:\r\n${vacationMsg}\r\n.\r\n;\r\n`
let vacation
vacation = await prefs.get('Vacation')
vacation.enabled = 1
await prefs.setNoSave('autoReplyText', vacationMsg)
await prefs.setNoSave('daysBetweenResponse', daysInterval)
await prefs.setNoSave('autoReplyEmailAddresses', [user.email])
await prefs.save()
const createdScript = await _getSogoSieveScript()
expect(createdScript)
.withContext(`sogo Sieve script`)
.toBe(sieveSimpleVacation)
})
it('enable vacation script - ignore lists', async function() {
const vacationMsg = 'vacation test - ignore list'
const daysInterval = 3
const mailaddr = user.email
const sieveVacationIgnoreLists = `require ["vacation"];\r\nif allof ( not exists ["list-help", "list-unsubscribe", "list-subscribe", "list-owner", "list-post", "list-archive", "list-id", "Mailing-List"], not header :comparator "i;ascii-casemap" :is "Precedence" ["list", "bulk", "junk"], not header :comparator "i;ascii-casemap" :matches "To" "Multiple recipients of*" ) { vacation :days ${daysInterval} :addresses ["${mailaddr}"] text:\r\n${vacationMsg}\r\n.\r\n;\r\n}\r\n`
let vacation
vacation = await prefs.get('Vacation')
vacation.enabled = 1
await prefs.setNoSave('autoReplyText', vacationMsg)
await prefs.setNoSave('daysBetweenResponse', daysInterval)
await prefs.setNoSave('autoReplyEmailAddresses', [user.email])
await prefs.setNoSave('ignoreLists', 1)
await prefs.save()
const createdScript = await _getSogoSieveScript()
expect(createdScript)
.withContext(`sogo Sieve script`)
.toBe(sieveVacationIgnoreLists)
})
it('enable simple forwarding', async function() {
const redirectMailaddr = 'nonexistent@inverse.com'
const sieveSimpleForward = `redirect "${redirectMailaddr}";\r\n`
let forward
// Enabling Forward now is an 'enabled' setting in the subdict Forward
// We need to get that subdict first -- next save/set will also save this
forward = await prefs.get('Forward')
forward.enabled = 1
await prefs.set('forwardAddress', [redirectMailaddr])
const createdScript = await _getSogoSieveScript()
expect(createdScript)
.withContext(`sogo Sieve script`)
.toBe(sieveSimpleForward)
})
it('enable email forwarding - keep a copy', async function() {
const redirectMailaddr = 'nonexistent@inverse.com'
const sieveForwardKeep = `redirect "${redirectMailaddr}";\r\nkeep;\r\n`
let forward
// Enabling Forward now is an 'enabled' setting in the subdict Forward
// We need to get that subdict first -- next save/set will also save this
forward = await prefs.get('Forward')
forward.enabled = 1
await prefs.setNoSave('forwardAddress', [redirectMailaddr])
await prefs.setNoSave('keepCopy', 1)
await prefs.save()
const createdScript = await _getSogoSieveScript()
expect(createdScript)
.withContext(`sogo Sieve script`)
.toBe(sieveForwardKeep)
})
it('add simple sieve filter', async function() {
const folderName = 'Sent'
const subject = 'add simple sieve filter'
const sieveFilter = `require ["fileinto"];\r\nif anyof (header :contains "subject" "${subject}") {\r\n fileinto "${folderName}";\r\n}\r\n`
await prefs.set('SOGoSieveFilters', [{
active: true,
actions: [{
method: 'fileinto',
argument: 'Sent'
}],
rules: [{
operator: 'contains',
field: 'subject',
value: subject
}],
match: 'any',
name: folderName
}])
const createdScript = await _getSogoSieveScript()
expect(createdScript)
.withContext(`sogo Sieve script`)
.toBe(sieveFilter)
})
})
+16 -6
View File
@@ -12,7 +12,7 @@ describe('webdav sync', function() {
await webdav_su.deleteObject(resource)
})
it("webdav sync", async function() {
it('webdav sync', async function() {
const nsShort = DAVNamespaceShorthandMap[DAVNamespace.DAV].toUpperCase()
let response, xml, token
@@ -23,7 +23,9 @@ describe('webdav sync', function() {
response = await webdav.makeCalendar(resource)
expect(response.length).toBe(1)
expect(response[0].status).toBe(201)
expect(response[0].status)
.withContext(`HTTP status code when creating a Calendar`)
.toBe(201)
// test queries:
// empty collection:
@@ -36,15 +38,23 @@ describe('webdav sync', function() {
response = await webdav.syncQuery(resource, null, [ 'getetag' ])
xml = await response.text();
({ [`${nsShort}:multistatus`]: { [`${nsShort}:sync-token`]: { _text: token } } } = convert.xml2js(xml, {compact: true, nativeType: true}))
expect(response.status).toBe(207)
expect(token).toBeGreaterThanOrEqual(0)
expect(response.status)
.withContext(`HTTP status code when performing sync-query without a token`)
.toBe(207)
expect(token)
.withContext(`Sync query returns valid token`)
.toBeGreaterThanOrEqual(0)
// we make sure that any token is accepted when the collection is
// empty, but that the returned token differs
response = await webdav.syncQuery(resource, '1234', [ 'getetag' ])
xml = await response.text();
({ [`${nsShort}:multistatus`]: { [`${nsShort}:sync-token`]: { _text: token } } } = convert.xml2js(xml, {compact: true, nativeType: true}))
expect(response.status).toBe(207)
expect(token).toBeGreaterThanOrEqual(0)
expect(response.status)
.withContext(`HTTP status code when performing sync-query with a token`)
.toBe(207)
expect(token)
.withContext(`Sync query returns valid token`)
.toBeGreaterThanOrEqual(0)
})
})