Dear Harbour friends,
I would like to share TInstinct, a small Harbour class based on Instinct. It lets an application exchange messages by email with the user's own Instinct assistant. Instinct is the assistant behind this approach; TInstinct is the Harbour-side transport class, not a separate AI model.
If you would like to try Instinct, the invitation link is:
The idea is to bring high-quality AI and access to agents into our Harbour applications without adding a separate model API integration. My aim is to make this possible at no additional AI API cost, using the user's own assistant. This prototype does not establish Instinct's pricing, account availability or service limits; those depend on the user's account and current service terms.
The class is neutral: there is no hard-coded assistant address, and each user configures their own sender, shared token and destination. It uses Harbour and hbtip, not FiveWin. Console programs and applications with other interfaces can use the same class.
This is a transport prototype, not an official Instinct API or a ready-made agent execution system. Before using it, arrange with your own assistant how requests will be handled and how JSON replies containing "id" and "text" will be returned. Sending an email does not by itself authorize the assistant to act on your behalf.
I have included the usage notes and source code in this message, rather than a ZIP.
1. Three configuration values
-----------------------------
- cFrom: your own email address, from which the application sends requests. Use a mailbox you control and arrange the permitted communication scope with your assistant through your normal trusted conversation.
- cToken: a shared secret. Choose your own value and exchange it securely. Do not publish a real token in source code, documentation or this group. The token alone does not prove identity or authorize actions.
- cTo: the email address of your own Instinct. Ask your assistant for its email address. There is no default destination.
Replace TU_EMAIL, TU_TOKEN and EMAIL_DE_TU_INSTINCT in the examples. The constructor order is always:
```harbour
TInstinct():New( cFrom, cToken, cTo )
```
2. A local SMTP/POP relay is required
------------------------------------
The class connects only to a local relay. The default cHost is "127.0.0.1", with nSmtpPort = 25 and nPopPort = 110. Change the ports to match your relay.
Set cSmtpUser/cSmtpPassword if the relay requires SMTP authentication, and cPopUser/cPopPassword to read replies. The relay must protect the external connection with TLS and validate certificates. TInstinct does not provide that external TLS connection, and a direct unencrypted remote SMTP/POP connection is not supported.
3. Minimal example: send and wait
---------------------------------
Save this example as test.prg:
```harbour
PROCEDURE Main()
LOCAL oInstinct := TInstinct():New( "TU_EMAIL", "TU_TOKEN", ;
"EMAIL_DE_TU_INSTINCT" )
LOCAL cRespuesta
IF ! oInstinct:Send( "Hola, Instinct" )
? oInstinct:cError
RETURN
ENDIF
cRespuesta := oInstinct:Answer() // Waits up to 30 seconds.
IF cRespuesta == NIL
? oInstinct:cError
ELSE
? cRespuesta
ENDIF
RETURN
```
Send() returns .T. when the relay accepts the email, not when Instinct has received it. The request identifier is stored in cId.
Answer() waits for the reply to that request. Use Answer( 60 ) to wait up to 60 seconds. It returns the reply text, or NIL on failure/timeout. Do not blindly resend after an uncertain send result: the relay may already have accepted the message.
4. Callback and timer-based polling
-----------------------------------
For an application with a user interface, register a callback with OnAnswer() and call Pump() from its timer or idle loop.
Save this second example as test_async.prg. Its loop calls Pump() once a second for up to 30 seconds:
```harbour
PROCEDURE Main()
LOCAL oInstinct := TInstinct():New( "TU_EMAIL", "TU_TOKEN", ;
"EMAIL_DE_TU_INSTINCT" )
LOCAL lRespondido := .F.
LOCAL nInicio, nSiguiente
oInstinct:OnAnswer( {| cTexto, cId | ;
MostrarRespuesta( cTexto, cId ), lRespondido := .T. } )
IF ! oInstinct:Send( "Resume mis tareas pendientes" )
? oInstinct:cError
RETURN
ENDIF
nInicio := hb_MilliSeconds()
nSiguiente := nInicio
DO WHILE ! lRespondido .AND. hb_MilliSeconds() - nInicio < 30000
IF hb_MilliSeconds() >= nSiguiente
oInstinct:Pump()
IF ! Empty( oInstinct:cError )
? oInstinct:cError
EXIT
ENDIF
nSiguiente := hb_MilliSeconds() + 1000
ENDIF
hb_IdleSleep( 0.05 )
ENDDO
RETURN
FUNCTION MostrarRespuesta( cTexto, cId )
? cId, cTexto
RETURN NIL
```
OnAnswer() receives the text and identifier of each new reply. Pump() does not wait for a reply to arrive, but the POP connection and polling operation can still take time. It is polling, not a background worker thread.
5. Build, errors and log
-----------------------
Compile each example separately with Harbour and hbtip:
```sh
hbmk2 test.prg TInstinct.prg hbtip.hbc -gtstd
hbmk2 test_async.prg TInstinct.prg hbtip.hbc -gtstd
```
JSON support belongs to the Harbour runtime; no hbjson.hbc dependency is needed.
- cError contains the last error. Check it immediately after the operation.
- cLog records sends, polls, replies and errors.
- SaveLog( "miapp.log" ) saves the log and returns .T. or .F. Without an argument it uses tinstinct.log.
The class handles JSON, simple text/plain MIME replies and persistent POP UIDL deduplication. It does not delete mail from the reply mailbox. Use a dedicated mailbox and archive processed messages. Parsing a JSON reply is not sender authentication, and reply text should not be treated as automatic permission to execute actions.
Mailbox and pending replies
--------------------------
Use a dedicated POP mailbox and only one instance of the class per mailbox. PollOnce( nLimit ) processes up to nLimit new messages per call (20 by default, range 1 to 100), skipping UIDLs already processed even if they occupy the beginning of the mailbox. Subsequent calls reach the rest. UIDL state is saved in cUidlFile; no mail is deleted from the server. Archive processed mail periodically to avoid scanning an ever-growing mailbox.
If Answer() waits for ID A and receives ID B first, it preserves B in aPending. A later Answer( seconds, "B" ) can collect it; Pump() also delivers pending replies to the callback. Pump() without a callback does not consume mail. This queue lives in the instance, not on disk: consume it before closing the application. UIDL persistence prevents duplicate processing, but does not provide a durable response queue across restarts.
The complete corrected class and a standalone Harbour demo appear below. These are the locally validated sources; their addition to FWH is pending.
I would appreciate your feedback, especially test results, problems you find and suggestions for technical improvements.
Best regards,
Antonio Linares
SOURCE CODE
===========
TInstinct.prg
-------------
```harbour
/* TInstinct: prototipo de transporte, NO una API oficial de Instinct.
*
* Cada usuario habla con SU propio Instinct: configura el destino con la
* direccion del buzon de tu asistente (preguntale a tu Instinct por ella)
* y usa tu propio token compartido. Sin destino configurado, Send() falla.
* La libreria es neutral: no lleva la direccion de ningun usuario concreto.
*
* La clase absorbe toda la fontaneria (JSON, MIME, SMTP, POP, deduplicacion
* UIDL, espera de respuesta y log) y el programa queda en tres pasos:
*
* oInstinct:Send( "Hola Instinct" ) // .T./.F.; el id queda en oInstinct:cId
* ? oInstinct:Answer() // espera y devuelve lo recibido
*
* En una app con interfaz no conviene bloquear: registra un bloque con
* oInstinct:OnAnswer( {| cTexto, cId | ... } ) y llama a oInstinct:Pump()
* desde un timer o el bucle idle; el bloque se dispara con cada respuesta.
*
* La clase guarda un log de todo lo ocurrido (envios, sondeos, respuestas y
* errores recibidos del servidor) en oInstinct:cLog; oInstinct:SaveLog() lo
* vuelca a disco y oInstinct:cError conserva el ultimo error.
*
* Compilar la demo: hbmk2 test.prg TInstinct.prg hbtip.hbc -gtstd
* Compilar la bateria: hbmk2 test_full.prg TInstinct.prg hbtip.hbc -gtstd
* JSON pertenece al runtime de Harbour; no existe dependencia hbjson.hbc.
* Solo acepta un relay local. El relay debe proteger el salto externo con TLS
* y validar certificados. Nunca enviar tokens/passwords por SMTP/POP remoto
* sin cifrar. El token del JSON NO demuestra identidad ni autoriza acciones.
* El receptor y sus respuestas JSON necesitan un acuerdo/implementacion aparte.
*/
#ifdef __XHARBOUR__
#xcommand BEGIN SEQUENCE WITH <b> => TRY
#xcommand RECOVER USING <e> => CATCH <e>
#xcommand END SEQUENCE => END
#xtranslate hb_eol() => ( Chr( 13 ) + Chr( 10 ) )
#xtranslate hb_MemoRead( <f> ) => MemoRead( <f> )
#endif
CLASS TInstinct
/* Configuracion: valores por defecto razonables; tocar solo lo necesario. */
DATA cHost INIT "127.0.0.1" // relay local; jamas un host remoto
DATA nSmtpPort INIT 25 // puerto SMTP del relay local
DATA nPopPort INIT 110 // puerto POP3 del buzon de respuestas
DATA cSmtpUser INIT "" // auth SMTP opcional (LOGIN)
DATA cSmtpPassword INIT ""
DATA cPopUser INIT "" // auth POP3 del buzon
DATA cPopPassword INIT ""
DATA cFrom INIT "" // remitente (quien envia)
DATA cTo INIT "" // buzon del Instinct del usuario: obligatorio
DATA cToken INIT "" // secreto compartido con su Instinct
DATA cApp INIT "Harbour" // etiqueta de la aplicacion
DATA nTimeout INIT 5000 // ms de espera de conexion
DATA nMaxBytes INIT 1048576 // tope de tamano de un correo
DATA nPollEvery INIT 500 // ms entre sondeos en Answer()
DATA cUidlFile INIT "tinstinct.uidl" // deduplicacion persistente
/* Estado y resultados. */
DATA cId INIT "" // id del ultimo Send() aceptado
DATA cSentAt INIT "" // sello UTC del ultimo Send()
DATA cError INIT "" // ultimo error recibido
DATA aWarnings INIT {} // correos no validos vistos al sondear
DATA cLog INIT "" // log de actividad (ver SaveLog())
DATA hSeen INIT { => } // UIDL ya tratados (en memoria)
DATA aPending INIT {} // replies waiting for another Answer/Pump
DATA bOnAnswer INIT NIL // bloque async: {| cTexto, cId | ... }
DATA lSeenLoaded INIT .F. // hSeen ya cargado de disco
METHOD New( cFrom, cToken, cTo ) CONSTRUCTOR
METHOD Send( cText, cId ) // -> .T./.F.; el id queda en ::cId
METHOD Answer( nSeconds, cId ) // -> texto recibido o NIL (espera)
METHOD OnAnswer( bBlock ) // registra el bloque para Pump()
METHOD Pump() // timer/idle: dispara bOnAnswer
METHOD PollOnce( nLimit ) // -> array de respuestas NUEVAS o NIL
METHOD ParseReply( cRaw, cExpectedId )
METHOD LogAdd( cMsg ) // anota una linea en ::cLog
METHOD SetError( cMsg ) // fija ::cError y lo anota en el log
METHOD SaveLog( cFile ) // vuelca ::cLog a disco
METHOD LoadSeen() // carga ::hSeen de ::cUidlFile
METHOD SaveSeen() // guarda ::hSeen en ::cUidlFile
ENDCLASS
METHOD New( cFrom, cToken, cTo ) CLASS TInstinct
::cFrom := InstinctDefault( cFrom, "" )
::cToken := InstinctDefault( cToken, "" )
::cTo := InstinctDefault( cTo, "" )
::LogAdd( "TInstinct listo: from=" + ::cFrom + " to=" + ::cTo )
RETURN Self
METHOD Send( cText, cId ) CLASS TInstinct
LOCAL oURL, oSMTP, oMail, hPayload, lOK := .F., oErr, cRaw
::cError := ""
IF ! LocalHost( ::cHost )
::SetError( "Se requiere relay local; transporte remoto sin TLS prohibido" )
RETURN .F.
ENDIF
IF Empty( ::cTo )
::SetError( "Falta el destino: configura cTo con la direccion de tu propio Instinct (pregunta a tu Instinct por ella)" )
RETURN .F.
ENDIF
IF ! Mailbox( ::cFrom ) .OR. ! Mailbox( ::cTo ) .OR. ! HB_ISSTRING( cText )
::SetError( "Remitente, destino o texto no valido" )
RETURN .F.
ENDIF
IF cId == NIL
cId := Lower( hb_StrToHex( hb_randStr( 16 ) ) ) // ID automatico
ENDIF
IF ! SafeId( cId )
::SetError( "ID no valido (1-128 caracteres: letras, cifras, guion, punto, _)" )
RETURN .F.
ENDIF
#ifdef __XHARBOUR__
::cSentAt := TI_UtcStamp()
#else
::cSentAt := StrTran( hb_TSToStr( hb_DateTime() - hb_UTCOffset() / 86400 ), " ", "T" ) + "Z"
#endif
hPayload := { "app" => ::cApp, "id" => cId, "text" => cText, ;
"token" => ::cToken, "sent_at" => ::cSentAt }
oMail := TIPMail():New( hb_jsonEncode( hPayload ), "base64" )
oMail:SetHeader( "TInstinct " + cId, ::cFrom, ::cTo )
oMail:hHeaders[ "Content-Type" ] := 'text/plain; charset="UTF-8"'
cRaw := oMail:ToString()
IF Len( cRaw ) > ::nMaxBytes
::SetError( "Mensaje demasiado grande" )
RETURN .F.
ENDIF
/* Asignar campos evita meter passwords en URLs o trazas. */
oURL := TUrl():New( "smtp://" + ::cHost )
oURL:nPort := ::nSmtpPort
BEGIN SEQUENCE WITH {| e | Break( e ) }
oSMTP := TInstinctSMTP():New( oURL, .F. )
oSMTP:nConnTimeout := ::nTimeout
IF oSMTP:OpenSecure()
lOK := .T.
IF ! Empty( ::cSmtpUser )
lOK := oSMTP:Auth( ::cSmtpUser, ::cSmtpPassword )
ENDIF
IF lOK
lOK := oSMTP:Mail( ::cFrom )
ENDIF
IF lOK
lOK := oSMTP:Rcpt( ::cTo )
ENDIF
IF lOK
lOK := oSMTP:Data( cRaw )
ENDIF
ENDIF
IF ! lOK
::SetError( "SMTP rechazado o conexion fallida: " + InstinctDefault( oSMTP:cReply, "sin respuesta" ) )
ENDIF
RECOVER USING oErr
::SetError( "SMTP: " + oErr:Description )
END SEQUENCE
IF HB_ISOBJECT( oSMTP )
oSMTP:Close()
ENDIF
IF lOK
::cId := cId
::LogAdd( "SEND ok id=" + cId + " bytes=" + hb_ntos( Len( cRaw ) ) )
ENDIF
/* Exito significa aceptacion del relay, no llegada ni respuesta de Instinct.
* Ante un timeout final, NO reintentar a ciegas: pudo haberse aceptado. */
RETURN lOK
METHOD Answer( nSeconds, cId ) CLASS TInstinct
LOCAL nStart := InstinctMillis(), aReplies, hItem, cPollError := "", n
nSeconds := InstinctDefault( nSeconds, 30 )
cId := InstinctDefault( cId, ::cId )
IF ! HB_ISNUMERIC( nSeconds ) .OR. nSeconds < 1 .OR. nSeconds > 3600
::SetError( "Segundos de espera fuera de 1..3600" )
RETURN NIL
ENDIF
IF ! SafeId( cId )
::SetError( "No hay ID de envio que esperar (falta Send()?)" )
RETURN NIL
ENDIF
::LogAdd( "ANSWER esperando id=" + cId + " hasta " + hb_ntos( nSeconds ) + " s" )
DO WHILE .T.
FOR n := 1 TO Len( ::aPending )
IF ::aPending[ n ][ "reply" ][ "id" ] == cId
hItem := ::aPending[ n ]
ADel( ::aPending, n )
ASize( ::aPending, Len( ::aPending ) - 1 )
::LogAdd( "ANSWER recibido id=" + cId )
RETURN hItem[ "reply" ][ "text" ]
ENDIF
NEXT
IF InstinctElapsed( nStart ) >= nSeconds * 1000
EXIT
ENDIF
aReplies := ::PollOnce()
IF HB_ISARRAY( aReplies )
cPollError := ""
FOR EACH hItem IN aReplies
AAdd( ::aPending, hItem )
NEXT
IF ! Empty( aReplies )
LOOP // inspect the queue without losing replies for other IDs
ENDIF
ELSE
cPollError := ::cError // guardar: reintentamos hasta el limite
ENDIF
IF InstinctElapsed( nStart ) >= nSeconds * 1000
EXIT
ENDIF
hb_IdleSleep( ::nPollEvery / 1000 )
ENDDO
::SetError( iif( Empty( cPollError ), ;
"Tiempo agotado esperando la respuesta " + cId, cPollError ) )
RETURN NIL
/* Registra el bloque que recibira cada respuesta nueva: {| cTexto, cId | }. */
METHOD OnAnswer( bBlock ) CLASS TInstinct
IF bBlock != NIL .AND. ! HB_ISBLOCK( bBlock )
::SetError( "OnAnswer espera un bloque de codigo" )
RETURN .F.
ENDIF
::bOnAnswer := bBlock
RETURN .T.
/* Llamar desde un timer o el bucle idle: sondea UNA vez (sin esperar) y
* dispara el bloque de OnAnswer con cada respuesta nueva. Devuelve cuantas
* se entregaron; ante un error de sondeo devuelve 0 y fija ::cError. */
METHOD Pump() CLASS TInstinct
LOCAL aReplies, hItem, nFired := 0
IF ! HB_ISBLOCK( ::bOnAnswer )
RETURN 0 // no receiver: do not consume replies
ENDIF
aReplies := ::PollOnce()
IF HB_ISARRAY( aReplies )
FOR EACH hItem IN aReplies
AAdd( ::aPending, hItem )
NEXT
ENDIF
DO WHILE ! Empty( ::aPending ) .AND. HB_ISBLOCK( ::bOnAnswer )
hItem := ::aPending[ 1 ]
ADel( ::aPending, 1 )
ASize( ::aPending, Len( ::aPending ) - 1 )
Eval( ::bOnAnswer, hItem[ "reply" ][ "text" ], hItem[ "reply" ][ "id" ] )
nFired++
ENDDO
RETURN nFired
METHOD PollOnce( nLimit ) CLASS TInstinct
LOCAL oURL, oPOP, aReplies := {}, nCount, n, cUID, cRaw, hReply, oErr, lNew := .F., nProcessed := 0
::cError := ""
IF ! LocalHost( ::cHost )
::SetError( "POP requiere proxy local; POP remoto sin TLS prohibido" )
RETURN NIL
ENDIF
::aWarnings := {}
nLimit := InstinctDefault( nLimit, 20 )
IF nLimit < 1 .OR. nLimit > 100
::SetError( "Limite POP fuera de 1..100" )
RETURN NIL
ENDIF
::LoadSeen()
oURL := TUrl():New( "pop://" + ::cHost )
oURL:nPort := ::nPopPort
oURL:cUserid := ::cPopUser
oURL:cPassword := ::cPopPassword
BEGIN SEQUENCE WITH {| e | Break( e ) }
oPOP := TIPClientPOP():New( oURL, .F. )
oPOP:nConnTimeout := ::nTimeout
IF ! oPOP:Open()
Break( InstinctError( "Conexion/autenticacion POP fallida" ) )
ENDIF
nCount := oPOP:CountMail()
IF nCount < 0
Break( InstinctError( "STAT POP fallido" ) )
ENDIF
FOR n := 1 TO nCount
cUID := oPOP:UIDL( n )
IF ! HB_ISSTRING( cUID ) .OR. Empty( cUID )
Break( InstinctError( "UIDL POP fallido" ) )
ENDIF
IF SafeUid( cUID ) .AND. hb_HHasKey( ::hSeen, cUID )
LOOP // ya tratado en un sondeo anterior
ENDIF
IF nProcessed >= Int( nLimit )
EXIT
ENDIF
nProcessed++
oPOP:Reset()
/* Retrieve limitado: no descargar indefinidamente mensajes ajenos. */
cRaw := oPOP:Retrieve( n, ::nMaxBytes + 1 )
IF ! HB_ISSTRING( cRaw ) .OR. Len( cRaw ) > ::nMaxBytes
Break( InstinctError( "RETR fallido o mensaje demasiado grande" ) )
ENDIF
hReply := ::ParseReply( cRaw )
IF SafeUid( cUID )
::hSeen[ cUID ] := .T.
lNew := .T.
ENDIF
IF HB_ISHASH( hReply )
AAdd( aReplies, { "uid" => cUID, "reply" => hReply } )
ELSE
AAdd( ::aWarnings, { "uid" => cUID, "error" => ::cError } )
ENDIF
::cError := ""
NEXT
RECOVER USING oErr
::SetError( "POP: " + oErr:Description )
aReplies := NIL
END SEQUENCE
IF HB_ISOBJECT( oPOP )
oPOP:Close()
ENDIF
IF HB_ISARRAY( aReplies )
::LogAdd( "POLL nuevos=" + hb_ntos( Len( aReplies ) ) + ;
" avisos=" + hb_ntos( Len( ::aWarnings ) ) )
ENDIF
IF lNew
::SaveSeen()
ENDIF
/* No DELE: el buzon conserva el correo y la deduplicacion es por UIDL
* persistido en ::cUidlFile. Usar un buzon dedicado y archivar lo tratado. */
RETURN aReplies
METHOD ParseReply( cRaw, cExpectedId ) CLASS TInstinct
LOCAL oMail, hReply, cBody, nUsed, oErr
::cError := ""
IF ! HB_ISSTRING( cRaw ) .OR. Len( cRaw ) > ::nMaxBytes
::SetError( "Correo ausente o demasiado grande" )
RETURN NIL
ENDIF
BEGIN SEQUENCE WITH {| e | Break( e ) }
#ifdef __XHARBOUR__
/* TIPMail on xHarbour only consumes complete CRLF lines. */
IF Right( cRaw, 2 ) != Chr( 13 ) + Chr( 10 )
cRaw += Chr( 13 ) + Chr( 10 )
ENDIF
#endif
oMail := TIPMail():New()
IF oMail:FromString( cRaw ) <= 0 .OR. oMail:IsMultiPart() .OR. ;
!( Lower( oMail:GetContentType() ) == "text/plain" )
::SetError( "Se espera un correo MIME simple text/plain" )
Break( InstinctError( ::cError ) )
ENDIF
cBody := oMail:GetBody()
DO WHILE Len( cBody ) > 0 .AND. Asc( Right( cBody, 1 ) ) <= 32
cBody := Left( cBody, Len( cBody ) - 1 )
ENDDO
DO WHILE Len( cBody ) > 0 .AND. Asc( Left( cBody, 1 ) ) <= 32
cBody := SubStr( cBody, 2 )
ENDDO
nUsed := hb_jsonDecode( cBody, @hReply )
IF nUsed != Len( cBody ) .OR. ! HB_ISHASH( hReply )
::SetError( "JSON incompleto/no valido" )
Break( InstinctError( ::cError ) )
ENDIF
IF ! hb_HHasKey( hReply, "id" ) .OR. ! hb_HHasKey( hReply, "text" )
::SetError( "Respuesta sin id/text" )
Break( InstinctError( ::cError ) )
ENDIF
IF ! SafeId( hReply[ "id" ] ) .OR. ! HB_ISSTRING( hReply[ "text" ] )
::SetError( "Tipos id/text no validos" )
Break( InstinctError( ::cError ) )
ENDIF
IF cExpectedId != NIL .AND. !( hReply[ "id" ] == cExpectedId )
::SetError( "ID no corresponde a la peticion" )
Break( InstinctError( ::cError ) )
ENDIF
RECOVER USING oErr
::SetError( "MIME/JSON: " + oErr:Description )
RETURN NIL
END SEQUENCE
/* Parsear no autentica al autor. No ejecutar instrucciones de este hash. */
RETURN hReply
/* Cada linea del log lleva fecha y hora local: que ocurrio y cuando. */
METHOD LogAdd( cMsg ) CLASS TInstinct
::cLog += DToC( Date() ) + " " + Time() + " " + cMsg + hb_eol()
RETURN NIL
METHOD SetError( cMsg ) CLASS TInstinct
::cError := cMsg
::LogAdd( "ERROR: " + cMsg )
RETURN NIL
METHOD SaveLog( cFile ) CLASS TInstinct
cFile := InstinctDefault( cFile, "tinstinct.log" )
::LogAdd( "LOG guardado en " + cFile )
RETURN hb_MemoWrit( cFile, ::cLog )
METHOD LoadSeen() CLASS TInstinct
LOCAL cLine
IF ::lSeenLoaded
RETURN NIL
ENDIF
::lSeenLoaded := .T.
IF hb_FileExists( ::cUidlFile )
FOR EACH cLine IN hb_ATokens( StrTran( hb_MemoRead( ::cUidlFile ), ;
Chr( 13 ) + Chr( 10 ), Chr( 10 ) ), Chr( 10 ) )
IF SafeUid( cLine )
::hSeen[ cLine ] := .T.
ENDIF
NEXT
ENDIF
RETURN NIL
METHOD SaveSeen() CLASS TInstinct
LOCAL cUid, cOut := ""
FOR EACH cUid IN hb_HKeys( ::hSeen )
cOut += cUid + hb_eol()
NEXT
RETURN hb_MemoWrit( ::cUidlFile, cOut )
/* hbtip original admite algunos 4xx como OK; aqui se rechazan. */
CLASS TInstinctSMTP FROM TIPClientSMTP
#ifdef __XHARBOUR__
METHOD OpenSecure( cUrl )
#endif
METHOD GetOK()
METHOD Data( cData )
ENDCLASS
#ifdef __XHARBOUR__
METHOD OpenSecure( cUrl ) CLASS TInstinctSMTP
/* xHarbour's OpenSecure sends EHLO but returns the greeting, leaving
* its 250 reply unread. Consume that reply before MAIL/AUTH. */
IF ! ::super:OpenSecure( cUrl ) .OR. !( Left( ::cReply, 3 ) == "220" )
RETURN .F.
ENDIF
DO WHILE .T.
IF ! ::GetOK() .OR. !( Left( ::cReply, 3 ) == "250" )
RETURN .F.
ENDIF
IF SubStr( ::cReply, 4, 1 ) != "-"
EXIT
ENDIF
ENDDO
RETURN .T.
#endif
METHOD GetOK() CLASS TInstinctSMTP
LOCAL nCode
::cReply := ::inetRecvLine( ::SocketCon,, 512 )
IF ::inetErrorCode( ::SocketCon ) != 0 .OR. ! HB_ISSTRING( ::cReply )
RETURN .F.
ENDIF
nCode := Val( Left( ::cReply, 3 ) )
RETURN ( nCode >= 200 .AND. nCode < 300 ) .OR. nCode == 334 .OR. nCode == 354
METHOD Data( cData ) CLASS TInstinctSMTP
::inetSendAll( ::SocketCon, "DATA" + ::cCRLF )
IF ! ::GetOK() .OR. !( Left( ::cReply, 3 ) == "354" )
RETURN .F.
ENDIF
IF ::inetSendAll( ::SocketCon, cData + ::cCRLF + "." + ::cCRLF ) < Len( cData ) + 5
RETURN .F.
ENDIF
RETURN ::GetOK() .AND. Left( ::cReply, 3 ) == "250"
STATIC FUNCTION LocalHost( cHost )
RETURN HB_ISSTRING( cHost ) .AND. ( cHost == "127.0.0.1" .OR. cHost == "localhost" )
STATIC FUNCTION Mailbox( cValue )
RETURN HB_ISSTRING( cValue ) .AND. ! Empty( cValue ) .AND. ;
! Empty( hb_regex( "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+$", cValue ) )
STATIC FUNCTION SafeId( cValue )
RETURN HB_ISSTRING( cValue ) .AND. Len( cValue ) >= 1 .AND. Len( cValue ) <= 128 .AND. ;
! Empty( hb_regex( "^[A-Za-z0-9._-]+$", cValue ) )
STATIC FUNCTION SafeUid( cValue )
RETURN HB_ISSTRING( cValue ) .AND. Len( cValue ) >= 1 .AND. Len( cValue ) <= 128 .AND. ;
! Empty( hb_regex( "^[A-Za-z0-9._:@%+-]+$", cValue ) )
STATIC FUNCTION InstinctError( cDescription )
LOCAL oError := ErrorNew()
oError:Description := cDescription
RETURN oError
STATIC FUNCTION InstinctDefault( xValue, xDefault )
RETURN iif( ValType( xValue ) == ValType( xDefault ), xValue, xDefault )
STATIC FUNCTION InstinctMillis()
#ifdef __XHARBOUR__
RETURN TI_MilliSeconds()
#else
RETURN hb_MilliSeconds()
#endif
#ifdef __XHARBOUR__
/* xHarbour has no hb_UTCOffset/hb_MilliSeconds. Use UTC and monotonic
* clocks directly, never label local time as UTC. */
#pragma BEGINDUMP
#include "hbapi.h"
#include <stdio.h>
#if defined( _WIN32 )
#include <windows.h>
#else
#include <time.h>
#endif
HB_FUNC( TI_UTCSTAMP )
{
char stamp[ 32 ];
#if defined( _WIN32 )
SYSTEMTIME utc;
GetSystemTime( &utc );
sprintf( stamp, "%04u-%02u-%02uT%02u:%02u:%02u.%03uZ",
utc.wYear, utc.wMonth, utc.wDay, utc.wHour, utc.wMinute,
utc.wSecond, utc.wMilliseconds );
#else
time_t now = time( NULL );
struct tm utc;
gmtime_r( &now, &utc );
strftime( stamp, sizeof( stamp ), "%Y-%m-%dT%H:%M:%SZ", &utc );
#endif
hb_retc( stamp );
}
HB_FUNC( TI_MILLISECONDS )
{
#if defined( _WIN32 )
hb_retnd( ( double ) GetTickCount() );
#else
struct timespec ts;
clock_gettime( CLOCK_MONOTONIC, &ts );
hb_retnd( ( double ) ts.tv_sec * 1000 + ts.tv_nsec / 1000000 );
#endif
}
#pragma ENDDUMP
#endif
STATIC FUNCTION InstinctElapsed( nStart )
LOCAL nElapsed := InstinctMillis() - nStart
#ifdef __XHARBOUR__
IF nElapsed < 0
nElapsed += 4294967296
ENDIF
#endif
RETURN nElapsed
```
Standalone Harbour demo: test.prg
--------------------------------
```harbour
/* test.prg - demo minima de TInstinct: tres pasos (crear, enviar, recibir).
* Cada usuario configura la clase con SU remitente, SU token y la direccion
* de SU propio Instinct; la clase absorbe JSON, MIME, SMTP, POP,
* deduplicacion UIDL y log.
* Compilar: hbmk2 test.prg TInstinct.prg hbtip.hbc -gtstd
*/
PROCEDURE Main()
LOCAL oInstinct, cAnswer, nTries
hb_cdpSelect( "UTF8" )
/* Configuracion por usuario: remitente, token compartido y la direccion
* del buzon de SU Instinct (cada uno le pregunta a su Instinct por ella). */
oInstinct := TInstinct():New( "a...@example.test", ; // quien envia
"LOCAL-DEMO-NOT-A-SECRET", ; // su token
/* Solo en esta demo: apuntar a los servidores locales de prueba. */
oInstinct:nSmtpPort := 25252
oInstinct:nPopPort := 21110
oInstinct:cPopUser := "local-user"
oInstinct:cPopPassword := "local-password"
IF oInstinct:Send( "Hola Instinct: TInstinct en tres lineas" )
cAnswer := oInstinct:Answer( 10 ) // espera y devuelve lo recibido
OutStd( "Enviado id=" + oInstinct:cId + hb_eol() )
OutStd( iif( cAnswer == NIL, "Sin respuesta: " + oInstinct:cError, ;
"Respuesta: " + cAnswer ) + hb_eol() )
ELSE
OutStd( "Error: " + oInstinct:cError + hb_eol() )
ErrorLevel( 1 )
ENDIF
/* Estilo GUI (opcional): bloque + Pump() desde un timer o bucle idle,
* asi la app no se queda bloqueada esperando la respuesta. */
oInstinct:OnAnswer( {| cTexto, cId | OutStd( "Async [" + cId + "]: " + ;
cTexto + hb_eol() ) } )
IF oInstinct:Send( "y ahora sin bloquear" )
nTries := 0
DO WHILE oInstinct:Pump() == 0 .AND. Empty( oInstinct:cError ) .AND. ++nTries < 20
hb_IdleSleep( 0.5 ) // aqui la app atenderia su interfaz
ENDDO
ENDIF
oInstinct:SaveLog( "demo.log" ) // la clase guarda todo lo ocurrido
RETURN
```