add user creation and mail body APIs

This commit is contained in:
2026-04-02 21:27:16 +08:00
parent 91609d15aa
commit df9dd80bb3
3 changed files with 159 additions and 2 deletions

149
server.js
View File

@@ -13,6 +13,7 @@ const {
CLIENT_ID,
CLIENT_SECRET,
MAILBOX_ADDRESS = '',
DEFAULT_DOMAIN = '',
ACCESS_PASSWORD = '',
PORT = '3000',
} = process.env;
@@ -46,6 +47,7 @@ const tokenCache = {
accessToken: null,
expiresAt: 0,
};
let cachedCreateUserDomain = DEFAULT_DOMAIN || '';
function timingSafeStringEqual(left, right) {
const leftBuffer = Buffer.from(left);
@@ -154,7 +156,7 @@ function parseResponseBody(text) {
}
}
async function curlJsonRequest(url, { method = 'GET', headers = {}, form = null } = {}) {
async function curlJsonRequest(url, { method = 'GET', headers = {}, form = null, json = null, body = null } = {}) {
const args = [
'-sS',
'-L',
@@ -180,6 +182,13 @@ async function curlJsonRequest(url, { method = 'GET', headers = {}, form = null
});
}
if (json !== null && json !== undefined) {
args.push('-H', 'Content-Type: application/json');
args.push('--data', JSON.stringify(json));
} else if (body !== null && body !== undefined) {
args.push('--data', body);
}
args.push(url);
try {
@@ -247,6 +256,8 @@ async function graphRequest(pathname, query, init = {}) {
Authorization: `Bearer ${accessToken}`,
...(init.headers || {}),
},
json: init.json ?? null,
body: init.body ?? null,
});
if (response.status < 200 || response.status >= 300) {
@@ -259,6 +270,41 @@ async function graphRequest(pathname, query, init = {}) {
return response.body;
}
function buildRandomSuffix() {
return crypto.randomBytes(4).toString('hex');
}
function buildTemporaryPassword() {
return `${crypto.randomBytes(12).toString('base64url')}Aa1!`;
}
function extractDomainFromMailbox() {
if (DEFAULT_DOMAIN) {
return DEFAULT_DOMAIN;
}
const atIndex = MAILBOX_ADDRESS.indexOf('@');
if (atIndex !== -1) {
return MAILBOX_ADDRESS.slice(atIndex + 1);
}
return '';
}
function generateRandomUserPayload() {
const suffix = buildRandomSuffix();
const mailNickname = `office365-${suffix}`;
const domain = extractDomainFromMailbox();
const userPrincipalName = domain ? `${mailNickname}@${domain}` : '';
return {
displayName: `Office365 User ${suffix}`,
mailNickname,
userPrincipalName,
password: buildTemporaryPassword(),
};
}
async function listUsers() {
const users = [];
let nextUrl = buildGraphUrl('/users', {
@@ -358,6 +404,64 @@ async function getLatestEmail(userId) {
};
}
async function resolveDomainForUserCreation() {
if (cachedCreateUserDomain) {
return cachedCreateUserDomain;
}
cachedCreateUserDomain = extractDomainFromMailbox();
return cachedCreateUserDomain;
}
async function createRandomUser() {
const domain = await resolveDomainForUserCreation();
if (!domain) {
const error = new Error('Missing DEFAULT_DOMAIN or MAILBOX_ADDRESS domain for user creation.');
error.status = 400;
error.details = { code: 'missing_domain' };
throw error;
}
let lastError = null;
for (let attempt = 0; attempt < 3; attempt += 1) {
const payload = generateRandomUserPayload();
payload.userPrincipalName = `${payload.mailNickname}@${domain}`;
try {
const user = await graphRequest('/users', undefined, {
method: 'POST',
json: {
accountEnabled: true,
displayName: payload.displayName,
mailNickname: payload.mailNickname,
userPrincipalName: payload.userPrincipalName,
passwordProfile: {
forceChangePasswordNextSignIn: true,
password: payload.password,
},
},
});
return {
id: user.id,
displayName: user.displayName || payload.displayName,
userPrincipalName: user.userPrincipalName || payload.userPrincipalName,
mailNickname: payload.mailNickname,
temporaryPassword: payload.password,
};
} catch (error) {
lastError = error;
if (error.status !== 409 && error.status !== 400) {
throw error;
}
}
}
throw lastError || new Error('Failed to create random user.');
}
function sendGraphError(response, error) {
const graphMessage = error.details?.error?.message;
const graphCode = error.details?.error?.code;
@@ -452,6 +556,49 @@ app.get('/api/users/:userId/latest-email', async (request, response) => {
}
});
app.get('/api/users/:userId/latest-email/body', async (request, response) => {
try {
const user = await getUserById(request.params.userId);
if (!user.hasMailboxAddress) {
response.status(400).json({
error: 'This account does not expose a mailbox address in Microsoft Graph.',
code: 'mailbox_not_available',
});
return;
}
const message = await getLatestEmail(user.id);
if (!message) {
response.json({ user, message: null });
return;
}
response.json({
user,
message: {
id: message.id,
subject: message.subject,
receivedDateTime: message.receivedDateTime,
from: message.from,
body: message.body,
},
});
} catch (error) {
sendGraphError(response, error);
}
});
app.post('/api/users/random', async (_request, response) => {
try {
const user = await createRandomUser();
response.status(201).json({ user });
} catch (error) {
sendGraphError(response, error);
}
});
app.listen(Number(PORT), () => {
console.log(`Office 365 mail console listening on http://localhost:${PORT}`);
console.log(`Access password protection: ${authEnabled ? 'enabled' : 'disabled'}`);