Prava a bezpecnost: spravce firmy uz nemuze nastavit priznak spravce platformy ani clenstvi v cizi firme; pozvanky, konektory a automatizace kontroluji sve pravo; cizi firma v query je 404; zivy stream posila udalosti jen firmam, kterych se tykaji; akce nad ticketem maji kontrolu prava za firmu ticketu a strop viditelnosti; tokeny se nelogujou; limit pokusu na prihlaseni, kontakt a pozvanky; bezpecnostni hlavicky; zachyceni chyb v async handlerech; timing-safe porovnani tokenu. Vykon: audit neskenuje celou kolekci pri kazdem zapisu a konecne maze firemni zaznamy; ticket se uklada jednou misto trikrat; zapisy do Postgresu jsou serializovane podle ID; prava se pocitaji jednou na request; widgety nacitaji tickety jednou; strankovani seznamu; worker je pool misto kol; na webu udalost ze streamu neodmontuje stranku, dotazy maji spolecny debounce a cache, ciselniky drzi typovany sklad. Runtime: opakuji se jen chyby oznacene retryable; smycka nenarazi na strop 50 kroku (novy strop 1000 akci); podminka nad datem funguje; vystup MCP nastroje neprepisuje spoustec; sandbox skriptu firmy nejde opustit; MCP session id se drzi mezi volanimi; incident z kroku patri firme; jedno rozhodnuti o rezimu uloziste; snapshot neprepise soubor po chybe cteni. Refaktory: sdilene typy API v src/shared (web nic nekopiruje, osm rozjetych tvaru sjednoceno); spolecny modul net/guard pro volani ven; formularova vrstva ui/form; rozdeleni Connectors a FlowCanvas; jeden helper pro firmu z query, validaci a CRUD udalosti; pomucky ctx.util pro skripty konektoru; i18n verejneho webu vcetne anglictiny. Nova funkce: zalozeni firmy z registru ARES v Nastaveni (IC nebo nazev, dotazeni IC, DIC, sidla a pravni formy, vyber soucasnych statutarnich zastupcu a prokury, ucty spravce firmy s nahradnim e-mailem IC-poradi@placeholder.cz). Dokumentace: zaznam v 99-zmeny.md a aktualizace 15 dalsich dokumentu. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
112 lines
3.5 KiB
JavaScript
112 lines
3.5 KiB
JavaScript
/**
|
|
* Google Workspace: odeslani e-mailu pres Gmail.
|
|
*
|
|
* Sluzba: https://services.csbot.cz/apps/google-service
|
|
* Endpoint: POST /google/gmail/messages/send
|
|
*
|
|
* Gmail neprijima predmet a telo zvlast. Chce **cely e-mail podle RFC 2822**
|
|
* zakodovany do base64url, takze zpravu sklada skript. Predmet s diakritikou
|
|
* se navic musi zabalit do MIME slova, jinak z nej cestou zbydou otazniky.
|
|
*
|
|
* Kodovani se dela pres `TextEncoder` a `btoa`, ne pres Buffer: jsou to
|
|
* standardni funkce, ktere budou k dispozici i az skripty pobezi v sandboxu.
|
|
*/
|
|
|
|
export const manifest = {
|
|
id: 'google.send-email',
|
|
name: 'Odeslat e-mail',
|
|
description:
|
|
'Pošle e-mail přes Gmail účtu, pod kterým je napojení. Příjemců může být víc, ' +
|
|
'oddělují se čárkou.',
|
|
|
|
inputs: [
|
|
{
|
|
id: 'to',
|
|
label: 'Příjemci',
|
|
type: 'string',
|
|
required: true,
|
|
hint: 'E-maily oddělené čárkou.',
|
|
},
|
|
{ id: 'cc', label: 'Kopie', type: 'string', required: false },
|
|
{ id: 'subject', label: 'Předmět', type: 'string', required: true },
|
|
{ id: 'body', label: 'Text zprávy', type: 'string', required: true, multiline: true },
|
|
{
|
|
id: 'html',
|
|
label: 'Text je HTML',
|
|
type: 'boolean',
|
|
required: false,
|
|
default: false,
|
|
hint: 'Vypnuté odešle zprávu jako čistý text.',
|
|
},
|
|
{
|
|
id: 'from',
|
|
label: 'Odesílatel',
|
|
type: 'string',
|
|
required: false,
|
|
hint: 'Jen když má účet povolený alias. Bez vyplnění pošle Gmail zprávu za vlastníka schránky.',
|
|
},
|
|
],
|
|
|
|
outputs: [
|
|
{ id: 'messageId', label: 'ID zprávy', type: 'string', required: true },
|
|
{ id: 'threadId', label: 'ID konverzace', type: 'string', required: false },
|
|
{ id: 'recipients', label: 'Počet příjemců', type: 'number', required: true },
|
|
],
|
|
|
|
timeoutMs: 30000,
|
|
};
|
|
|
|
/** Base64 z textu. Pres bajty, aby diakritika prezila. */
|
|
function base64(value) {
|
|
const bytes = new TextEncoder().encode(value);
|
|
let binary = '';
|
|
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
return btoa(binary);
|
|
}
|
|
|
|
/** Base64url bez vypln, jak ho chce Gmail. */
|
|
function base64Url(value) {
|
|
return base64(value).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
}
|
|
|
|
/** Hlavicka s diakritikou musi byt MIME slovo, jinak z ni zbydou otazniky. */
|
|
function header(value) {
|
|
for (const character of value) {
|
|
if (character.codePointAt(0) > 127) return `=?UTF-8?B?${base64(value)}?=`;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
export async function run(inputs, ctx) {
|
|
const { pick, text, need, addresses } = ctx.util;
|
|
|
|
// Clovek pise carky, Gmail chce jeden retezec.
|
|
const to = addresses(inputs.to);
|
|
if (to.length === 0) ctx.fail('Nezůstal žádný příjemce, zkontrolujte pole Příjemci.');
|
|
const cc = addresses(inputs.cc);
|
|
|
|
const lines = [
|
|
`To: ${to.join(', ')}`,
|
|
...(cc.length > 0 ? [`Cc: ${cc.join(', ')}`] : []),
|
|
...(inputs.from ? [`From: ${inputs.from}`] : []),
|
|
`Subject: ${header(inputs.subject)}`,
|
|
'MIME-Version: 1.0',
|
|
`Content-Type: text/${inputs.html ? 'html' : 'plain'}; charset="UTF-8"`,
|
|
'Content-Transfer-Encoding: 8bit',
|
|
'',
|
|
inputs.body,
|
|
];
|
|
|
|
const { body } = await ctx.http.post('/google/gmail/messages/send', {
|
|
raw: base64Url(lines.join('\r\n')),
|
|
});
|
|
|
|
ctx.log(`Odesláno ${to.length + cc.length} příjemcům.`);
|
|
|
|
return {
|
|
messageId: need(text(pick(body, 'id')), 'ID zprávy'),
|
|
threadId: text(pick(body, 'threadId')),
|
|
recipients: to.length + cc.length,
|
|
};
|
|
}
|